mirror of
https://github.com/standardebooks/web.git
synced 2025-07-16 03:16:36 -04:00
Allow projects to auto-assign managers and reviewers
This commit is contained in:
parent
e51cc4395e
commit
4596aeb007
12 changed files with 116 additions and 21 deletions
|
@ -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;
|
||||
|
|
5
config/sql/se/ProjectUnassignedUsers.sql
Normal file
5
config/sql/se/ProjectUnassignedUsers.sql
Normal file
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
7
lib/Enums/ProjectRoleType.php
Normal file
7
lib/Enums/ProjectRoleType.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?
|
||||
namespace Enums;
|
||||
|
||||
enum ProjectRoleType: string{
|
||||
case Manager = 'manager';
|
||||
case Reviewer = 'reviewer';
|
||||
}
|
|
@ -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();
|
||||
|
|
40
lib/User.php
40
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".
|
||||
*
|
||||
|
|
|
@ -105,7 +105,7 @@ $isEditForm = $isEditForm ?? false;
|
|||
<span>Type</span>
|
||||
<span>
|
||||
<select name="type-collection-name-1">
|
||||
<option value=""></option>
|
||||
<option value=""> </option>
|
||||
<option value="<?= Enums\CollectionType::Series->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 0 && $ebook->CollectionMemberships[0]->Collection->Type == Enums\CollectionType::Series){ ?> selected="selected"<? } ?>>Series</option>
|
||||
<option value="<?= Enums\CollectionType::Set->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 0 && $ebook->CollectionMemberships[0]->Collection->Type == Enums\CollectionType::Set){ ?> selected="selected"<? } ?>>Set</option>
|
||||
</select>
|
||||
|
@ -139,7 +139,7 @@ $isEditForm = $isEditForm ?? false;
|
|||
<span>Type</span>
|
||||
<span>
|
||||
<select name="type-collection-name-2">
|
||||
<option value=""></option>
|
||||
<option value=""> </option>
|
||||
<option value="<?= Enums\CollectionType::Series->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1 && $ebook->CollectionMemberships[1]->Collection->Type == Enums\CollectionType::Series){ ?> selected="selected"<? } ?>>Series</option>
|
||||
<option value="<?= Enums\CollectionType::Set->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1 && $ebook->CollectionMemberships[1]->Collection->Type == Enums\CollectionType::Set){ ?> selected="selected"<? } ?>>Set</option>
|
||||
</select>
|
||||
|
@ -171,7 +171,7 @@ $isEditForm = $isEditForm ?? false;
|
|||
<span>Type</span>
|
||||
<span>
|
||||
<select name="type-collection-name-3">
|
||||
<option value=""></option>
|
||||
<option value=""> </option>
|
||||
<option value="<?= Enums\CollectionType::Series->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2 && $ebook->CollectionMemberships[2]->Collection->Type == Enums\CollectionType::Series){ ?> selected="selected"<? } ?>>Series</option>
|
||||
<option value="<?= Enums\CollectionType::Set->value ?>"<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2 && $ebook->CollectionMemberships[2]->Collection->Type == Enums\CollectionType::Set){ ?> selected="selected"<? } ?>>Set</option>
|
||||
</select>
|
||||
|
@ -232,7 +232,7 @@ $isEditForm = $isEditForm ?? false;
|
|||
<span>Difficulty</span>
|
||||
<span>
|
||||
<select name="ebook-placeholder-difficulty">
|
||||
<option value=""></option>
|
||||
<option value=""> </option>
|
||||
<option value="<?= Enums\EbookPlaceholderDifficulty::Beginner->value ?>"<? if($ebook->EbookPlaceholder?->Difficulty == Enums\EbookPlaceholderDifficulty::Beginner){ ?> selected="selected"<? } ?>>Beginner</option>
|
||||
<option value="<?= Enums\EbookPlaceholderDifficulty::Intermediate->value ?>"<? if($ebook->EbookPlaceholder?->Difficulty == Enums\EbookPlaceholderDifficulty::Intermediate){ ?> selected="selected"<? } ?>>Intermediate</option>
|
||||
<option value="<?= Enums\EbookPlaceholderDifficulty::Advanced->value ?>"<? if($ebook->EbookPlaceholder?->Difficulty == Enums\EbookPlaceholderDifficulty::Advanced){ ?> selected="selected"<? } ?>>Advanced</option>
|
||||
|
|
|
@ -32,8 +32,14 @@ $isEditForm = $isEditForm ?? false;
|
|||
|
||||
<label class="icon user">
|
||||
<span>Manager</span>
|
||||
<? if(!$isEditForm){ ?>
|
||||
<span>Leave blank to auto-assign.</span>
|
||||
<? } ?>
|
||||
<span>
|
||||
<select name="project-manager-user-id">
|
||||
<? if(!$isEditForm){ ?>
|
||||
<option value=""> </option>
|
||||
<? } ?>
|
||||
<? foreach($managers as $manager){ ?>
|
||||
<option value="<?= $manager->UserId ?>"<? if(isset($project->ManagerUserId) && $project->ManagerUserId == $manager->UserId){ ?> selected="selected"<? } ?>><?= Formatter::EscapeHtml($manager->Name) ?></option>
|
||||
<? } ?>
|
||||
|
@ -43,8 +49,14 @@ $isEditForm = $isEditForm ?? false;
|
|||
|
||||
<label class="icon user">
|
||||
<span>Reviewer</span>
|
||||
<? if(!$isEditForm){ ?>
|
||||
<span>Leave blank to auto-assign.</span>
|
||||
<? } ?>
|
||||
<span>
|
||||
<select name="project-reviewer-user-id">
|
||||
<? if(!$isEditForm){ ?>
|
||||
<option value=""> </option>
|
||||
<? } ?>
|
||||
<? foreach($reviewers as $reviewer){ ?>
|
||||
<option value="<?= $reviewer->UserId ?>"<? if(isset($project->ReviewerUserId) && $project->ReviewerUserId == $reviewer->UserId){ ?> selected="selected"<? } ?>><?= Formatter::EscapeHtml($reviewer->Name) ?></option>
|
||||
<? } ?>
|
||||
|
@ -65,7 +77,7 @@ $isEditForm = $isEditForm ?? false;
|
|||
</label>
|
||||
|
||||
<label>
|
||||
<span>Automatically update status?</span>
|
||||
<span>Automatically update status</span>
|
||||
<input type="hidden" name="project-is-status-automatically-updated" value="false" />
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
@ -1899,11 +1899,11 @@ label:has(select) > span{
|
|||
display: block;
|
||||
}
|
||||
|
||||
label:has(select) > span + span{
|
||||
label:has(select) > span + span:last-child{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
label:has(select) > span + span::after{
|
||||
label:has(select) > span + span:last-child::after{
|
||||
display: block;
|
||||
font-style: normal;
|
||||
position: absolute;
|
||||
|
@ -1927,7 +1927,7 @@ label:has(select):hover > span + span::after{
|
|||
transition: none;
|
||||
}
|
||||
|
||||
label:has(select) > span + span,
|
||||
label:has(select) > span + span:last-child,
|
||||
label:has(input[type="email"]),
|
||||
label:has(input[type="search"]),
|
||||
label:has(input[type="url"]),
|
||||
|
@ -4069,7 +4069,7 @@ nav.breadcrumbs + h1{
|
|||
.ebook-carousel img,
|
||||
.ebook-carousel a:active img,
|
||||
ol.ebooks-list > li a[tabindex]:active,
|
||||
label:has(select) > span + span::after,
|
||||
label:has(select) > span + span:last-child::after,
|
||||
label:has(input[type="search"]):focus-within::before,
|
||||
label:has(input[type="search"]):hover::before,
|
||||
label:has(select):hover > span + span::after,
|
||||
|
|
|
@ -17,7 +17,7 @@ try{
|
|||
throw new Exceptions\EbookNotFoundException();
|
||||
}
|
||||
|
||||
if($isSaved){
|
||||
if($isSaved || $isProjectSaved){
|
||||
session_unset();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,7 @@ try{
|
|||
$project = new Project();
|
||||
$project->FillFromHttpPost();
|
||||
$project->Started = NOW;
|
||||
$project->EbookId = 0; // Dummy value to pass validation, we'll set it to the real value before creating the `Project`.
|
||||
$project->Validate();
|
||||
$project->Validate(true, true);
|
||||
}
|
||||
|
||||
try{
|
||||
|
@ -86,7 +85,7 @@ catch(Exceptions\LoginRequiredException){
|
|||
catch(Exceptions\InvalidPermissionsException | Exceptions\InvalidHttpMethodException | Exceptions\HttpMethodNotAllowedException){
|
||||
Template::ExitWithCode(Enums\HttpCode::Forbidden);
|
||||
}
|
||||
catch(Exceptions\AppException $ex){
|
||||
catch(Exceptions\InvalidEbookException | Exceptions\InvalidProjectException $ex){
|
||||
$_SESSION['ebook'] = $ebook;
|
||||
$_SESSION['exception'] = $ex;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ try{
|
|||
// POSTing a new `Project`.
|
||||
if($httpMethod == Enums\HttpMethod::Post){
|
||||
$project = new Project();
|
||||
|
||||
$project->FillFromHttpPost();
|
||||
|
||||
$project->Create();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue