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 @@ +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 + + + @@ -43,8 +49,14 @@ $isEditForm = $isEditForm ?? false;