znt/module/project/tao.php

1205 lines
47 KiB
PHP
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* The tao file of project module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author sunguangming <sunguangming@easycorp.ltd>
* @link https://www.zentao.net
*/
class projectTao extends projectModel
{
/**
* Update project table when start a project.
*
* @param int $projectID
* @param object $project
* @access protected
* @return bool
*/
protected function doStart(int $projectID, object $project): bool
{
$this->dao->update(TABLE_PROJECT)->data($project, 'comment')
->autoCheck()
->check($this->config->project->start->requiredFields, 'notempty')
->checkIF($project->realBegan != '', 'realBegan', 'le', helper::today())
->checkFlow()
->where('id')->eq($projectID)
->exec();
return !dao::isError();
}
/**
* Update project table when suspend a project.
*
* @param int $projectID
* @param object $project
*
* @access protected
* @return bool
*/
protected function doSuspend(int $projectID, object $project): bool
{
$this->dao->update(TABLE_PROJECT)->data($project)
->autoCheck()
->checkFlow()
->where('id')->eq($projectID)
->exec();
return !dao::isError();
}
/**
* Update project table when close a project.
*
* @param int $projectID
* @param object $project
* @param object $oldProject
*
* @access protected
* @return bool
*/
protected function doClosed(int $projectID, object $project, object $oldProject): bool
{
$this->lang->error->ge = $this->lang->project->ge;
$this->dao->update(TABLE_PROJECT)->data($project)
->autoCheck()
->check($this->config->project->close->requiredFields, 'notempty')
->checkIF($project->realEnd != '', 'realEnd', 'le', helper::today())
->checkIF($project->realEnd != '', 'realEnd', 'ge', $oldProject->realBegan)
->checkFlow()
->where('id')->eq($projectID)
->exec();
return !dao::isError();
}
/**
* 激活项目时更新项目的信息。
* Update project table when activate a project.
*
* @param int $projectID
* @param object $project
* @access protected
* @return bool
*/
protected function doActivate(int $projectID ,object $project): bool
{
$this->dao->update(TABLE_PROJECT)->data($project , 'readjustTime, readjustTask, comment')
->autoCheck()
->checkFlow()
->where('id')->eq((int)$projectID)
->exec();
return !dao::isError();
}
/**
* 更新项目主表。
* Update project table when edit a project.
*
* @param int $projectID
* @param object $project
* @access protected
* @return bool
*/
public function doUpdate(int $projectID, object $project): bool
{
$project->id = $projectID;
$this->dao->update(TABLE_PROJECT)->data($project)
->autoCheck('begin,end')
->check('end', 'gt', $project->begin)
->checkIF(!empty($project->name), 'name', 'unique', "id != $projectID and `type` = 'project' and `parent` = '$project->parent' and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
->checkIF(!empty($project->code), 'code', 'unique', "id != $projectID and `type` = 'project' and `parent` = '$project->parent' and `deleted` = '0'")
->checkFlow()
->where('id')->eq($projectID)
->exec();
return !dao::isError();
}
/**
* 新增项目。
* Insert a project to project table.
*
* @param object $project
* @access protected
* @return bool
*/
protected function doCreate(object $project): bool
{
$this->lang->error->unique = $this->lang->error->repeat;
$this->dao->insert(TABLE_PROJECT)->data($project)
->autoCheck()
->batchCheck($this->config->project->create->requiredFields, 'notempty')
->checkIF(!empty($project->name), 'name', 'unique', "`type`='project' and `parent` = " . $this->dao->sqlobj->quote($project->parent) . " and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
->checkIF(!empty($project->code), 'code', 'unique', "`type`='project' and `parent` = " . $this->dao->sqlobj->quote($project->parent) . " and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
->checkIF($project->end != '', 'end', 'gt', $project->begin)
->checkFlow()
->exec();
return !dao::isError();
}
/**
* 获取未完成的任务。
* Fetch undone tasks.
*
* @param int $projectID
* @access protected
* @return object[]
*/
protected function fetchUndoneTasks(int $projectID): array
{
return $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK)
->where('deadline')->notZeroDate()
->andWhere('status')->in('wait,doing')
->andWhere('project')->eq($projectID)
->fetchAll();
}
/**
* 更新任务的起止日期。
* Update start and end date of tasks.
*
* @param array $tasks
* @param object $oldProject
* @param object $project
* @access protected
* @return bool
*/
protected function updateTasksStartAndEndDate(array $tasks, object $oldProject, object $project): bool
{
$beginTimeStamp = strtotime($project->begin);
foreach($tasks as $task)
{
if($task->status == 'wait' && !helper::isZeroDate($task->estStarted))
{
$taskDays = helper::diffDate($task->deadline, $task->estStarted);
$taskOffset = helper::diffDate($task->estStarted, $oldProject->begin);
$estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600;
$estStarted = date('Y-m-d', $estStartedTimeStamp);
$deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600);
if($estStarted > $project->end) $estStarted = $project->end;
if($deadline > $project->end) $deadline = $project->end;
$this->dao->update(TABLE_TASK)
->set('estStarted')->eq($estStarted)
->set('deadline')->eq($deadline)
->where('id')->eq($task->id)
->exec();
if(dao::isError()) return false;
}
else
{
$taskOffset = helper::diffDate($task->deadline, $oldProject->begin);
$deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600);
if($deadline > $project->end) $deadline = $project->end;
$this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec();
if(dao::isError()) return false;
}
}
return true;
}
/**
* 插入项目团队成员。
* Insert project members.
*
* @param array $members
* @param int $projectID
* @param array $oldJoin
* @access protected
* @return array
*/
protected function insertMember(array $members, int $projectID, array $oldJoin): array
{
$accounts = array();
foreach($members as $member)
{
if(empty($member->account)) continue;
$account = $member->account;
$accounts[] = $account;
$data = new stdclass();
$data->role = $member->role;
$data->days = $member->days;
$data->hours = $member->hours;
$data->limited = isset($member->limited) ? $member->limited : 'no';
$data->root = $projectID;
$data->account = $account;
$data->join = isset($oldJoin[$account]) ? $oldJoin[$account] : helper::today();
$data->type = 'project';
$this->dao->insert(TABLE_TEAM)->data($data)->exec();
}
return $accounts;
}
/**
* 更新项目团队成员的视图权限
* Update member view when project member changed.
*
* @param int $projectID
* @param array $accounts
* @param array $oldJoin
* @access protected
* @return void
*/
protected function updateMemberView(int $projectID, array $accounts, array $oldJoin): void
{
/* Only changed account update userview. */
$oldAccounts = array_keys($oldJoin);
$removedAccounts = array_diff($oldAccounts, $accounts);
$addedAccounts = array_diff($accounts, $oldAccounts);
$changedAccounts = array_merge($removedAccounts, $addedAccounts);
$changedAccounts = array_unique($changedAccounts);
$childSprints = $this->dao->select('id')->from(TABLE_PROJECT)
->where('project')->eq($projectID)
->andWhere('type')->in('stage,sprint,kanban')
->andWhere('deleted')->eq('0')
->fetchPairs();
$linkedProducts = $this->dao->select("t2.id")->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
->where('t2.deleted')->eq(0)
->andWhere('t1.project')->eq($projectID)
->andWhere('t2.vision')->eq($this->config->vision)
->fetchPairs();
$this->loadModel('user')->updateUserView(array($projectID), 'project', $changedAccounts);
if(!empty($childSprints)) $this->user->updateUserView($childSprints, 'sprint', $changedAccounts);
if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccounts);
if(empty($addedAccounts) && empty($removedAccounts)) return;
/* Log history. */
$actionID = $this->loadModel('action')->create('project', $projectID, 'ManagedTeam');
$users = $this->loadModel('user')->getPairs('noletter');
$addedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $addedAccounts);
$removedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $removedAccounts);
if(!empty($addedAccountList)) $changes[] = array('field' => 'addDiff', 'old' => '', 'new' => '', 'diff' => join(',', $addedAccountList));
if(!empty($removedAccountList)) $changes[] = array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => join(',', $removedAccountList));
if(!empty($changes)) $this->action->logHistory($actionID, $changes);
/* Remove execution members. */
if($this->post->removeExecution == 'yes' and !empty($childSprints) and !empty($removedAccountList))
{
$childTeams = $this->dao->select('root,account')->from(TABLE_TEAM)
->where('root')->in($childSprints)
->andWhere('type')->eq('execution')
->andWhere('account')->in($removedAccounts)
->fetchGroup('root', 'account');
$this->dao->delete()->from(TABLE_TEAM)
->where('root')->in($childSprints)
->andWhere('type')->eq('execution')
->andWhere('account')->in($removedAccounts)
->exec();
foreach($childTeams as $sprintID => $childTeam)
{
$childAccounts = array_keys($childTeam);
$removedAccountList = array_intersect($childAccounts, $removedAccounts);
if(empty($removedAccountList)) continue;
$removedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $removedAccountList);
$changes = array(array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => join(',', $removedAccountList)));
$actionID = $this->action->create('execution', $sprintID, 'syncProjectTeam');
$this->action->logHistory($actionID, $changes);
}
}
}
/**
* 获取项目的详情包含project表的所有内容。
* Get project details, including all contents of the project.
*
* @param int $projectID
* @param string $type
* @access protected
* @return object|false
*/
protected function fetchProjectInfo(int $projectID, string $type = ''): object|false
{
$project = $this->dao->select('*')
->from(TABLE_PROJECT)
->where('id')->eq($projectID)
->beginIF(!empty($type))->andWhere('`type`')->in($type)->fi()
->fetch();
/* Filter the date is empty or 1970. */
if($project && helper::isZeroDate($project->end)) $project->end = '';
return $project;
}
/**
* 创建项目后,创建默认的项目主库。
* Create doclib after create a project.
*
* @param int $projectID
* @param object $project
* @param object $program
* @access protected
* @return bool
*/
protected function createDocLib(int $projectID, object $project, object $program): bool
{
/* Create doc lib. */
$this->app->loadLang('doc');
$authorizedUsers = array();
if($project->parent && $project->acl == 'program')
{
$stakeHolders = $this->loadModel('stakeholder')->getStakeHolderPairs($project->parent);
$authorizedUsers = array_keys($stakeHolders);
foreach(explode(',', $project->whitelist) as $user)
{
if(empty($user)) continue;
$authorizedUsers[$user] = $user;
}
$authorizedUsers[$project->PM] = $project->PM;
$authorizedUsers[$project->openedBy] = $project->openedBy;
$authorizedUsers[$program->PM] = $program->PM;
$authorizedUsers[$program->openedBy] = $program->openedBy;
}
$lib = new stdclass();
$lib->project = $projectID;
$lib->name = $this->lang->doclib->main['project'];
$lib->type = 'project';
$lib->main = '1';
$lib->acl = 'default';
$lib->users = ',' . implode(',', array_filter($authorizedUsers)) . ',';
$lib->vision = zget($project, 'vision', 'rnd');
$lib->addedBy = $this->app->user->account;
$lib->addedDate = helper::now();
$this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
return !dao::isError();
}
/**
* 创建项目时,如果直接输入了产品名,则创建产品并与项目关联。
* Create doclib after create a project.
*
* @param int $projectID
* @param object $project
* @param object $postData
* @param object $program
* @access protected
* @return bool
*/
protected function createProduct(int $projectID, object $project, object $postData, object $program): bool
{
/* If parent not empty, link products or create products. */
$product = new stdclass();
$product->name = $project->hasProduct && !empty($postData->rawdata->productName) ? $postData->rawdata->productName : zget($project, 'name', '');
$product->shadow = zget($project, 'vision', 'rnd') == 'rnd' ? (int)empty($project->hasProduct) : 1;
$product->bind = $postData->rawdata->parent ? 0 : 1;
$product->program = $project->parent ? current(array_filter(explode(',', zget($program, 'path', '')))) : 0;
$product->acl = $project->acl == 'open' ? 'open' : 'private';
$product->PO = $project->PM;
$product->QD = '';
$product->RD = '';
$product->whitelist = '';
$product->createdBy = $this->app->user->account;
$product->createdDate = helper::now();
$product->status = 'normal';
$product->line = 0;
$product->desc = '';
$product->createdVersion = $this->config->version;
$product->vision = zget($project, 'vision', 'rnd');
if($this->config->edition != 'open') $product->workflowGroup = $this->dao->select('id')->from(TABLE_WORKFLOWGROUP)->where('code')->eq('productproject')->fetch('id');
$this->app->loadLang('product');
$this->dao->insert(TABLE_PRODUCT)->data($product)
->check('name', 'notempty')
->checkIF(!empty($product->name), 'name', 'unique', "`program` = {$product->program} and `deleted` = '0'")
->exec();
if(dao::isError()) return false;
$productID = $this->dao->lastInsertId();
/* Update whitelist and user view. */
if(!$project->hasProduct) $this->loadModel('personnel')->updateWhitelist(explode(',', $project->whitelist), 'product', $productID);
$this->loadModel('action')->create('product', $productID, 'opened');
$this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($productID * 5)->where('id')->eq($productID)->exec();
if($product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
$productSettingList = isset($this->config->global->productSettingList) ? json_decode($this->config->global->productSettingList, true) : array();
if($this->config->edition != 'open' && $productSettingList)
{
$productSettingList[] = $productID;
$this->loadModel('setting')->setItem('system.common.global.productSettingList', json_encode($productSettingList));
}
/* Link product. */
$projectProduct = new stdclass();
$projectProduct->project = $projectID;
$projectProduct->product = $productID;
$projectProduct->branch = 0;
$projectProduct->plan = 0;
$this->dao->insert(TABLE_PROJECTPRODUCT)->data($projectProduct)->exec();
if(dao::isError()) return false;
/* Create doc lib. */
if($project->hasProduct) $this->createProductDocLib($productID);
return !dao::isError();
}
/**
* 创建产品后,创建默认的产品主库。
* Create doclib after create a product.
*
* @param int $productID
* @access protected
* @return bool
*/
protected function createProductDocLib(int $productID): bool
{
$this->app->loadLang('doc');
$lib = new stdclass();
$lib->product = $productID;
$lib->name = $this->lang->doclib->main['product'];
$lib->type = 'product';
$lib->main = '1';
$lib->acl = 'default';
$lib->addedBy = $this->app->user->account;
$lib->addedDate = helper::now();
$this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
return !dao::isError();
}
/**
* 获取创建项目时选择的产品数量。
* Get products count from post.
*
* @param object $project
* @param object $rawdata
* @access protected
* @return int
*/
protected function getLinkedProductsCount(object $project, object $rawdata): int
{
if(!isset($project) || !empty($project->newProject)) return 0;
$linkedProductsCount = 0;
if($project->hasProduct && isset($rawdata->products))
{
foreach($rawdata->products as $product)
{
if(empty($product)) continue;
$linkedProductsCount ++;
}
}
return $linkedProductsCount;
}
/**
* 创建项目后,将项目创建者加到项目管理员分组。
* Create project admin after create a project.
*
* @param int $projectID
* @access protected
* @return bool
*/
protected function addProjectAdmin(int $projectID): bool
{
$projectAdmin = $this->dao->select('*')->from(TABLE_PROJECTADMIN)->where('account')->eq($this->app->user->account)->fetch();
if(!empty($projectAdmin) && $projectAdmin->projects != 'all')
{
$newProject = $projectAdmin->projects . ",$projectID";
$this->dao->update(TABLE_PROJECTADMIN)->set('projects')->eq($newProject)->where('account')->eq($projectAdmin->account)->andWhere('`group`')->eq($projectAdmin->group)->exec();
}
else
{
$maxGroupID = $this->dao->select('max(`group`) as maxGroupID')->from(TABLE_PROJECTADMIN)->fetch('maxGroupID');
$projectAdmin = new stdclass();
$projectAdmin->account = $this->app->user->account;
$projectAdmin->group = (int)$maxGroupID + 1;
$projectAdmin->projects = $projectID;
$this->dao->replace(TABLE_PROJECTADMIN)->data($projectAdmin)->exec();
}
return !dao::isError();
}
/**
* 生成project模块的项目下拉框跳转链接.
* Build project link in project page.
*
* @param string $method
* @access protected
* @return string
*/
protected function buildLinkForProject(string $method) :string
{
if($method == 'execution')
return helper::createLink($module, $method, "status=all&projectID=%s");
if($method == 'managePriv')
return helper::createLink($module, 'group', "projectID=%s");
if($method == 'showerrornone')
return helper::createLink('projectstory', 'story', "projectID=%s");
$methods = ',bug,testcase,testtask,testreport,build,dynamic,view,manageproducts,team,managemembers,whitelist,addwhitelist,group,';
if(strpos($methods, ',' . $method . ',') !== false)
return helper::createLink($module, $method, "projectID=%s");
}
/**
* 生成bug模块的项目下拉框跳转链接.
* Build project link in bug page.
*
* @param string $method
* @access protected
* @return string
*/
protected function buildLinkForBug(string $method) :string
{
if($method == 'create')
return helper::createLink($module, $method, "productID=0&branch=0&extras=projectID=%s");
if($method == 'edit')
return helper::createLink('project', 'bug', "projectID=%s");
}
/**
* 生成story模块的项目下拉框跳转链接.
* Build project link in story page.
*
* @param string $method
* @access protected
* @return string
*/
protected function buildLinkForStory(string $method) :string
{
if($method == 'change' || $method == 'create')
return helper::createLink('projectstory', 'story', "projectID=%s");
if($method == 'zerocase')
return helper::createLink('project', 'testcase', "projectID=%s");
}
/**
* 构造批量更新项目的数据。
* Build bathc update project data.
*
* @param array $data
* @param array $oldProjects
* @access public
* @return array
*/
public function buildBatchUpdateProjects(array $data, array $oldProjects): array
{
if(empty($data)) return array();
$extendFields = $this->getFlowExtendFields();
$projects = array();
foreach($data as $projectID => $project)
{
$projectID = (int)$projectID;
$projects[$projectID] = new stdClass();
$projects[$projectID]->id = $projectID;
$projects[$projectID]->name = $project->name;
$projects[$projectID]->model = $oldProjects[$projectID]->model;
$projects[$projectID]->PM = $project->PM;
$projects[$projectID]->begin = $project->begin;
$projects[$projectID]->end = $project->end == $this->lang->project->longTime ? LONG_TIME : $project->end;
$projects[$projectID]->acl = $project->acl;
$projects[$projectID]->days = $project->days;
$projects[$projectID]->lastEditedBy = $this->app->user->account;
$projects[$projectID]->lastEditedDate = helper::now();
if(isset($project->parent)) $projects[$projectID]->parent = $project->parent;
if(isset($project->code)) $projects[$projectID]->code = $project->code;
if($project->end == $this->lang->project->longTime) $projects[$projectID]->days = 0;
foreach($extendFields as $extendField)
{
$projects[$projectID]->{$extendField->field} = $project->{$extendField->field};
if(is_array($projects[$projectID]->{$extendField->field})) $projects[$projectID]->{$extendField->field} = implode(',', $projects[$projectID]->{$extendField->field});
$projects[$projectID]->{$extendField->field} = htmlSpecialString($projects[$projectID]->{$extendField->field});
}
}
return $projects;
}
/**
* 删除项目团队成员。
* Delete project team member.
*
* @param array|int $projectIdList
* @param string $type
* @param string $account
* @param string $realname
* @param array $changes
* @access protected
* @return bool
*/
protected function unlinkTeamMember(int|array $projectIdList, string $type, string $account, string $realname, array $changes): bool
{
$accountProjects = $this->dao->select('root')->from(TABLE_TEAM)
->where('root')->in($projectIdList)
->andWhere('type')->eq($type)
->andWhere('account')->eq($account)
->fetchPairs();
$this->dao->delete()->from(TABLE_TEAM)
->where('root')->in($projectIdList)
->andWhere('type')->eq($type)
->andWhere('account')->eq($account)
->exec();
$this->loadModel('action');
foreach($accountProjects as $projectID)
{
$actionID = $this->action->create($type, $projectID, $type == 'project' ? 'managedTeam' : 'syncProjectTeam');
$this->action->logHistory($actionID, $changes);
}
return !dao::isError();
}
/**
* 对项目按照我的、其它的和关闭的进行分类。
* Classify projects according to my, other and closed.
*
* @param array $projects
* @access protected
* @return array
*/
protected function classifyProjects(array $projects): array
{
$classifiedProjects = array('myProjects' => array(), 'otherProjects' => array(), 'closedProjects' => array());
foreach($projects as $project)
{
if(!str_contains('wait,doing,closed', $project->status)) continue;
/* Convert predefined HTML entities to characters. */
$project->name = htmlspecialchars_decode($project->name, ENT_QUOTES);
$projectPath = explode(',', trim($project->path, ','));
$topProgram = !empty($project->parent) ? $projectPath[0] : $project->parent;
if(!helper::hasFeature('program')) $topProgram = 0;
if($project->PM == $this->app->user->account)
{
if($project->status != 'closed')
{
$classifiedProjects['myProjects'][$topProgram][$project->status][] = $project;
}
else
{
$classifiedProjects['closedProjects']['my'][$topProgram][$project->closedDate] = $project;
}
}
else
{
if($project->status != 'closed')
{
$classifiedProjects['otherProjects'][$topProgram][$project->status][] = $project;
}
else
{
$classifiedProjects['closedProjects']['other'][$topProgram][$project->closedDate] = $project;
}
}
}
return $classifiedProjects;
}
/**
* 对groups列表进行排序和缩减。
* Sort and reduce projects.
*
* @param array $projectsStats
* @param int $retainNum
* @access protected
* @return array
*/
protected function sortAndReduceClosedProjects(array $projectsStats, int $retainNum = 2): array
{
$sortedAndReducedProjects = array('my' => $projectsStats['myProjects'], 'other' => $projectsStats['otherProjects']);
$closedProjects = $projectsStats['closedProjects'];
foreach($closedProjects as $group => $groupedProjects)
{
foreach($groupedProjects as $topProgram => $projects)
{
krsort($projects);
if($retainNum > 0)
{
$sortedAndReducedProjects[$group][$topProgram]['closed'] = array_slice($projects, 0, $retainNum);
}
}
}
return $sortedAndReducedProjects;
}
/**
* 根据项目集ID查询所有项目集的层级。
* Get all program level of a program.
*
* @param int $program
* @param string $path
* @param int $grade
* @access protected
* @return string
*/
protected function getParentProgram(string $path, int $grade): string
{
$programList = $this->dao->select('id,name')->from(TABLE_PROGRAM)
->where('id')->in(trim($path, ','))
->andWhere('grade')->lt($grade)
->orderBy('grade asc')
->fetchPairs();
return implode('/', $programList);
}
/**
* 根据状态和和我参与的查询项目列表。
* Get project list by status and with my participation.
*
* @param string $status
* @param string $orderBy
* @param bool $involved
* @param object $pager
* @access protected
* @return array
*/
protected function fetchProjectList(string $status, string $orderBy, bool $involved, object|null $pager): array
{
$query = '';
if($status == 'bysearch' && $this->session->projectQuery !== false) $query = $this->session->projectQuery;
$query = str_replace('`id`','t1.id', $query);
return $this->dao->select('DISTINCT t1.*')->from(TABLE_PROJECT)->alias('t1')
->leftJoin(TABLE_TEAM)->alias('t2')->on('t1.id=t2.root')
->leftJoin(TABLE_STAKEHOLDER)->alias('t3')->on('t1.id=t3.objectID')
->where('t1.deleted')->eq('0')
->andWhere('t1.vision')->eq($this->config->vision)
->andWhere('t1.type')->eq('project')
->beginIF(!in_array($status, array('all', 'undone', 'review', 'unclosed', 'delayed', 'bysearch'), true))->andWhere('t1.status')->eq($status)->fi()
->beginIF(in_array($status, array('undone', 'unclosed')))->andWhere('t1.status')->in('wait,doing')->fi()
->beginIF($status == 'delayed')->andWhere('t1.status')->notIn('done,closed,suspend')->andWhere('t1.end')->lt(helper::today())->fi()
->beginIF($status == 'review')
->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
->andWhere('t1.reviewStatus')->eq('doing')
->fi()
->beginIF($this->cookie->involved || $involved)
->andWhere('t2.type')->eq('project')
->andWhere('t1.openedBy', true)->eq($this->app->user->account)
->orWhere('t1.PM')->eq($this->app->user->account)
->orWhere('t2.account')->eq($this->app->user->account)
->orWhere('(t3.user')->eq($this->app->user->account)
->andWhere('t3.deleted')->eq(0)
->markRight(1)
->orWhere("CONCAT(',', t1.whitelist, ',')")->like("%,{$this->app->user->account},%")
->markRight(1)
->fi()
->beginIF($status == 'bysearch' && $query)->andWhere($query)->fi()
->orderBy($orderBy)
->page($pager, 't1.id')
->fetchAll('id');
}
/**
* 通过条件查询和数量限制查询项目列表。
* Get project list by query and with limit.
*
* @param string $status
* @param int $projectID
* @param string $orderBy
* @param int $limit
* @param string $excludedModel
* @access protected
* @return array
*/
protected function fetchProjectListByQuery(string $status = 'all', int $projectID = 0, string $orderBy = 'order_asc', int $limit = 0, string $excludedModel = ''): array
{
return $this->dao->select('*')->from(TABLE_PROJECT)
->where('type')->eq('project')
->andWhere('vision')->eq($this->config->vision)
->andWhere('deleted')->eq(0)
->beginIF($projectID)->andWhere('hasProduct')->eq('1')->fi()
->beginIF($projectID)->andWhere('multiple')->eq('1')->fi()
->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi()
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
->beginIF($status == 'unclosed')->andWhere('status')->ne('closed')->fi()
->beginIF($status && !in_array($status, array('all', 'undone', 'unclosed', 'involved')))->andWhere('status')->eq($status)->fi()
->beginIF($status == 'involved')
->andWhere('openedBy', true)->eq($this->app->user->account)
->orWhere('PM')->eq($this->app->user->account)
->orWhere("CONCAT(',', whitelist, ',')")->like("%,{$this->app->user->account},%")
->markRight(1)
->fi()
->beginIF($projectID)->orWhere('id')->eq($projectID)->fi()
->orderBy($orderBy)
->beginIF($limit)->limit($limit)->fi()
->fetchAll('id');
}
/**
* 根据项目ID列表查询团队成员数量。
* Get project team member count by project id list.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function fetchMemberCountByIdList(array $projectIdList): array
{
return $this->dao->select('t1.root, count(t1.id) as count')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.root')->in($projectIdList)
->andWhere('t2.deleted')->eq(0)
->groupBy('t1.root')
->fetchPairs();
}
/**
* 根据项目ID列表查询任务的总预计工时。
* Get task all estimate by project id list.
*
* @param array $projectIdList
* @param string $fields
* @access protected
* @return array
*/
protected function fetchTaskEstimateByIdList(array $projectIdList, string $fields = 'estimate'): array
{
$fields = explode(',', $fields);
$selectFields = 't2.project';
foreach($fields as $field) $selectFields .= ", ROUND(SUM(t1.{$field}), 1) AS {$field}";
return $this->dao->select($selectFields)->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
->where('t1.parent')->lt(1)
->andWhere('t2.project')->in($projectIdList)
->andWhere('t1.deleted')->eq(0)
->andWhere('t2.deleted')->eq(0)
->groupBy('t2.project')
->fetchAll('project');
}
/**
* 通过项目ID列表查询需求的数量。
* Get the number of stories associated with the project.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function getTotalStoriesByProject(array $projectIdList): array
{
return $this->dao->select("t1.project, count(t2.id) as `allStories`, count(if(t2.status = 'active' or t2.status = 'changing', 1, null)) as `leftStories`, count(if(t2.status = 'closed' and t2.`closedReason` = 'done', 1, null)) as `doneStories`")->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id')
->where('t1.project')->in($projectIdList)
->andWhere('deleted')->eq('0')
->groupBy('project')
->fetchAll('project');
}
/**
* 删除团队成员。
* Delete Members.
*
* @param int $projectID
* @param string $openedBy
* @param array $deleteMembers
* @access protected
* @return bool
*/
protected function deleteMembers(int $projectID, string $openedBy, array $deleteMembers): bool
{
$this->dao->delete()->from(TABLE_TEAM)
->where('root')->eq($projectID)
->andWhere('type')->eq('project')
->andWhere('account')->in($deleteMembers)
->andWhere('account')->ne($openedBy)
->exec();
return !dao::isError();
}
/**
* 通过项目ID获取任务数量统计。
* Get the number of tasks associated with the project.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function getTotalTaskByProject(array $projectIdList): array
{
$executionIdList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->in($projectIdList)->andWhere('deleted')->eq(0)->fetchPairs();
return $this->dao->select("project, count(id) as allTasks, count(if(status = 'wait', 1, null)) as waitTasks, count(if(status = 'doing', 1, null)) as doingTasks, count(if(status = 'done', 1, null)) as doneTasks, count(if(status = 'wait' or status = 'pause' or status = 'cancel', 1, null)) as leftTasks, count(if(status = 'done' or status = 'closed', 1, null)) as litedoneTasks")->from(TABLE_TASK)
->where('execution')->in($executionIdList)
->andWhere('deleted')->eq(0)
->groupBy('project')
->fetchAll('project');
}
/**
* 返回请求被拒绝的跳转信息。
* return accessDenied response.
*
* @access protected
* @return string
*/
protected function accessDenied(): string
{
$this->session->set('project', '0');
return $this->app->control->sendError($this->lang->project->accessDenied, helper::createLink('project', 'browse'));
}
/**
* 修改无迭代项目下执行的状态。
* Modify the execution status when changing the status of no execution project.
*
* @param int $projectID
* @param string $status
* @access protected
* @return array|false|int
*/
protected function changeExecutionStatus(int $projectID, string $status): array|false|int
{
if(!in_array($status, array('start', 'suspend', 'activate', 'close'))) return false;
$execution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('multiple')->eq('0')->fetch();
if(!$execution) return false;
$project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
$postData = new stdclass();
$postData->status = $status;
$postData->begin = $execution->begin;
$postData->end = $execution->end;
$postData->uid = '';
$postData->comment = '';
if($status == 'close') $postData->realEnd = $project->realEnd;
return $this->loadModel('execution')->$status($execution->id, $postData);
}
/**
* 通过项目ID获取Bug数量统计。
* Get the number of bugs associated with the project.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function getTotalBugByProject(array $projectIdList): array
{
return $this->dao->select("project, count(id) AS allBugs, count(if(status = 'active', 1, null)) AS leftBugs, count(if(status = 'resolved', 1, null)) AS doneBugs")->from(TABLE_BUG)
->where('project')->in($projectIdList)
->andWhere('deleted')->eq(0)
->groupBy('project')
->fetchAll('project');
}
/**
* 根据项目模式设置菜单。
* Set menu by project model.
*
* @param string $projectModel
* @access protected
* @return bool
*/
protected function setMenuByModel(string $projectModel): bool
{
global $lang, $config, $app;
$model = 'scrum';
if(in_array($projectModel, $this->config->project->waterfallList))
{
$model = 'waterfall';
$lang->project->createExecution = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->createExecution);
$lang->project->lastIteration = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->lastIteration);
$this->loadModel('execution');
$executionCommonLang = $lang->executionCommon;
$lang->executionCommon = $lang->project->stage;
include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
$lang->execution->typeList['sprint'] = $executionCommonLang;
if($app->rawModule == 'project' && $app->rawMethod == 'executionreport') $lang->waterfall->menu->execution['subModule'] .= ',project';
}
elseif($projectModel == 'kanban')
{
$model = 'kanbanProject';
$lang->executionCommon = $lang->project->kanban;
}
if(isset($lang->$model))
{
$key = 'project-' . $model;
if(isset($config->customMenu->$key))
{
$lang->{$model}->menuOrder = array();
$lang->{$model}->dividerMenu = '';
$items = json_decode($config->customMenu->$key);
$prevItem = '';
foreach($items as $item)
{
$lang->{$model}->menuOrder[$item->order] = $item->name;
if($prevItem == 'divider') $lang->{$model}->dividerMenu .= $item->name . ',';
$prevItem = $item->name;
}
if($lang->{$model}->dividerMenu) $lang->{$model}->dividerMenu = ',' . trim($lang->{$model}->dividerMenu, ',') . ',';
}
if($this->config->inCompose)
{
$repoServers = $this->loadModel('pipeline')->getPairs($this->config->pipeline->checkRepoServers);
if(empty($repoServers)) unset($lang->{$model}->menu->devops);
}
$lang->project->menu = $lang->{$model}->menu;
$lang->project->menuOrder = $lang->{$model}->menuOrder;
$lang->project->dividerMenu = $lang->{$model}->dividerMenu;
}
return true;
}
/**
* 根据是否产品型项目设置菜单。
* Set menu by whether product project.
*
* @param int $projectID
* @param int $hasProduct
* @param string $model
* @access protected
* @return bool
*/
protected function setMenuByProduct(int $projectID, int $hasProduct, string $model): bool
{
global $lang;
if($hasProduct)
{
unset($lang->project->menu->projectplan);
unset($lang->project->menu->settings['subMenu']->module);
}
$projectProduct = (int)$this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetch('product');
if(isset($lang->project->menu->settings['subMenu']->module['link'])) $lang->project->menu->settings['subMenu']->module['link'] = sprintf($lang->project->menu->settings['subMenu']->module['link'], $projectProduct);
if(!$hasProduct && in_array($model, $this->config->project->scrumList) && isset($lang->project->menu->projectplan)) $lang->project->menu->projectplan['link'] = sprintf($lang->project->menu->projectplan['link'], $projectProduct);
if(!$hasProduct) unset($lang->project->menu->settings['subMenu']->products);
if(!in_array($model, $this->config->project->scrumList)) unset($lang->project->menu->projectplan);
return true;
}
/**
* 根据访问模块的导航组设置菜单。
* Set menu lang for module.
*
* @param string $navGroup
* @param int $executionID
* @param object $project
* @access protected
* @return bool
*/
protected function setNavGroupMenu(string $navGroup, int $executionID, object $project): bool
{
global $lang;
foreach($lang->$navGroup->menu as $label => $menu)
{
$objectID = 0;
if(strpos($this->config->project->multiple['project'], ",{$label},") !== false) $objectID = $project->id;
if(strpos($this->config->project->multiple['execution'], ",{$label},") !== false)
{
$objectID = $executionID;
$lang->$navGroup->menu->{$label}['subModule'] = 'project';
}
$lang->$navGroup->menu->$label = commonModel::setMenuVarsEx($menu, $objectID);
if(isset($menu['subMenu']))
{
foreach($menu['subMenu'] as $key1 => $subMenu) $lang->$navGroup->menu->{$label}['subMenu']->$key1 = common::setMenuVarsEx($subMenu, $objectID);
}
if(!isset($menu['dropMenu'])) continue;
foreach($menu['dropMenu'] as $key2 => $dropMenu)
{
if(strpos($this->config->project->multiple['project'], ",{$label},") !== false) $objectID = $project->id;
if(strpos($this->config->project->multiple['execution'], ",{$key2},") !== false)
{
$objectID = $executionID;
$lang->$navGroup->menu->{$label}['subModule'] = 'project';
}
$lang->$navGroup->menu->{$label}['dropMenu']->$key2 = common::setMenuVarsEx($dropMenu, $objectID);
if(!isset($dropMenu['subMenu'])) continue;
foreach($dropMenu['subMenu'] as $key3 => $subMenu) $lang->$navGroup->menu->{$label}['dropMenu']->$key3 = common::setMenuVarsEx($subMenu, $objectID);
}
}
return true;
}
/**
* 创建里程碑报告。
*
* @param int $projectID
* @access protected
* @return bool
*/
protected function createMilestoneReport(int $projectID): bool
{
$reportID = $this->dao->select('id')->from(TABLE_DOC)->where('project')->eq($projectID)->andWhere('reportModule')->eq('milestone')->andWhere('type')->eq('projectReport')->fetch('id');
if(!empty($reportID)) return true;
$report = new stdclass();
$report->title = $this->lang->project->milestoneReport;
$report->project = $projectID;
$report->reportModule = 'milestone';
$report->templateType = 'projectReport';
$report->addedBy = 'system';
$report->addedDate = helper::now();
$this->dao->insert(TABLE_DOC)->data($report)->exec();
return !dao::isError();
}
}