diff --git a/config/sql/se/Benefits.sql b/config/sql/se/Benefits.sql
index ff881b4a..ba2af07c 100644
--- a/config/sql/se/Benefits.sql
+++ b/config/sql/se/Benefits.sql
@@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` (
`CanManageProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanReviewProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
+ `CanBeAutoAssignedToProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`UserId`),
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/config/sql/se/ProjectUnassignedUsers.sql b/config/sql/se/ProjectUnassignedUsers.sql
new file mode 100644
index 00000000..02326c65
--- /dev/null
+++ b/config/sql/se/ProjectUnassignedUsers.sql
@@ -0,0 +1,5 @@
+CREATE TABLE `ProjectUnassignedUsers` (
+ `UserId` int(10) unsigned NOT NULL,
+ `Role` enum('manager','reviewer') NOT NULL,
+ UNIQUE KEY `idxUnique` (`Role`,`UserId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git a/lib/Benefits.php b/lib/Benefits.php
index 52d05c6c..4b39fc76 100644
--- a/lib/Benefits.php
+++ b/lib/Benefits.php
@@ -21,6 +21,7 @@ class Benefits{
public bool $CanManageProjects = false;
public bool $CanReviewProjects = false;
public bool $CanEditProjects = false;
+ public bool $CanBeAutoAssignedToProjects = false;
protected bool $_HasBenefits;
@@ -45,6 +46,8 @@ class Benefits{
$this->CanReviewProjects
||
$this->CanEditProjects
+ ||
+ $this->CanBeAutoAssignedToProjects
){
return true;
}
diff --git a/lib/Enums/ProjectRoleType.php b/lib/Enums/ProjectRoleType.php
new file mode 100644
index 00000000..7fbb5082
--- /dev/null
+++ b/lib/Enums/ProjectRoleType.php
@@ -0,0 +1,7 @@
+
+namespace Enums;
+
+enum ProjectRoleType: string{
+ case Manager = 'manager';
+ case Reviewer = 'reviewer';
+}
diff --git a/lib/Project.php b/lib/Project.php
index e4eca54d..d46fbcb6 100644
--- a/lib/Project.php
+++ b/lib/Project.php
@@ -183,10 +183,10 @@ final class Project{
/**
* @throws Exceptions\InvalidProjectException If the `Project` is invalid.
*/
- public function Validate(): void{
+ public function Validate(bool $allowUnsetEbookId = false, bool $allowUnsetRoles = false): void{
$error = new Exceptions\InvalidProjectException();
- if(!isset($this->EbookId)){
+ if(!$allowUnsetEbookId && !isset($this->EbookId)){
$error->Add(new Exceptions\EbookRequiredException());
}
@@ -235,10 +235,10 @@ final class Project{
}
}
- if(!isset($this->ManagerUserId)){
+ if(!$allowUnsetRoles && !isset($this->ManagerUserId)){
$error->Add(new Exceptions\ManagerRequiredException());
}
- else{
+ elseif(isset($this->ManagerUserId)){
try{
$this->_Manager = User::Get($this->ManagerUserId);
}
@@ -247,10 +247,10 @@ final class Project{
}
}
- if(!isset($this->ReviewerUserId)){
- $error->Add(new Exceptions\ManagerRequiredException());
+ if(!$allowUnsetRoles && !isset($this->ReviewerUserId)){
+ $error->Add(new Exceptions\ReviewerRequiredException());
}
- else{
+ elseif(isset($this->ReviewerUserId)){
try{
$this->_Reviewer = User::Get($this->ReviewerUserId);
}
@@ -269,12 +269,39 @@ final class Project{
}
/**
+ * Creates a new `Project`. If `Project::$Manager` or `Project::$Reviewer` are unassigned, they will be automatically assigned.
+ *
* @throws Exceptions\InvalidProjectException If the `Project` is invalid.
* @throws Exceptions\EbookIsNotAPlaceholderException If the `Project`'s `Ebook` is not a placeholder.
* @throws Exceptions\ProjectExistsException If the `Project`'s `Ebook` already has an active `Project`.
+ * @throws Exceptions\UserNotFoundException If a manager or reviewer could not be auto-assigned.
*/
public function Create(): void{
- $this->Validate();
+ if(!isset($this->ManagerUserId)){
+ try{
+ $this->Manager = User::GetByAvailableForProjectAssignment(Enums\ProjectRoleType::Manager, null);
+ }
+ catch(Exceptions\UserNotFoundException){
+ throw new Exceptions\UserNotFoundException('Could not auto-assign a suitable manager.');
+ }
+
+ $this->ManagerUserId = $this->Manager->UserId;
+ }
+
+ if(!isset($this->ReviewerUserId)){
+ try{
+ $this->Reviewer = User::GetByAvailableForProjectAssignment(Enums\ProjectRoleType::Reviewer, $this->Manager->UserId);
+ }
+ catch(Exceptions\UserNotFoundException){
+ unset($this->Manager);
+ unset($this->ManagerUserId);
+ throw new Exceptions\UserNotFoundException('Could not auto-assign a suitable reviewer.');
+ }
+
+ $this->ReviewerUserId = $this->Reviewer->UserId;
+ }
+
+ $this->Validate(false, true);
try{
$this->FetchLastDiscussionTimestamp();
diff --git a/lib/User.php b/lib/User.php
index 042e2720..02209ef2 100644
--- a/lib/User.php
+++ b/lib/User.php
@@ -401,6 +401,46 @@ class User{
', [], User::class);
}
+ /**
+ * Get a random `User` who is available to be assigned to the given role.
+ *
+ * @param Enums\ProjectRoleType $role The role to select for.
+ * @param int $excludedUserId Don't include this `UserId` when selecting; `null` to not exclude any users.
+ *
+ * @throws Exceptions\UserNotFoundException If no `User` is available to be assigned to a `Project`.
+ */
+ public static function GetByAvailableForProjectAssignment(Enums\ProjectRoleType $role, ?int $excludedUserId): User{
+ // First, check if there are `User`s available for assignment.
+ // We use `coalesce()` to allow comparison in case `$excludedUserId` is `null` - there will never be a `UserId` of `0`.
+ $doUnassignedUsersExist = Db::QueryBool('SELECT exists (select * from ProjectUnassignedUsers where Role = ? and UserId != coalesce(?, 0))', [$role, $excludedUserId]);
+
+ // No unassigned `User`s left. Refill the list.
+ if(!$doUnassignedUsersExist){
+ Db::Query('
+ INSERT ignore
+ into ProjectUnassignedUsers
+ (UserId, Role)
+ select
+ Users.UserId,
+ ?
+ from Users
+ inner join Benefits
+ using (UserId)
+ where
+ Benefits.CanManageProjects = true
+ and Benefits.CanBeAutoAssignedToProjects = true
+ ', [$role]);
+ }
+
+ // Now, select a random `User`.
+ $user = Db::Query('SELECT u.* from Users u inner join ProjectUnassignedUsers puu using (UserId) where Role = ? and UserId != coalesce(?, 0) order by rand()', [$role, $excludedUserId], User::class)[0] ?? throw new Exceptions\UserNotFoundException();
+
+ // Delete the `User` we just got from the unassigned users list.
+ Db::Query('DELETE from ProjectUnassignedUsers where UserId = ? and Role = ?', [$user->UserId, $role]);
+
+ return $user;
+ }
+
/**
* Get a `User` if they are considered "registered".
*
diff --git a/templates/EbookPlaceholderForm.php b/templates/EbookPlaceholderForm.php
index a6913ea6..4d95e8da 100644
--- a/templates/EbookPlaceholderForm.php
+++ b/templates/EbookPlaceholderForm.php
@@ -105,7 +105,7 @@ $isEditForm = $isEditForm ?? false;
Type
@@ -139,7 +139,7 @@ $isEditForm = $isEditForm ?? false;
Type
@@ -171,7 +171,7 @@ $isEditForm = $isEditForm ?? false;
Type
@@ -232,7 +232,7 @@ $isEditForm = $isEditForm ?? false;
Difficulty