diff options
Diffstat (limited to 'app')
87 files changed, 1355 insertions, 547 deletions
diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index 1d4a2f13..d70d2ee8 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -76,7 +76,7 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->columnModel->getFirstColumnId($this->getParam('project_id')); - return (bool) $this->taskDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); + return (bool) $this->taskProjectDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } /** diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index 73ad4b69..66635a63 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -75,7 +75,7 @@ class TaskMoveAnotherProject extends Base */ public function doAction(array $data) { - return $this->taskDuplicationModel->moveToProject($data['task_id'], $this->getParam('project_id')); + return $this->taskProjectMoveModel->moveToProject($data['task_id'], $this->getParam('project_id')); } /** diff --git a/app/Api/Procedure/ProjectFileProcedure.php b/app/Api/Procedure/ProjectFileProcedure.php new file mode 100644 index 00000000..48466ce3 --- /dev/null +++ b/app/Api/Procedure/ProjectFileProcedure.php @@ -0,0 +1,68 @@ +<?php + +namespace Kanboard\Api\Procedure; + +use Kanboard\Api\Authorization\ProjectAuthorization; +use Kanboard\Core\ObjectStorage\ObjectStorageException; + +/** + * Project File API controller + * + * @package Kanboard\Api\Procedure + * @author Frederic Guillot + */ +class ProjectFileProcedure extends BaseProcedure +{ + public function getProjectFile($project_id, $file_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectFile', $project_id); + return $this->projectFileModel->getById($file_id); + } + + public function getAllProjectFiles($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllProjectFiles', $project_id); + return $this->projectFileModel->getAll($project_id); + } + + public function downloadProjectFile($project_id, $file_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadProjectFile', $project_id); + + try { + $file = $this->projectFileModel->getById($file_id); + + if (! empty($file)) { + return base64_encode($this->objectStorage->get($file['path'])); + } + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + + return ''; + } + + public function createProjectFile($project_id, $filename, $blob) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createProjectFile', $project_id); + + try { + return $this->projectFileModel->uploadContent($project_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + return false; + } + } + + public function removeProjectFile($project_id, $file_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectFile', $project_id); + return $this->projectFileModel->remove($file_id); + } + + public function removeAllProjectFiles($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAllProjectFiles', $project_id); + return $this->projectFileModel->removeAll($project_id); + } +} diff --git a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php index 5d1988d6..5ceaa08d 100644 --- a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php +++ b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php @@ -5,7 +5,7 @@ namespace Kanboard\Api\Procedure; use Kanboard\Api\Authorization\SubtaskAuthorization; /** - * Subtask Time Tracking API controller + * Subtask Time Tracking API controller * * @package Kanboard\Api\Procedure * @author Frederic Guillot @@ -19,19 +19,19 @@ class SubtaskTimeTrackingProcedure extends BaseProcedure return $this->subtaskTimeTrackingModel->hasTimer($subtask_id, $user_id); } - public function logSubtaskStartTime($subtask_id, $user_id) + public function setSubtaskStartTime($subtask_id, $user_id) { - SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskStartTime', $subtask_id); + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskStartTime', $subtask_id); return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); } - public function logSubtaskEndTime($subtask_id,$user_id) + public function setSubtaskEndTime($subtask_id, $user_id) { - SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskEndTime', $subtask_id); + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskEndTime', $subtask_id); return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id); } - public function getSubtaskTimeSpent($subtask_id,$user_id) + public function getSubtaskTimeSpent($subtask_id, $user_id) { SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSubtaskTimeSpent', $subtask_id); return $this->subtaskTimeTrackingModel->getTimeSpent($subtask_id, $user_id); diff --git a/app/Api/Procedure/TaskExternalLinkProcedure.php b/app/Api/Procedure/TaskExternalLinkProcedure.php new file mode 100644 index 00000000..05ec6906 --- /dev/null +++ b/app/Api/Procedure/TaskExternalLinkProcedure.php @@ -0,0 +1,106 @@ +<?php + +namespace Kanboard\Api\Procedure; + +use Kanboard\Api\Authorization\TaskAuthorization; +use Kanboard\Core\ExternalLink\ExternalLinkManager; +use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound; + +/** + * Task External Link API controller + * + * @package Kanboard\Api\Procedure + * @author Frederic Guillot + */ +class TaskExternalLinkProcedure extends BaseProcedure +{ + public function getExternalTaskLinkTypes() + { + return $this->externalLinkManager->getTypes(); + } + + public function getExternalTaskLinkProviderDependencies($providerName) + { + try { + return $this->externalLinkManager->getProvider($providerName)->getDependencies(); + } catch (ExternalLinkProviderNotFound $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + return false; + } + } + + public function getExternalTaskLinkById($task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLink', $task_id); + return $this->taskExternalLinkModel->getById($link_id); + } + + public function getAllExternalTaskLinks($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLinks', $task_id); + return $this->taskExternalLinkModel->getAll($task_id); + } + + public function createExternalTaskLink($task_id, $url, $dependency, $type = ExternalLinkManager::TYPE_AUTO, $title = '') + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createExternalTaskLink', $task_id); + + try { + $provider = $this->externalLinkManager + ->setUserInputText($url) + ->setUserInputType($type) + ->find(); + + $link = $provider->getLink(); + + $values = array( + 'task_id' => $task_id, + 'title' => $title ?: $link->getTitle(), + 'url' => $link->getUrl(), + 'link_type' => $provider->getType(), + 'dependency' => $dependency, + ); + + list($valid, $errors) = $this->externalLinkValidator->validateCreation($values); + + if (! $valid) { + $this->logger->error(__METHOD__.': '.var_export($errors)); + return false; + } + + return $this->taskExternalLinkModel->create($values); + } catch (ExternalLinkProviderNotFound $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + } + + return false; + } + + public function updateExternalTaskLink($task_id, $link_id, $title = null, $url = null, $dependency = null) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateExternalTaskLink', $task_id); + + $link = $this->taskExternalLinkModel->getById($link_id); + $values = $this->filterValues(array( + 'title' => $title, + 'url' => $url, + 'dependency' => $dependency, + )); + + $values = array_merge($link, $values); + list($valid, $errors) = $this->externalLinkValidator->validateModification($values); + + if (! $valid) { + $this->logger->error(__METHOD__.': '.var_export($errors)); + return false; + } + + return $this->taskExternalLinkModel->update($values); + } + + public function removeExternalTaskLink($task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeExternalTaskLink', $task_id); + return $this->taskExternalLinkModel->remove($link_id); + } +} diff --git a/app/Api/Procedure/TaskFileProcedure.php b/app/Api/Procedure/TaskFileProcedure.php index 5aa7ea0b..bd006578 100644 --- a/app/Api/Procedure/TaskFileProcedure.php +++ b/app/Api/Procedure/TaskFileProcedure.php @@ -30,7 +30,7 @@ class TaskFileProcedure extends BaseProcedure public function downloadTaskFile($file_id) { TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadTaskFile', $file_id); - + try { $file = $this->taskFileModel->getById($file_id); @@ -51,7 +51,7 @@ class TaskFileProcedure extends BaseProcedure try { return $this->taskFileModel->uploadContent($task_id, $filename, $blob); } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); + $this->logger->error(__METHOD__.': '.$e->getMessage()); return false; } } diff --git a/app/Api/Procedure/TaskProcedure.php b/app/Api/Procedure/TaskProcedure.php index 2d29a4ef..8661deef 100644 --- a/app/Api/Procedure/TaskProcedure.php +++ b/app/Api/Procedure/TaskProcedure.php @@ -77,13 +77,13 @@ class TaskProcedure extends BaseProcedure public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'moveTaskToProject', $project_id); - return $this->taskDuplicationModel->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + return $this->taskProjectMoveModel->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); } public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'duplicateTaskToProject', $project_id); - return $this->taskDuplicationModel->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + return $this->taskProjectDuplicationModel->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); } public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php index b9730c5c..fdf936b1 100644 --- a/app/Auth/ReverseProxyAuth.php +++ b/app/Auth/ReverseProxyAuth.php @@ -45,7 +45,8 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac $username = $this->request->getRemoteUser(); if (! empty($username)) { - $this->userInfo = new ReverseProxyUserProvider($username); + $userProfile = $this->userModel->getByUsername($username); + $this->userInfo = new ReverseProxyUserProvider($username, $userProfile ?: array()); return true; } diff --git a/app/Console/TaskOverdueNotificationCommand.php b/app/Console/TaskOverdueNotificationCommand.php index 225a6a1a..36276615 100644 --- a/app/Console/TaskOverdueNotificationCommand.php +++ b/app/Console/TaskOverdueNotificationCommand.php @@ -149,7 +149,7 @@ class TaskOverdueNotificationCommand extends BaseCommand $this->userNotificationModel->sendUserNotification( $user, TaskModel::EVENT_OVERDUE, - array('tasks' => $user_tasks, 'project_name' => implode(", ", $project_names)) + array('tasks' => $user_tasks, 'project_name' => implode(', ', $project_names)) ); } } diff --git a/app/Controller/ActionCreationController.php b/app/Controller/ActionCreationController.php index abd8abd3..9b228f28 100644 --- a/app/Controller/ActionCreationController.php +++ b/app/Controller/ActionCreationController.php @@ -81,7 +81,7 @@ class ActionCreationController extends BaseController 'colors_list' => $this->colorModel->getList(), 'categories_list' => $this->categoryModel->getList($project['id']), 'links_list' => $this->linkModel->getList(0, false), - 'priorities_list' => $this->projectModel->getPriorities($project), + 'priorities_list' => $this->projectTaskPriorityModel->getPriorities($project), 'project' => $project, 'available_actions' => $this->actionManager->getAvailableActions(), 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), diff --git a/app/Controller/BoardTooltipController.php b/app/Controller/BoardTooltipController.php index 2a947027..134d728e 100644 --- a/app/Controller/BoardTooltipController.php +++ b/app/Controller/BoardTooltipController.php @@ -107,9 +107,9 @@ class BoardTooltipController extends BaseController $this->response->html($this->template->render('task_recurrence/info', array( 'task' => $task, - 'recurrence_trigger_list' => $this->taskModel->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->taskModel->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->taskModel->getRecurrenceBasedateList(), + 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(), ))); } diff --git a/app/Controller/ColumnController.php b/app/Controller/ColumnController.php index 95fbcaaa..d3f0e36e 100644 --- a/app/Controller/ColumnController.php +++ b/app/Controller/ColumnController.php @@ -66,7 +66,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateCreation($values); if ($valid) { - if ($this->columnModel->create($project['id'], $values['title'], $values['task_limit'], $values['description']) !== false) { + $result = $this->columnModel->create( + $project['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result !== false) { $this->flash->success(t('Column created successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])), true); } else { @@ -111,7 +119,15 @@ class ColumnController extends BaseController list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { - if ($this->columnModel->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) { + $result = $this->columnModel->update( + $values['id'], + $values['title'], + $values['task_limit'], + $values['description'], + isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0 + ); + + if ($result) { $this->flash->success(t('Board updated successfully.')); return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id']))); } else { diff --git a/app/Controller/TaskDuplicationController.php b/app/Controller/TaskDuplicationController.php index 6a475374..915bf8f8 100644 --- a/app/Controller/TaskDuplicationController.php +++ b/app/Controller/TaskDuplicationController.php @@ -50,7 +50,7 @@ class TaskDuplicationController extends BaseController $values = $this->request->getValues(); list($valid, ) = $this->taskValidator->validateProjectModification($values); - if ($valid && $this->taskDuplicationModel->moveToProject($task['id'], + if ($valid && $this->taskProjectMoveModel->moveToProject($task['id'], $values['project_id'], $values['swimlane_id'], $values['column_id'], @@ -80,7 +80,7 @@ class TaskDuplicationController extends BaseController list($valid, ) = $this->taskValidator->validateProjectModification($values); if ($valid) { - $task_id = $this->taskDuplicationModel->duplicateToProject( + $task_id = $this->taskProjectDuplicationModel->duplicateToProject( $task['id'], $values['project_id'], $values['swimlane_id'], $values['column_id'], $values['category_id'], $values['owner_id'] ); diff --git a/app/Controller/TaskRecurrenceController.php b/app/Controller/TaskRecurrenceController.php index dc7a0e1b..c6fdfa37 100644 --- a/app/Controller/TaskRecurrenceController.php +++ b/app/Controller/TaskRecurrenceController.php @@ -31,10 +31,10 @@ class TaskRecurrenceController extends BaseController 'values' => $values, 'errors' => $errors, 'task' => $task, - 'recurrence_status_list' => $this->taskModel->getRecurrenceStatusList(), - 'recurrence_trigger_list' => $this->taskModel->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->taskModel->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->taskModel->getRecurrenceBasedateList(), + 'recurrence_status_list' => $this->taskRecurrenceModel->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(), ))); } diff --git a/app/Controller/WebNotificationController.php b/app/Controller/WebNotificationController.php index 46a42063..30e317f8 100644 --- a/app/Controller/WebNotificationController.php +++ b/app/Controller/WebNotificationController.php @@ -54,14 +54,14 @@ class WebNotificationController extends BaseController $this->response->redirect($this->helper->url->to( 'TaskViewController', 'show', - array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id']), + array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data'])), 'comment-'.$notification['event_data']['comment']['id'] )); } else { $this->response->redirect($this->helper->url->to( 'TaskViewController', 'show', - array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id']) + array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data'])) )); } } diff --git a/app/Core/Base.php b/app/Core/Base.php index eacca65d..8103ec14 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -86,15 +86,21 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectGroupRoleModel $projectGroupRoleModel * @property \Kanboard\Model\ProjectNotificationModel $projectNotificationModel * @property \Kanboard\Model\ProjectNotificationTypeModel $projectNotificationTypeModel + * @property \Kanboard\Model\ProjectTaskDuplicationModel $projectTaskDuplicationModel + * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel * @property \Kanboard\Model\SubtaskModel $subtaskModel * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel * @property \Kanboard\Model\SwimlaneModel $swimlaneModel + * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel * @property \Kanboard\Model\TagModel $tagModel * @property \Kanboard\Model\TaskModel $taskModel * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel * @property \Kanboard\Model\TaskCreationModel $taskCreationModel * @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel + * @property \Kanboard\Model\TaskProjectDuplicationModel $taskProjectDuplicationModel + * @property \Kanboard\Model\TaskProjectMoveModel $taskProjectMoveModel + * @property \Kanboard\Model\TaskRecurrenceModel $taskRecurrenceModel * @property \Kanboard\Model\TaskExternalLinkModel $taskExternalLinkModel * @property \Kanboard\Model\TaskFinderModel $taskFinderModel * @property \Kanboard\Model\TaskLinkModel $taskLinkModel diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php index 804e6b34..5a037999 100644 --- a/app/Core/ExternalLink/ExternalLinkManager.php +++ b/app/Core/ExternalLink/ExternalLinkManager.php @@ -153,6 +153,30 @@ class ExternalLinkManager extends Base } /** + * Set provider type + * + * @access public + * @param string $userInputType + * @return ExternalLinkManager + */ + public function setUserInputType($userInputType) + { + $this->userInputType = $userInputType; + return $this; + } + + /** + * Set external link + * @param string $userInputText + * @return ExternalLinkManager + */ + public function setUserInputText($userInputText) + { + $this->userInputText = $userInputText; + return $this; + } + + /** * Find a provider that user input * * @access private diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php index fa5b8d2d..3ff57641 100644 --- a/app/Core/Filter/Lexer.php +++ b/app/Core/Filter/Lexer.php @@ -30,7 +30,7 @@ class Lexer '/^([<=>]{1,2}\w+)/u' => 'T_STRING', '/^([<=>]{1,2}".+")/' => 'T_STRING', '/^("(.+)")/' => 'T_STRING', - '/^(\w+)/u' => 'T_STRING', + '/^(\S+)/u' => 'T_STRING', '/^(#\d+)/' => 'T_STRING', ); diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php index 91b48530..4bc1f5f9 100644 --- a/app/Core/Ldap/User.php +++ b/app/Core/Ldap/User.php @@ -116,7 +116,7 @@ class User */ protected function getRole(array $groupIds) { - if ($this->hasGroupsNotConfigured()) { + if (! $this->hasGroupsConfigured()) { return null; } @@ -278,14 +278,14 @@ class User } /** - * Return true if LDAP Group mapping is not configured + * Return true if LDAP Group mapping are configured * * @access public * @return boolean */ - public function hasGroupsNotConfigured() + public function hasGroupsConfigured() { - return !$this->getGroupAdminDn() && !$this->getGroupManagerDn(); + return $this->getGroupAdminDn() || $this->getGroupManagerDn(); } /** diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php index f8736cce..7ca36328 100644 --- a/app/Core/Queue/JobHandler.php +++ b/app/Core/Queue/JobHandler.php @@ -40,6 +40,7 @@ class JobHandler extends Base { $payload = $job->getBody(); $className = $payload['class']; + $this->memoryCache->flush(); $this->prepareJobSession($payload['user_id']); if (DEBUG) { diff --git a/app/Formatter/TaskICalFormatter.php b/app/Formatter/TaskICalFormatter.php index 890674c7..ad2a4449 100644 --- a/app/Formatter/TaskICalFormatter.php +++ b/app/Formatter/TaskICalFormatter.php @@ -6,6 +6,7 @@ use DateTime; use Eluceo\iCal\Component\Calendar; use Eluceo\iCal\Component\Event; use Eluceo\iCal\Property\Event\Attendees; +use Eluceo\iCal\Property\Event\Organizer; use Kanboard\Core\Filter\FormatterInterface; /** @@ -117,16 +118,24 @@ class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterIn $vEvent->setModified($dateModif); $vEvent->setUseTimezone(true); $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); + $vEvent->setDescription($task['description']); + $vEvent->setDescriptionHTML($this->helper->text->markdown($task['description'])); $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); if (! empty($task['owner_id'])) { - $vEvent->setOrganizer($task['assignee_name'] ?: $task['assignee_username'], $task['assignee_email']); + $attendees = new Attendees; + $attendees->add( + 'MAILTO:'.($task['assignee_email'] ?: $task['assignee_username'].'@kanboard.local'), + array('CN' => $task['assignee_name'] ?: $task['assignee_username']) + ); + $vEvent->setAttendees($attendees); } if (! empty($task['creator_id'])) { - $attendees = new Attendees; - $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); - $vEvent->setAttendees($attendees); + $vEvent->setOrganizer(new Organizer( + 'MAILTO:' . $task['creator_email'] ?: $task['creator_username'].'@kanboard.local', + array('CN' => $task['creator_name'] ?: $task['creator_username']) + )); } return $vEvent; diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php index 58e9ce4f..e1d65cca 100644 --- a/app/Helper/TaskHelper.php +++ b/app/Helper/TaskHelper.php @@ -27,17 +27,17 @@ class TaskHelper extends Base public function recurrenceTriggers() { - return $this->taskModel->getRecurrenceTriggerList(); + return $this->taskRecurrenceModel->getRecurrenceTriggerList(); } public function recurrenceTimeframes() { - return $this->taskModel->getRecurrenceTimeframeList(); + return $this->taskRecurrenceModel->getRecurrenceTimeframeList(); } public function recurrenceBasedates() { - return $this->taskModel->getRecurrenceBasedateList(); + return $this->taskRecurrenceModel->getRecurrenceBasedateList(); } public function selectTitle(array $values, array $errors) @@ -70,6 +70,7 @@ class TaskHelper extends Base $options = $this->tagModel->getAssignableList($project['id']); $html = $this->helper->form->label(t('Tags'), 'tags[]'); + $html .= '<input type="hidden" name="tags[]" value="">'; $html .= '<select name="tags[]" id="form-tags" class="tag-autocomplete" multiple>'; foreach ($options as $tag) { @@ -167,10 +168,20 @@ class TaskHelper extends Base return $html; } - public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + public function selectReference(array $values, array $errors = array(), array $attributes = array()) { $attributes = array_merge(array('tabindex="9"'), $attributes); + $html = $this->helper->form->label(t('Reference'), 'reference'); + $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small'); + + return $html; + } + + public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="10"'), $attributes); + $html = $this->helper->form->label(t('Original estimate'), 'time_estimated'); $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes); $html .= ' '.t('hours'); @@ -180,7 +191,7 @@ class TaskHelper extends Base public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array()) { - $attributes = array_merge(array('tabindex="10"'), $attributes); + $attributes = array_merge(array('tabindex="11"'), $attributes); $html = $this->helper->form->label(t('Time spent'), 'time_spent'); $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes); @@ -192,7 +203,7 @@ class TaskHelper extends Base public function selectStartDate(array $values, array $errors = array(), array $attributes = array()) { $placeholder = date($this->configModel->get('application_date_format', 'm/d/Y H:i')); - $attributes = array_merge(array('tabindex="11"', 'placeholder="'.$placeholder.'"'), $attributes); + $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes); $html = $this->helper->form->label(t('Start Date'), 'date_started'); $html .= $this->helper->form->text('date_started', $values, $errors, $attributes, 'form-datetime'); @@ -203,7 +214,7 @@ class TaskHelper extends Base public function selectDueDate(array $values, array $errors = array(), array $attributes = array()) { $placeholder = date($this->configModel->get('application_date_format', 'm/d/Y')); - $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes); + $attributes = array_merge(array('tabindex="13"', 'placeholder="'.$placeholder.'"'), $attributes); $html = $this->helper->form->label(t('Due Date'), 'date_due'); $html .= $this->helper->form->text('date_due', $values, $errors, $attributes, 'form-date'); diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php index ae3efe1d..ab259a62 100644 --- a/app/Helper/UserHelper.php +++ b/app/Helper/UserHelper.php @@ -107,6 +107,10 @@ class UserHelper extends Base */ public function hasAccess($controller, $action) { + if (! $this->userSession->isLogged()) { + return false; + } + $key = 'app_access:'.$controller.$action; $result = $this->memoryCache->get($key); @@ -128,6 +132,10 @@ class UserHelper extends Base */ public function hasProjectAccess($controller, $action, $project_id) { + if (! $this->userSession->isLogged()) { + return false; + } + if ($this->userSession->isAdmin()) { return true; } diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 5a60829f..5f513347 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index f0fce806..1c28f4f9 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -100,7 +100,7 @@ return array( 'There is nobody assigned' => 'Není přiřazeno žádnému uživateli', 'Column on the board:' => 'Sloupec:', 'Close this task' => 'Uzavřít úkol', - 'Open this task' => 'Aufgabe wieder öffnen', + 'Open this task' => 'Otevřít tento úkol', 'There is no description.' => 'Bez popisu', 'Add a new task' => 'Přidat nový úkol', 'The username is required' => 'Uživatelské jméno je vyžadováno', @@ -393,8 +393,8 @@ return array( 'Default values are "%s"' => 'Standardní hodnoty jsou: "%s"', 'Default columns for new projects (Comma-separated)' => 'Výchozí sloupce pro nové projekty (odděleny čárkou)', 'Task assignee change' => 'Změna přiřazení uživatelů', - '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', - '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s', + '%s change the assignee of the task #%d to %s' => '%s změnil přidělení úkolu #%d na uživatele %s', + '%s changed the assignee of the task %s to %s' => '%s změnil přidělení úkolu %s na uživatele %s', 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', 'Choose an event' => 'Vybrat událost', 'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem', @@ -457,13 +457,13 @@ return array( 'The project id must be an integer' => 'ID projektu musí být celé číslo', 'The status must be an integer' => 'Status musí být celé číslo', 'The subtask id is required' => 'Je požadováno id dílčího úkolu', - 'The subtask id must be an integer' => 'Die Teilaufgabenid muss eine ganze Zahl sein', - 'The task id is required' => 'Die Aufgabenid ist benötigt', - 'The task id must be an integer' => 'Die Aufgabenid muss eine ganze Zahl sein', - 'The user id must be an integer' => 'Die Userid muss eine ganze Zahl sein', - 'This value is required' => 'Dieser Wert ist erforderlich', - 'This value must be numeric' => 'Dieser Wert muss numerisch sein', - 'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden', + 'The subtask id must be an integer' => 'ID dílčího úkolu musí být číslo', + 'The task id is required' => 'ID úkolu je povinné', + 'The task id must be an integer' => 'ID úkolu musí být číslo', + 'The user id must be an integer' => 'ID uživatele musí být číslo', + 'This value is required' => 'Hodnota je povinná', + 'This value must be numeric' => 'Hodnota musí být číselná', + 'Unable to create this task.' => 'Nelze vytvořit tento úkol', 'Cumulative flow diagram' => 'Kumulativní diagram', 'Cumulative flow diagram for "%s"' => 'Kumulativní diagram pro "%s"', 'Daily project summary' => 'Denní přehledy', @@ -471,26 +471,26 @@ return array( 'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"', 'Exports' => 'Exporty', 'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.', - 'Active swimlanes' => 'Aktive Swimlane', - 'Add a new swimlane' => 'Přidat nový řádek', - 'Change default swimlane' => 'Standard Swimlane ändern', - 'Default swimlane' => 'Výchozí Swimlane', - 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', - 'Inactive swimlanes' => 'Inaktive Swimlane', - 'Remove a swimlane' => 'Odstranit swimlane', - 'Show default swimlane' => 'Standard Swimlane anzeigen', - 'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "%s"', - 'Swimlane removed successfully.' => 'Swimlane erfolgreich entfernt.', - 'Swimlanes' => 'Swimlanes', - 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.', - 'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.', - 'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.', - 'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.', - 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', - 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', + 'Active swimlanes' => 'Aktivní dráhy', + 'Add a new swimlane' => 'Přidat novou dráhu', + 'Change default swimlane' => 'Změnit výchozí dráhu', + 'Default swimlane' => 'Výchozí dráha', + 'Do you really want to remove this swimlane: "%s"?' => 'Opravdu si přejete odstranit tuto dráhu: "%s"?', + 'Inactive swimlanes' => 'Neaktivní dráha', + 'Remove a swimlane' => 'Odstranit dráhu', + 'Show default swimlane' => 'Zobrazit výchozí dráhu', + 'Swimlane modification for the project "%s"' => 'Změny dráhy pro projekt "%s"', + 'Swimlane removed successfully.' => 'Dráha byla odstraněna.', + 'Swimlanes' => 'Dráhy', + 'Swimlane updated successfully.' => 'Dráha byla upravena.', + 'The default swimlane have been updated successfully.' => 'Výchozí dráha byla upravena', + 'Unable to remove this swimlane.' => 'Tuto dráhu nelze odstranit.', + 'Unable to update this swimlane.' => 'Tuto dráhu nelze upravit.', + 'Your swimlane have been created successfully.' => 'Dráha byla vytvořena.', + 'Example: "Bug, Feature Request, Improvement"' => 'Například: "Chyba", "Nápad", "Požadavek"...', 'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)', 'Integrations' => 'Integrace', - 'Integration with third-party services' => 'Integration von Fremdleistungen', + 'Integration with third-party services' => 'Integrace se službami třetích stran', 'Subtask Id' => 'Dílčí úkol Id', 'Subtasks' => 'Dílčí úkoly', 'Subtasks Export' => 'Export dílčích úkolů', @@ -510,7 +510,7 @@ return array( 'User dashboard' => 'Nástěnka uživatele', 'Allow only one subtask in progress at the same time for a user' => 'Umožnit uživateli práci pouze na jednom dílčím úkolu ve stejném čase', 'Edit column "%s"' => 'Upravit sloupec "%s" ', - 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"', + 'Select the new status of the subtask: "%s"' => 'Vyberte nový stav pro podúkol: "%s"', 'Subtask timesheet' => 'Časový rozvrh dílčích úkolů', 'There is nothing to show.' => 'Žádná položka k zobrazení', 'Time Tracking' => 'Sledování času', @@ -523,9 +523,9 @@ return array( 'Days in this column' => 'Dní v tomto sloupci', '%dd' => '%d d', 'Add a new link' => 'Přidat nový odkaz', - 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', - 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', - 'Field required' => 'Feld erforderlich', + 'Do you really want to remove this link: "%s"?' => 'Opravdu chcete odstranit odkaz "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Opravdu chcete odstranit odkaz na úkol #%d ?', + 'Field required' => 'Povinné pole', 'Link added successfully.' => 'Propojení bylo úspěšně přidáno.', 'Link updated successfully.' => 'Propojení bylo úspěšně aktualizováno.', 'Link removed successfully.' => 'Propojení bylo úspěšně odebráno.', @@ -549,8 +549,8 @@ return array( 'is duplicated by' => 'je duplikován', 'is a child of' => 'je podřízený', 'is a parent of' => 'je nadřízený', - 'targets milestone' => 'targets milestone', - 'is a milestone of' => 'is a milestone of', + 'targets milestone' => 'patří k milníku', + 'is a milestone of' => 'je milníkem', 'fixes' => 'nahrazuje', 'is fixed by' => 'je nahrazen', 'This task' => 'Tento úkol', @@ -592,7 +592,7 @@ return array( 'Time spent in the column' => 'Trvání jednotlivých etap', 'Task transitions' => 'Přesuny úkolů', 'Task transitions export' => 'Export přesunů mezi sloupci', - 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Tento seznam obsahuje všechny pohyby úkolů s daty, uživateli a časy strávenými na úkolu.', 'Currency rates' => 'Aktuální kurzy', 'Rate' => 'Kurz', 'Change reference currency' => 'Změnit referenční měnu', @@ -604,28 +604,28 @@ return array( '%s remove the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ', 'Enable Gravatar images' => 'Aktiviere Gravatar Bilder', 'Information' => 'Informace', - 'Check two factor authentication code' => 'Prüfe Zwei-Faktor-Authentifizierungscode', - 'The two factor authentication code is not valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist ungültig.', - 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', - 'Code' => 'Code', + 'Check two factor authentication code' => 'Zkontrolujte dvouúrovňový autentifikační klíč', + 'The two factor authentication code is not valid.' => 'Dvouúrovňový autentifikační klíč není platný.', + 'The two factor authentication code is valid.' => 'Dvouúrovňový autentifikační klíč je platný.', + 'Code' => 'Klíč', 'Two factor authentication' => 'Dvouúrovňová autorizace', - 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', + 'This QR code contains the key URI: ' => 'Tento QR kód obsahuje adresu s klíčem', 'Check my code' => 'Kontrola mého kódu', 'Secret key: ' => 'Tajný klíč', 'Test your device' => 'Test Vašeho zařízení', 'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce', '%s via Kanboard' => '%s via Kanboard', - 'Burndown chart for "%s"' => 'Burndown-Chart für "%s"', + 'Burndown chart for "%s"' => 'Burndown-Chart pro "%s"', 'Burndown chart' => 'Burndown-Chart', 'This chart show the task complexity over the time (Work Remaining).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).', 'Screenshot taken %s' => 'Screenshot aufgenommen %s ', 'Add a screenshot' => 'Přidat snímek obrazovky', - 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Nimm einen Screenshot auf und drücke STRG+V oder ⌘+V um ihn hier einzufügen.', - 'Screenshot uploaded successfully.' => 'Screenshot erfolgreich hochgeladen.', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Pořiďte snímek obrazovky a v tomto poli stiskněte Ctrl+V nebo ⌘+V ', + 'Screenshot uploaded successfully.' => 'Snímek obrazovky byl úspěšně nahrán.', 'SEK - Swedish Krona' => 'SEK - Schwedische Kronen', - 'Identifier' => 'Identifikator', - 'Disable two factor authentication' => 'Zrušit dvou stupňovou autorizaci', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?', + 'Identifier' => 'Identifikátor', + 'Disable two factor authentication' => 'Zrušit dvouúrovňovou autorizaci', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Opravdu chcete vypnout dvouúrovňovou autentifikaci pro uživatele: "%s"?', // 'Edit link' => '', // 'Start to type task title...' => '', // 'A task cannot be linked to itself' => '', @@ -680,27 +680,27 @@ return array( 'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie', 'Send a task by email to someone' => 'Poslat někomu úkol poštou', 'Reopen a task' => 'Znovu otevřít úkol', - 'Column change' => 'Spalte geändert', - 'Position change' => 'Position geändert', - 'Swimlane change' => 'Swimlane geändert', - 'Assignee change' => 'Zuordnung geändert', - '[%s] Overdue tasks' => '[%s] überfallige Aufgaben', - 'Notification' => 'Benachrichtigungen', - '%s moved the task #%d to the first swimlane' => '%s hat die Aufgabe #%d in die erste Swimlane verschoben', - '%s moved the task #%d to the swimlane "%s"' => '%s hat die Aufgabe #%d in die Swimlane "%s" verschoben', + 'Column change' => 'Změna sloupce', + 'Position change' => 'Změna pozice', + 'Swimlane change' => 'Změna dráhy', + 'Assignee change' => 'Změna přidělení', + '[%s] Overdue tasks' => '[%s] přetažených úkolů', + 'Notification' => 'Upozornění', + '%s moved the task #%d to the first swimlane' => '%s přesunul úkol #%d do první dráhy', + '%s moved the task #%d to the swimlane "%s"' => '%s přesunul úkol #%d do dráhy "%s"', // 'Swimlane' => '', // 'Gravatar' => '', - '%s moved the task %s to the first swimlane' => '%s hat die Aufgabe %s in die erste Swimlane verschoben', - '%s moved the task %s to the swimlane "%s"' => '%s hat die Aufgaben %s in die Swimlane "%s" verschoben', + '%s moved the task %s to the first swimlane' => '%s přesunul úkol %s do první dráhy', + '%s moved the task %s to the swimlane "%s"' => '%s přesunul úkol %s do dráhy "%s"', 'This report contains all subtasks information for the given date range.' => 'Report obsahuje všechny informace o dílčích úkolech pro daný časový úsek', 'This report contains all tasks information for the given date range.' => 'Report obsahuje informace o všech úkolech pro daný časový úsek.', 'Project activities for %s' => 'Aktivity projektu %s', - 'view the board on Kanboard' => 'Pinnwand in Kanboard anzeigen', - 'The task have been moved to the first swimlane' => 'Die Aufgabe wurde in die erste Swimlane verschoben', - 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in ene andere Swimlane verschoben', - 'New title: %s' => 'Neuer Titel: %s', - 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen', - 'New assignee: %s' => 'Neue Zuordnung: %s', + 'view the board on Kanboard' => 'Zobrazit nástěnku', + 'The task have been moved to the first swimlane' => 'Úkol byl přesunut do první dráhy', + 'The task have been moved to another swimlane:' => 'Úkol byl přesunut do další dráhy', + 'New title: %s' => 'Nový název: %s', + 'The task is not assigned anymore' => 'Úkol již není přidělen', + 'New assignee: %s' => 'přidělení: %s', 'There is no category now' => 'Nyní neexistuje žádná kategorie', 'New category: %s' => 'Nová kategorie: %s', 'New color: %s' => 'Nová barva: %s', @@ -708,11 +708,11 @@ return array( 'The due date have been removed' => 'Datum dokončení byl odstraněn', 'There is no description anymore' => 'Ještě neexistuje žádný popis', 'Recurrence settings have been modified' => 'Nastavení opakování bylo změněno', - 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh', - 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh', - 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert', - 'The description has been modified:' => 'Die Beschreibung wurde geändert:', - 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)', + 'Time spent changed: %sh' => 'Strávený čas se změnil: %sh', + 'Time estimated changed: %sh' => 'Odhadovaný čas se změnil: %sh', + 'The field "%s" have been updated' => 'Sloupec "%s" byl upraven', + 'The description has been modified:' => 'Popis byl upraven:', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Opravdu si přejete úkol "%s" uzavřít? (včetně podúkolů)', 'I want to receive notifications for:' => 'Chci dostávat upozornění na:', 'All tasks' => 'Všechny úkoly', 'Only for tasks assigned to me' => 'pouze pro moje úkoly', @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index afe14d7f..abebd394 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 232b9f56..f569206b 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1196,23 +1196,25 @@ return array( 'Email transport' => 'E-Mail Verkehr', 'Webhook token' => 'Webhook Token', 'Imports' => 'Importe', - // 'Project tags management' => '', - // 'Tag created successfully.' => '', - // 'Unable to create this tag.' => '', - // 'Tag updated successfully.' => '', - // 'Unable to update this tag.' => '', - // 'Tag removed successfully.' => '', - // 'Unable to remove this tag.' => '', - // 'Global tags management' => '', - // 'Tags' => '', - // 'Tags management' => '', - // 'Add new tag' => '', - // 'Edit a tag' => '', - // 'Project tags' => '', - // 'There is no specific tag for this project at the moment.' => '', - // 'Tag' => '', - // 'Remove a tag' => '', - // 'Do you really want to remove this tag: "%s"?' => '', - // 'Global tags' => '', - // 'There is no global tag at the moment.' => '', + 'Project tags management' => 'Projektbezogenes Schlagwort-Management', + 'Tag created successfully.' => 'Schlagwort erfolgreich erstellt.', + 'Unable to create this tag.' => 'Das Schlagwort kann nicht erstellt werden.', + 'Tag updated successfully.' => 'Schlagwort erfolgreich aktualisiert.', + 'Unable to update this tag.' => 'Das Schlagwort kann nicht aktualisiert werden.', + 'Tag removed successfully.' => 'Schlagwort erfolgreich entfernt.', + 'Unable to remove this tag.' => 'Das Schlagwort kann nicht entfernt werden.', + 'Global tags management' => 'Globales Schlagwort-Management', + 'Tags' => 'Schlagworte', + 'Tags management' => 'Schlagwort-Management', + 'Add new tag' => 'Neues Schlagwort hinzufügen', + 'Edit a tag' => 'Schlagwort bearbeiten', + 'Project tags' => 'Projektbezogene Schlagwörter', + 'There is no specific tag for this project at the moment.' => 'Es gibt zur Zeit kein spezifisches Schlagwort.', + 'Tag' => 'Schlagwort', + 'Remove a tag' => 'Schlagwort entfernen', + 'Do you really want to remove this tag: "%s"?' => 'Soll dieses Schlagwort wirklich entfernt werden: "%s"?', + 'Global tags' => 'Globale Schlagwörter', + 'There is no global tag at the moment.' => 'Es gibt zur Zeit kein globales Schlagwort', + 'This field cannot be empty' => 'Dieses Feld kann nicht leer sein', + 'Hide tasks in this column in the dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 6454af12..c1d7c579 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 6ba86f8f..5699ce6f 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index cb2a8d9a..6fe4852c 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index cac1c73a..7663da0f 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -944,7 +944,7 @@ return array( 'Project Manager' => 'Chef de projet', 'Project Member' => 'Membre du projet', 'Project Viewer' => 'Visualiseur de projet', - 'Your account is locked for %d minutes' => 'Votre compte est vérouillé pour %d minutes', + 'Your account is locked for %d minutes' => 'Votre compte est verrouillé pour %d minutes', 'Invalid captcha' => 'Captcha invalid', 'The name must be unique' => 'Le nom doit être unique', 'View all groups' => 'Voir tous les groupes', @@ -1216,4 +1216,6 @@ return array( 'Do you really want to remove this tag: "%s"?' => 'Voulez-vous vraiment supprimer ce libellé : « %s » ?', 'Global tags' => 'Libellés globaux', 'There is no global tag at the moment.' => 'Il n\'y a aucun libellé global pour le moment.', + 'This field cannot be empty' => 'Ce champ ne peut être vide', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index e032be66..96db72ef 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index 29d11b8d..2d6e5aa3 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index c5710a4e..e10b61da 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1196,23 +1196,25 @@ return array( 'Email transport' => 'Trasporto Email', // 'Webhook token' => '', 'Imports' => 'Importa', - // 'Project tags management' => '', - // 'Tag created successfully.' => '', - // 'Unable to create this tag.' => '', - // 'Tag updated successfully.' => '', - // 'Unable to update this tag.' => '', - // 'Tag removed successfully.' => '', - // 'Unable to remove this tag.' => '', - // 'Global tags management' => '', - // 'Tags' => '', - // 'Tags management' => '', - // 'Add new tag' => '', - // 'Edit a tag' => '', - // 'Project tags' => '', - // 'There is no specific tag for this project at the moment.' => '', + 'Project tags management' => 'Gestione tag di progetto', + 'Tag created successfully.' => 'Tag creato con successo.', + 'Unable to create this tag.' => 'Impossibile creare questo tag.', + 'Tag updated successfully.' => 'Tag aggiornato con successo.', + 'Unable to update this tag.' => 'Impossibile aggiornare questo tag.', + 'Tag removed successfully.' => 'Tag rimosso con successo.', + 'Unable to remove this tag.' => 'Impossibile rimuovere questo tag.', + 'Global tags management' => 'Gestione dei tag globali', + 'Tags' => 'Tag', + 'Tags management' => 'Gestione dei tag', + 'Add new tag' => 'Aggiungi un nuovo tag', + 'Edit a tag' => 'Modifica un tag', + 'Project tags' => 'Tag di progetto', + 'There is no specific tag for this project at the moment.' => 'Non è definito nessun tag specifico per questo progetto al momento.', // 'Tag' => '', - // 'Remove a tag' => '', - // 'Do you really want to remove this tag: "%s"?' => '', - // 'Global tags' => '', - // 'There is no global tag at the moment.' => '', + 'Remove a tag' => 'Rimuovi un tag', + 'Do you really want to remove this tag: "%s"?' => 'Vuoi davvero rimuovere questo tag: "%s"?', + 'Global tags' => 'Tag globali', + 'There is no global tag at the moment.' => 'Non sono definiti tag globali al momento.', + 'This field cannot be empty' => 'Questo campo non può essere vuoto', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 589917f0..2fe13ac9 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index 2f142b48..bd25d11f 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index 02fa16f8..ff8960aa 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 6bb30642..8752a159 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 8dddbe7d..e07ea32c 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + //' Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 46c3b034..896d2ed4 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 6a0ebd65..40f3bb4d 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 42826240..08375ad0 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1196,23 +1196,25 @@ return array( 'Email transport' => 'Transportador de Email', 'Webhook token' => 'Token do Webhook', 'Imports' => 'Importados', - // 'Project tags management' => '', - // 'Tag created successfully.' => '', - // 'Unable to create this tag.' => '', - // 'Tag updated successfully.' => '', - // 'Unable to update this tag.' => '', - // 'Tag removed successfully.' => '', - // 'Unable to remove this tag.' => '', - // 'Global tags management' => '', - // 'Tags' => '', - // 'Tags management' => '', - // 'Add new tag' => '', - // 'Edit a tag' => '', - // 'Project tags' => '', - // 'There is no specific tag for this project at the moment.' => '', - // 'Tag' => '', - // 'Remove a tag' => '', - // 'Do you really want to remove this tag: "%s"?' => '', - // 'Global tags' => '', - // 'There is no global tag at the moment.' => '', + 'Project tags management' => 'Gestão de etiquetas do Projecto', + 'Tag created successfully.' => 'Etiqueta criada com sucesso.', + 'Unable to create this tag.' => 'Não foi possivel criar esta etiqueta.', + 'Tag updated successfully.' => 'Etiqueta actualizada com sucesso.', + 'Unable to update this tag.' => 'Não foi possivel actualizar esta etiqueta.', + 'Tag removed successfully.' => 'Etiqueta removida com sucesso.', + 'Unable to remove this tag.' => 'Não foi possivel remover esta etiqueta.', + 'Global tags management' => 'Gestão de etiquetas globais', + 'Tags' => 'Etiquetas', + 'Tags management' => 'Gestão de Etiquetas', + 'Add new tag' => 'Adicionar etiqueta nova', + 'Edit a tag' => 'Editar a etiqueta', + 'Project tags' => 'Etiquetas do Projecto', + 'There is no specific tag for this project at the moment.' => 'De momento não existe nenhuma etiqueta para este projecto.', + 'Tag' => 'Etiqueta', + 'Remove a tag' => 'Remover etiqueta', + 'Do you really want to remove this tag: "%s"?' => 'Tem a certeza que pretende remover esta etiqueta: "%s"?', + 'Global tags' => 'Etiquetas globais', + 'There is no global tag at the moment.' => 'De momento não existe nenhuma etiqueta global.', + // 'This field cannot be empty' => '', + //'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index e7b73ef0..c6285f6a 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -715,9 +715,9 @@ return array( 'Do you really want to close the task "%s" as well as all subtasks?' => 'Вы действительно хотите закрыть задачу "%s", а также все подзадачи?', 'I want to receive notifications for:' => 'Я хочу получать уведомления для:', 'All tasks' => 'Все задачи', - 'Only for tasks assigned to me' => 'Только для задач, назначенных на меня', + 'Only for tasks assigned to me' => 'Только для задач, назначенных мне', 'Only for tasks created by me' => 'Только для задач, созданных мной', - 'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мной', + 'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мне', '%%Y-%%m-%%d' => '%%Y-%%m-%%d', 'Total for all columns' => 'Суммарно для всех колонок', 'You need at least 2 days of data to show the chart.' => 'Для отображения диаграммы нужно по крайней мере 2 дня.', @@ -1154,65 +1154,67 @@ return array( 'Projects where "%s" is member' => 'Проекты, где членом является "%s"', 'Open tasks assigned to "%s"' => 'Открытые задачи, назначенные на "%s"', 'Closed tasks assigned to "%s"' => 'Закрытые задачи, назначенные на "%s"', - // 'Assign automatically a color based on a priority' => '', - // 'Overdue tasks for the project(s) "%s"' => '', - // 'Upload files' => '', - // 'Installed Plugins' => '', - // 'Plugin Directory' => '', - // 'Plugin installed successfully.' => '', - // 'Plugin updated successfully.' => '', - // 'Plugin removed successfully.' => '', - // 'Subtask converted to task successfully.' => '', - // 'Unable to convert the subtask.' => '', - // 'Unable to extract plugin archive.' => '', - // 'Plugin not found.' => '', - // 'You don\'t have the permission to remove this plugin.' => '', - // 'Unable to download plugin archive.' => '', - // 'Unable to write temporary file for plugin.' => '', - // 'Unable to open plugin archive.' => '', - // 'There is no file in the plugin archive.' => '', - // 'Create tasks in bulk' => '', - // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', - // 'There is no plugin available.' => '', - // 'Install' => '', - // 'Update' => '', - // 'Up to date' => '', - // 'Not available' => '', - // 'Remove plugin' => '', - // 'Do you really want to remove this plugin: "%s"?' => '', - // 'Uninstall' => '', - // 'Listing' => '', - // 'Metadata' => '', - // 'Manage projects' => '', - // 'Convert to task' => '', - // 'Convert sub-task to task' => '', - // 'Do you really want to convert this sub-task to a task?' => '', - // 'My task title' => '', - // 'Enter one task by line.' => '', - // 'Number of failed login:' => '', - // 'Account locked until:' => '', - // 'Email settings' => '', - // 'Email sender address' => '', - // 'Email transport' => '', + 'Assign automatically a color based on a priority' => 'Автоматически назначить цвет в зависимости от категории', + 'Overdue tasks for the project(s) "%s"' => 'Просроченные задачи для проекта(ов) "%s"', + 'Upload files' => 'Загрузить файлы', + 'Installed Plugins' => 'Установленные плагины', + 'Plugin Directory' => 'Доступные плагины', + 'Plugin installed successfully.' => 'Плагин успешно установлен.', + 'Plugin updated successfully.' => 'Плагин успешно обновлен.', + 'Plugin removed successfully.' => 'Плагин успешно удален.', + 'Subtask converted to task successfully.' => 'Подзадача успешно преобразована в задачу.', + 'Unable to convert the subtask.' => 'Невозможно преобразовать подзадачу.', + 'Unable to extract plugin archive.' => 'Невозможно распаковать архив с плагином.', + 'Plugin not found.' => 'Плагин не найден.', + 'You don\'t have the permission to remove this plugin.' => 'У Вас нет прав на удаление этого плагина.', + 'Unable to download plugin archive.' => 'Невозможно загрузить архив с плагином.', + 'Unable to write temporary file for plugin.' => 'Невозможно записать временный файл для плагина.', + 'Unable to open plugin archive.' => 'Невозможно открыть архив плагина.', + 'There is no file in the plugin archive.' => 'В арзиве плагина нет файлов.', + 'Create tasks in bulk' => 'Массовое создание задач', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Ваш Kanboard не сконфигурирова для установки плагинов через пользовательский интерфейс.', + 'There is no plugin available.' => 'Нет доступных плагинов.', + 'Install' => 'Установить', + 'Update' => 'Обновить', + 'Up to date' => 'Самый новый', + 'Not available' => 'Недоступен', + 'Remove plugin' => 'Удалить плагин', + 'Do you really want to remove this plugin: "%s"?' => 'Вы действительно хотите удалить плагин: "%s"?', + 'Uninstall' => 'Деинсталлировать', + 'Listing' => 'Список', + 'Metadata' => 'Метаданные', + 'Manage projects' => 'Управление проектами', + 'Convert to task' => 'Преобразовать в задачу', + 'Convert sub-task to task' => 'Преобразовать подзадачу в задачу', + 'Do you really want to convert this sub-task to a task?' => 'Вы действительно хотите преобразовать эту подзадачу в задачу?', + 'My task title' => 'Заголовок задачи', + 'Enter one task by line.' => 'Указывайте одну задачу на строке', + 'Number of failed login:' => 'Число неудачных попыток входа:', + 'Account locked until:' => 'Аккаунт заблокирован до:', + 'Email settings' => 'Настройки почты', + 'Email sender address' => 'Адрес отправителя', + 'Email transport' => 'Почтовый транспорт', // 'Webhook token' => '', // 'Imports' => '', - // 'Project tags management' => '', - // 'Tag created successfully.' => '', - // 'Unable to create this tag.' => '', - // 'Tag updated successfully.' => '', - // 'Unable to update this tag.' => '', - // 'Tag removed successfully.' => '', - // 'Unable to remove this tag.' => '', - // 'Global tags management' => '', - // 'Tags' => '', - // 'Tags management' => '', - // 'Add new tag' => '', - // 'Edit a tag' => '', - // 'Project tags' => '', - // 'There is no specific tag for this project at the moment.' => '', - // 'Tag' => '', - // 'Remove a tag' => '', - // 'Do you really want to remove this tag: "%s"?' => '', - // 'Global tags' => '', - // 'There is no global tag at the moment.' => '', + 'Project tags management' => 'Управление метками проекта', + 'Tag created successfully.' => 'Метка успешно создана.', + 'Unable to create this tag.' => 'Невозможно создать эту метку.', + 'Tag updated successfully.' => 'Метак успешно обновлена.', + 'Unable to update this tag.' => 'Невозможно обновить эту метку.', + 'Tag removed successfully.' => 'Метка успешно удалена.', + 'Unable to remove this tag.' => 'Невозможно удалить эту метку.', + 'Global tags management' => 'Управление глоабльными метками', + 'Tags' => 'Метки', + 'Tags management' => 'Управление метками', + 'Add new tag' => 'Добавить новую метку', + 'Edit a tag' => 'Редактировать метку', + 'Project tags' => 'Метки проекта', + 'There is no specific tag for this project at the moment.' => 'Нет меток для этого проекта.', + 'Tag' => 'Метка', + 'Remove a tag' => 'Удалить метку', + 'Do you really want to remove this tag: "%s"?' => 'Вы действительно хотите удалить метку: "%s"?', + 'Global tags' => 'Глобальные метка', + 'There is no global tag at the moment.' => 'Нет глобальных меток.', + 'This field cannot be empty' => 'Это поле не может быть пустым', + 'Hide tasks in this column in the dashboard' => 'Не показывать задачи из этой колонки в кабинете', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 96a463d1..92ed3424 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index cdfb36f6..eedcf0fc 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index fd59c003..a6de8bce 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index ab888266..35e29649 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index cafb8c55..0ef01ef7 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1215,4 +1215,6 @@ return array( // 'Do you really want to remove this tag: "%s"?' => '', // 'Global tags' => '', // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', + // 'Hide tasks in this column in the dashboard' => '', ); diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 795fe692..5498ef54 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -138,19 +138,21 @@ class ColumnModel extends Base * Add a new column to the board * * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @param integer $hide_in_dashboard + * @return bool|int */ - public function create($project_id, $title, $task_limit = 0, $description = '') + public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) { $values = array( 'project_id' => $project_id, 'title' => $title, 'task_limit' => intval($task_limit), 'position' => $this->getLastColumnPosition($project_id) + 1, + 'hide_in_dashboard' => $hide_in_dashboard, 'description' => $description, ); @@ -165,13 +167,15 @@ class ColumnModel extends Base * @param string $title Column title * @param integer $task_limit Task limit * @param string $description Optional description + * @param integer $hide_in_dashboard * @return boolean */ - public function update($column_id, $title, $task_limit = 0, $description = '') + public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) { return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( 'title' => $title, 'task_limit' => intval($task_limit), + 'hide_in_dashboard' => $hide_in_dashboard, 'description' => $description, )); } diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 8937b77e..4d697b5e 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -133,4 +133,41 @@ class NotificationModel extends Base return e('Notification'); } } + + /** + * Get task id from event + * + * @access public + * @param string $event_name + * @param array $event_data + * @return integer + */ + public function getTaskIdFromEvent($event_name, array $event_data) + { + switch ($event_name) { + case TaskFileModel::EVENT_CREATE: + return $event_data['file']['task_id']; + case CommentModel::EVENT_CREATE: + case CommentModel::EVENT_UPDATE: + return $event_data['comment']['task_id']; + case SubtaskModel::EVENT_CREATE: + case SubtaskModel::EVENT_UPDATE: + return $event_data['subtask']['task_id']; + case TaskModel::EVENT_CREATE: + case TaskModel::EVENT_UPDATE: + case TaskModel::EVENT_CLOSE: + case TaskModel::EVENT_OPEN: + case TaskModel::EVENT_MOVE_COLUMN: + case TaskModel::EVENT_MOVE_POSITION: + case TaskModel::EVENT_MOVE_SWIMLANE: + case TaskModel::EVENT_ASSIGNEE_CHANGE: + case CommentModel::EVENT_USER_MENTION: + case TaskModel::EVENT_USER_MENTION: + return $event_data['task']['id']; + case TaskModel::EVENT_OVERDUE: + return $event_data['tasks'][0]['id']; + default: + return 0; + } + } } diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php index b67f8302..94b83c80 100644 --- a/app/Model/ProjectDuplicationModel.php +++ b/app/Model/ProjectDuplicationModel.php @@ -22,7 +22,15 @@ class ProjectDuplicationModel extends Base */ public function getOptionalSelection() { - return array('categoryModel', 'projectPermissionModel', 'actionModel', 'swimlaneModel', 'taskModel', 'projectMetadataModel'); + return array( + 'categoryModel', + 'projectPermissionModel', + 'actionModel', + 'swimlaneModel', + 'tagDuplicationModel', + 'projectMetadataModel', + 'projectTaskDuplicationModel', + ); } /** @@ -33,7 +41,16 @@ class ProjectDuplicationModel extends Base */ public function getPossibleSelection() { - return array('boardModel', 'categoryModel', 'projectPermissionModel', 'actionModel', 'swimlaneModel', 'taskModel', 'projectMetadataModel'); + return array( + 'boardModel', + 'categoryModel', + 'projectPermissionModel', + 'actionModel', + 'swimlaneModel', + 'tagDuplicationModel', + 'projectMetadataModel', + 'projectTaskDuplicationModel', + ); } /** @@ -129,6 +146,9 @@ class ProjectDuplicationModel extends Base 'is_public' => 0, 'is_private' => $private ? 1 : $is_private, 'owner_id' => $owner_id, + 'priority_default' => $project['priority_default'], + 'priority_start' => $project['priority_start'], + 'priority_end' => $project['priority_end'], ); if (! $this->db->table(ProjectModel::TABLE)->save($values)) { diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php index 7382537e..850531c9 100644 --- a/app/Model/ProjectModel.php +++ b/app/Model/ProjectModel.php @@ -246,19 +246,6 @@ class ProjectModel extends Base } /** - * Get Priority range from a project - * - * @access public - * @param array $project - * @return array - */ - public function getPriorities(array $project) - { - $range = range($project['priority_start'], $project['priority_end']); - return array_combine($range, $range); - } - - /** * Gather some task metrics for a given project * * @access public diff --git a/app/Model/ProjectTaskDuplicationModel.php b/app/Model/ProjectTaskDuplicationModel.php new file mode 100644 index 00000000..5d2e1322 --- /dev/null +++ b/app/Model/ProjectTaskDuplicationModel.php @@ -0,0 +1,35 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Project Task Duplication Model + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ProjectTaskDuplicationModel extends Base +{ + /** + * Duplicate all tasks to another project + * + * @access public + * @param integer $src_project_id + * @param integer $dst_project_id + * @return boolean + */ + public function duplicate($src_project_id, $dst_project_id) + { + $task_ids = $this->taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)); + + foreach ($task_ids as $task_id) { + if (! $this->taskProjectDuplicationModel->duplicateToProject($task_id, $dst_project_id)) { + return false; + } + } + + return true; + } +} diff --git a/app/Model/ProjectTaskPriorityModel.php b/app/Model/ProjectTaskPriorityModel.php new file mode 100644 index 00000000..c1a0257a --- /dev/null +++ b/app/Model/ProjectTaskPriorityModel.php @@ -0,0 +1,74 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Project Task Priority Model + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ProjectTaskPriorityModel extends Base +{ + /** + * Get Priority range from a project + * + * @access public + * @param array $project + * @return array + */ + public function getPriorities(array $project) + { + $range = range($project['priority_start'], $project['priority_end']); + return array_combine($range, $range); + } + + /** + * Get task priority settings + * + * @access public + * @param int $project_id + * @return array|null + */ + public function getPrioritySettings($project_id) + { + return $this->db + ->table(ProjectModel::TABLE) + ->columns('priority_default', 'priority_start', 'priority_end') + ->eq('id', $project_id) + ->findOne(); + } + + /** + * Get default task priority + * + * @access public + * @param int $project_id + * @return int + */ + public function getDefaultPriority($project_id) + { + return $this->db->table(ProjectModel::TABLE)->eq('id', $project_id)->findOneColumn('priority_default') ?: 0; + } + + /** + * Get priority for a destination project + * + * @access public + * @param integer $dst_project_id + * @param integer $priority + * @return integer + */ + public function getPriorityForProject($dst_project_id, $priority) + { + $settings = $this->getPrioritySettings($dst_project_id); + + if ($priority >= $settings['priority_start'] && $priority <= $settings['priority_end']) { + return $priority; + } + + return $settings['priority_default']; + } +} diff --git a/app/Model/SwimlaneModel.php b/app/Model/SwimlaneModel.php index 35e39879..f20bfa2f 100644 --- a/app/Model/SwimlaneModel.php +++ b/app/Model/SwimlaneModel.php @@ -94,15 +94,17 @@ class SwimlaneModel extends Base * * @access public * @param integer $project_id - * @return array + * @return array|null */ public function getFirstActiveSwimlane($project_id) { - return $this->db->table(self::TABLE) - ->eq('is_active', self::ACTIVE) - ->eq('project_id', $project_id) - ->orderBy('position', 'asc') - ->findOne(); + $swimlanes = $this->getSwimlanes($project_id); + + if (empty($swimlanes)) { + return null; + } + + return $swimlanes[0]; } /** @@ -184,18 +186,18 @@ class SwimlaneModel extends Base ->orderBy('position', 'asc') ->findAll(); - $default_swimlane = $this->db + $defaultSwimlane = $this->db ->table(ProjectModel::TABLE) ->eq('id', $project_id) ->eq('show_default_swimlane', 1) ->findOneColumn('default_swimlane'); - if ($default_swimlane) { - if ($default_swimlane === 'Default swimlane') { - $default_swimlane = t($default_swimlane); + if ($defaultSwimlane) { + if ($defaultSwimlane === 'Default swimlane') { + $defaultSwimlane = t($defaultSwimlane); } - array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane)); + array_unshift($swimlanes, array('id' => 0, 'name' => $defaultSwimlane)); } return $swimlanes; diff --git a/app/Model/TagDuplicationModel.php b/app/Model/TagDuplicationModel.php new file mode 100644 index 00000000..fb0d8170 --- /dev/null +++ b/app/Model/TagDuplicationModel.php @@ -0,0 +1,87 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Tag Duplication + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class TagDuplicationModel extends Base +{ + /** + * Duplicate project tags to another project + * + * @access public + * @param integer $src_project_id + * @param integer $dst_project_id + * @return bool + */ + public function duplicate($src_project_id, $dst_project_id) + { + $tags = $this->tagModel->getAllByProject($src_project_id); + $results = array(); + + foreach ($tags as $tag) { + $results[] = $this->tagModel->create($dst_project_id, $tag['name']); + } + + return ! in_array(false, $results, true); + } + + /** + * Link tags to the new tasks + * + * @access public + * @param integer $src_task_id + * @param integer $dst_task_id + * @param integer $dst_project_id + */ + public function duplicateTaskTagsToAnotherProject($src_task_id, $dst_task_id, $dst_project_id) + { + $tags = $this->taskTagModel->getTagsByTask($src_task_id); + + foreach ($tags as $tag) { + $tag_id = $this->tagModel->getIdByName($dst_project_id, $tag['name']); + + if ($tag_id) { + $this->taskTagModel->associateTag($dst_task_id, $tag_id); + } + } + } + + /** + * Duplicate tags to the new task + * + * @access public + * @param integer $src_task_id + * @param integer $dst_task_id + */ + public function duplicateTaskTags($src_task_id, $dst_task_id) + { + $tags = $this->taskTagModel->getTagsByTask($src_task_id); + + foreach ($tags as $tag) { + $this->taskTagModel->associateTag($dst_task_id, $tag['id']); + } + } + + /** + * Remove tags that are not available in destination project + * + * @access public + * @param integer $task_id + * @param integer $dst_project_id + */ + public function syncTaskTagsToAnotherProject($task_id, $dst_project_id) + { + $tag_ids = $this->taskTagModel->getTagIdsByTaskNotAvailableInProject($task_id, $dst_project_id); + + foreach ($tag_ids as $tag_id) { + $this->taskTagModel->dissociateTag($task_id, $tag_id); + } + } +} diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index fa2d32c6..cd70a028 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -60,7 +60,7 @@ class TaskCreationModel extends Base $values = $this->dateParser->convert($values, array('date_started'), true); $this->helper->model->removeFields($values, array('another_task')); - $this->helper->model->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->helper->model->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); if (empty($values['column_id'])) { $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); diff --git a/app/Model/TaskDuplicationModel.php b/app/Model/TaskDuplicationModel.php index 9a4613e2..c9079653 100644 --- a/app/Model/TaskDuplicationModel.php +++ b/app/Model/TaskDuplicationModel.php @@ -2,10 +2,7 @@ namespace Kanboard\Model; -use DateTime; -use DateInterval; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Duplication @@ -18,10 +15,10 @@ class TaskDuplicationModel extends Base /** * Fields to copy when duplicating a task * - * @access private - * @var array + * @access protected + * @var string[] */ - private $fields_to_duplicate = array( + protected $fieldsToDuplicate = array( 'title', 'description', 'date_due', @@ -30,6 +27,7 @@ class TaskDuplicationModel extends Base 'column_id', 'owner_id', 'score', + 'priority', 'category_id', 'time_estimated', 'swimlane_id', @@ -49,106 +47,13 @@ class TaskDuplicationModel extends Base */ public function duplicate($task_id) { - return $this->save($task_id, $this->copyFields($task_id)); - } + $new_task_id = $this->save($task_id, $this->copyFields($task_id)); - /** - * Duplicate recurring task - * - * @access public - * @param integer $task_id Task id - * @return boolean|integer Recurrence task id - */ - public function duplicateRecurringTask($task_id) - { - $values = $this->copyFields($task_id); - - if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { - $values['recurrence_parent'] = $task_id; - $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); - $this->calculateRecurringTaskDueDate($values); - - $recurring_task_id = $this->save($task_id, $values); - - if ($recurring_task_id > 0) { - $parent_update = $this->db - ->table(TaskModel::TABLE) - ->eq('id', $task_id) - ->update(array( - 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED, - 'recurrence_child' => $recurring_task_id, - )); - - if ($parent_update) { - return $recurring_task_id; - } - } + if ($new_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id); } - return false; - } - - /** - * Duplicate a task to another project - * - * @access public - * @param integer $task_id - * @param integer $project_id - * @param integer $swimlane_id - * @param integer $column_id - * @param integer $category_id - * @param integer $owner_id - * @return boolean|integer - */ - public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) - { - $values = $this->copyFields($task_id); - $values['project_id'] = $project_id; - $values['column_id'] = $column_id !== null ? $column_id : $values['column_id']; - $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id']; - $values['category_id'] = $category_id !== null ? $category_id : $values['category_id']; - $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id']; - - $this->checkDestinationProjectValues($values); - - return $this->save($task_id, $values); - } - - /** - * Move a task to another project - * - * @access public - * @param integer $task_id - * @param integer $project_id - * @param integer $swimlane_id - * @param integer $column_id - * @param integer $category_id - * @param integer $owner_id - * @return boolean - */ - public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) - { - $task = $this->taskFinderModel->getById($task_id); - - $values = array(); - $values['is_active'] = 1; - $values['project_id'] = $project_id; - $values['column_id'] = $column_id !== null ? $column_id : $task['column_id']; - $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1; - $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; - $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; - $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id']; - - $this->checkDestinationProjectValues($values); - - if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { - $this->container['dispatcher']->dispatch( - TaskModel::EVENT_MOVE_PROJECT, - new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))) - ); - } - - return true; + return $new_task_id; } /** @@ -191,58 +96,28 @@ class TaskDuplicationModel extends Base $values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']); } - return $values; - } + // Check if priority exists for destination project + $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject( + $values['project_id'], + empty($values['priority']) ? 0 : $values['priority'] + ); - /** - * Calculate new due date for new recurrence task - * - * @access public - * @param array $values Task fields - */ - public function calculateRecurringTaskDueDate(array &$values) - { - if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) { - if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) { - $values['date_due'] = time(); - } - - $factor = abs($values['recurrence_factor']); - $subtract = $values['recurrence_factor'] < 0; - - switch ($values['recurrence_timeframe']) { - case TaskModel::RECURRING_TIMEFRAME_MONTHS: - $interval = 'P' . $factor . 'M'; - break; - case TaskModel::RECURRING_TIMEFRAME_YEARS: - $interval = 'P' . $factor . 'Y'; - break; - default: - $interval = 'P' . $factor . 'D'; - } - - $date_due = new DateTime(); - $date_due->setTimestamp($values['date_due']); - - $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval)); - - $values['date_due'] = $date_due->getTimestamp(); - } + return $values; } /** * Duplicate fields for the new task * - * @access private + * @access protected * @param integer $task_id Task id * @return array */ - private function copyFields($task_id) + protected function copyFields($task_id) { $task = $this->taskFinderModel->getById($task_id); $values = array(); - foreach ($this->fields_to_duplicate as $field) { + foreach ($this->fieldsToDuplicate as $field) { $values[$field] = $task[$field]; } @@ -252,16 +127,16 @@ class TaskDuplicationModel extends Base /** * Create the new task and duplicate subtasks * - * @access private + * @access protected * @param integer $task_id Task id * @param array $values Form values * @return boolean|integer */ - private function save($task_id, array $values) + protected function save($task_id, array $values) { $new_task_id = $this->taskCreationModel->create($values); - if ($new_task_id) { + if ($new_task_id !== false) { $this->subtaskModel->duplicate($task_id, $new_task_id); } diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php index 0e99c407..7268052c 100644 --- a/app/Model/TaskFinderModel.php +++ b/app/Model/TaskFinderModel.php @@ -81,7 +81,8 @@ class TaskFinderModel extends Base ->join(ColumnModel::TABLE, 'id', 'column_id') ->eq(TaskModel::TABLE.'.owner_id', $user_id) ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN) - ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE); + ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE) + ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0); } /** @@ -166,6 +167,7 @@ class TaskFinderModel extends Base ->table(TaskModel::TABLE) ->eq(TaskModel::TABLE.'.project_id', $project_id) ->eq(TaskModel::TABLE.'.is_active', $status_id) + ->asc(TaskModel::TABLE.'.id') ->findAll(); } @@ -183,7 +185,8 @@ class TaskFinderModel extends Base ->table(TaskModel::TABLE) ->eq(TaskModel::TABLE.'.project_id', $project_id) ->in(TaskModel::TABLE.'.is_active', $status) - ->findAllByColumn('id'); + ->asc(TaskModel::TABLE.'.id') + ->findAllByColumn(TaskModel::TABLE.'.id'); } /** @@ -367,6 +370,7 @@ class TaskFinderModel extends Base 'ua.name AS assignee_name', 'ua.username AS assignee_username', 'uc.email AS creator_email', + 'uc.name AS creator_name', 'uc.username AS creator_username' ); } diff --git a/app/Model/TaskModel.php b/app/Model/TaskModel.php index b0e7772a..5cddb509 100644 --- a/app/Model/TaskModel.php +++ b/app/Model/TaskModel.php @@ -5,7 +5,7 @@ namespace Kanboard\Model; use Kanboard\Core\Base; /** - * Task model + * Task Model * * @package Kanboard\Model * @author Frederic Guillot @@ -17,80 +17,80 @@ class TaskModel extends Base * * @var string */ - const TABLE = 'tasks'; + const TABLE = 'tasks'; /** * Task status * * @var integer */ - const STATUS_OPEN = 1; - const STATUS_CLOSED = 0; + const STATUS_OPEN = 1; + const STATUS_CLOSED = 0; /** * Events * * @var string */ - const EVENT_MOVE_PROJECT = 'task.move.project'; - const EVENT_MOVE_COLUMN = 'task.move.column'; - const EVENT_MOVE_POSITION = 'task.move.position'; - const EVENT_MOVE_SWIMLANE = 'task.move.swimlane'; - const EVENT_UPDATE = 'task.update'; - const EVENT_CREATE = 'task.create'; - const EVENT_CLOSE = 'task.close'; - const EVENT_OPEN = 'task.open'; - const EVENT_CREATE_UPDATE = 'task.create_update'; + const EVENT_MOVE_PROJECT = 'task.move.project'; + const EVENT_MOVE_COLUMN = 'task.move.column'; + const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_MOVE_SWIMLANE = 'task.move.swimlane'; + const EVENT_UPDATE = 'task.update'; + const EVENT_CREATE = 'task.create'; + const EVENT_CLOSE = 'task.close'; + const EVENT_OPEN = 'task.open'; + const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; - const EVENT_OVERDUE = 'task.overdue'; - const EVENT_USER_MENTION = 'task.user.mention'; - const EVENT_DAILY_CRONJOB = 'task.cronjob.daily'; + const EVENT_OVERDUE = 'task.overdue'; + const EVENT_USER_MENTION = 'task.user.mention'; + const EVENT_DAILY_CRONJOB = 'task.cronjob.daily'; /** * Recurrence: status * * @var integer */ - const RECURRING_STATUS_NONE = 0; - const RECURRING_STATUS_PENDING = 1; - const RECURRING_STATUS_PROCESSED = 2; + const RECURRING_STATUS_NONE = 0; + const RECURRING_STATUS_PENDING = 1; + const RECURRING_STATUS_PROCESSED = 2; /** * Recurrence: trigger * * @var integer */ - const RECURRING_TRIGGER_FIRST_COLUMN = 0; - const RECURRING_TRIGGER_LAST_COLUMN = 1; - const RECURRING_TRIGGER_CLOSE = 2; + const RECURRING_TRIGGER_FIRST_COLUMN = 0; + const RECURRING_TRIGGER_LAST_COLUMN = 1; + const RECURRING_TRIGGER_CLOSE = 2; /** * Recurrence: timeframe * * @var integer */ - const RECURRING_TIMEFRAME_DAYS = 0; - const RECURRING_TIMEFRAME_MONTHS = 1; - const RECURRING_TIMEFRAME_YEARS = 2; + const RECURRING_TIMEFRAME_DAYS = 0; + const RECURRING_TIMEFRAME_MONTHS = 1; + const RECURRING_TIMEFRAME_YEARS = 2; /** * Recurrence: base date used to calculate new due date * * @var integer */ - const RECURRING_BASEDATE_DUEDATE = 0; - const RECURRING_BASEDATE_TRIGGERDATE = 1; + const RECURRING_BASEDATE_DUEDATE = 0; + const RECURRING_BASEDATE_TRIGGERDATE = 1; /** * Remove a task * * @access public - * @param integer $task_id Task id + * @param integer $task_id Task id * @return boolean */ public function remove($task_id) { - if (! $this->taskFinderModel->exists($task_id)) { + if (!$this->taskFinderModel->exists($task_id)) { return false; } @@ -105,7 +105,7 @@ class TaskModel extends Base * Example: "Fix bug #1234" will return 1234 * * @access public - * @param string $message Text + * @param string $message Text * @return integer */ public function getTaskIdFromText($message) @@ -118,69 +118,11 @@ class TaskModel extends Base } /** - * Return the list user selectable recurrence status - * - * @access public - * @return array - */ - public function getRecurrenceStatusList() - { - return array( - TaskModel::RECURRING_STATUS_NONE => t('No'), - TaskModel::RECURRING_STATUS_PENDING => t('Yes'), - ); - } - - /** - * Return the list recurrence triggers - * - * @access public - * @return array - */ - public function getRecurrenceTriggerList() - { - return array( - TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), - TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), - TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'), - ); - } - - /** - * Return the list options to calculate recurrence due date - * - * @access public - * @return array - */ - public function getRecurrenceBasedateList() - { - return array( - TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), - TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), - ); - } - - /** - * Return the list recurrence timeframes - * - * @access public - * @return array - */ - public function getRecurrenceTimeframeList() - { - return array( - TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), - TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), - TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), - ); - } - - /** * Get task progress based on the column position * * @access public - * @param array $task - * @param array $columns + * @param array $task + * @param array $columns * @return integer */ public function getProgress(array $task, array $columns) @@ -201,25 +143,4 @@ class TaskModel extends Base return round(($position * 100) / count($columns), 1); } - - /** - * Helper method to duplicate all tasks to another project - * - * @access public - * @param integer $src_project_id - * @param integer $dst_project_id - * @return boolean - */ - public function duplicate($src_project_id, $dst_project_id) - { - $task_ids = $this->taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)); - - foreach ($task_ids as $task_id) { - if (! $this->taskDuplicationModel->duplicateToProject($task_id, $dst_project_id)) { - return false; - } - } - - return true; - } } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index 1b176a41..be5f53c8 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -108,8 +108,6 @@ class TaskModificationModel extends Base if (isset($values['tags'])) { $this->taskTagModel->save($original_task['project_id'], $values['id'], $values['tags']); unset($values['tags']); - } else { - $this->taskTagModel->save($original_task['project_id'], $values['id'], array()); } } } diff --git a/app/Model/TaskProjectDuplicationModel.php b/app/Model/TaskProjectDuplicationModel.php new file mode 100644 index 00000000..8ebed255 --- /dev/null +++ b/app/Model/TaskProjectDuplicationModel.php @@ -0,0 +1,60 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task Project Duplication + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class TaskProjectDuplicationModel extends TaskDuplicationModel +{ + /** + * Duplicate a task to another project + * + * @access public + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @return boolean|integer + */ + public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + $values = $this->prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + $this->checkDestinationProjectValues($values); + $new_task_id = $this->save($task_id, $values); + + if ($new_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id); + } + + return $new_task_id; + } + + /** + * Prepare values before duplication + * + * @access protected + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @return array + */ + protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id) + { + $values = $this->copyFields($task_id); + $values['project_id'] = $project_id; + $values['column_id'] = $column_id !== null ? $column_id : $values['column_id']; + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $values['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id']; + return $values; + } +} diff --git a/app/Model/TaskProjectMoveModel.php b/app/Model/TaskProjectMoveModel.php new file mode 100644 index 00000000..eda23c0b --- /dev/null +++ b/app/Model/TaskProjectMoveModel.php @@ -0,0 +1,68 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Event\TaskEvent; + +/** + * Task Project Move + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class TaskProjectMoveModel extends TaskDuplicationModel +{ + /** + * Move a task to another project + * + * @access public + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @return boolean + */ + public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + $task = $this->taskFinderModel->getById($task_id); + $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task); + + $this->checkDestinationProjectValues($values); + $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); + + if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { + $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))); + $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event); + } + + return true; + } + + /** + * Prepare new task values + * + * @access protected + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @param array $task + * @return array + */ + protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task) + { + $values = array(); + $values['is_active'] = 1; + $values['project_id'] = $project_id; + $values['column_id'] = $column_id !== null ? $column_id : $task['column_id']; + $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1; + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id']; + $values['priority'] = $task['priority']; + return $values; + } +} diff --git a/app/Model/TaskRecurrenceModel.php b/app/Model/TaskRecurrenceModel.php new file mode 100644 index 00000000..ffe43f8c --- /dev/null +++ b/app/Model/TaskRecurrenceModel.php @@ -0,0 +1,147 @@ +<?php + +namespace Kanboard\Model; + +use DateInterval; +use DateTime; + +/** + * Task Recurrence + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class TaskRecurrenceModel extends TaskDuplicationModel +{ + /** + * Return the list user selectable recurrence status + * + * @access public + * @return array + */ + public function getRecurrenceStatusList() + { + return array( + TaskModel::RECURRING_STATUS_NONE => t('No'), + TaskModel::RECURRING_STATUS_PENDING => t('Yes'), + ); + } + + /** + * Return the list recurrence triggers + * + * @access public + * @return array + */ + public function getRecurrenceTriggerList() + { + return array( + TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), + TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), + TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'), + ); + } + + /** + * Return the list options to calculate recurrence due date + * + * @access public + * @return array + */ + public function getRecurrenceBasedateList() + { + return array( + TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), + TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), + ); + } + + /** + * Return the list recurrence timeframes + * + * @access public + * @return array + */ + public function getRecurrenceTimeframeList() + { + return array( + TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), + TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), + TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), + ); + } + + /** + * Duplicate recurring task + * + * @access public + * @param integer $task_id Task id + * @return boolean|integer Recurrence task id + */ + public function duplicateRecurringTask($task_id) + { + $values = $this->copyFields($task_id); + + if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { + $values['recurrence_parent'] = $task_id; + $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); + $this->calculateRecurringTaskDueDate($values); + + $recurring_task_id = $this->save($task_id, $values); + + if ($recurring_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id); + + $parent_update = $this->db + ->table(TaskModel::TABLE) + ->eq('id', $task_id) + ->update(array( + 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED, + 'recurrence_child' => $recurring_task_id, + )); + + if ($parent_update) { + return $recurring_task_id; + } + } + } + + return false; + } + + /** + * Calculate new due date for new recurrence task + * + * @access public + * @param array $values Task fields + */ + public function calculateRecurringTaskDueDate(array &$values) + { + if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) { + if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) { + $values['date_due'] = time(); + } + + $factor = abs($values['recurrence_factor']); + $subtract = $values['recurrence_factor'] < 0; + + switch ($values['recurrence_timeframe']) { + case TaskModel::RECURRING_TIMEFRAME_MONTHS: + $interval = 'P' . $factor . 'M'; + break; + case TaskModel::RECURRING_TIMEFRAME_YEARS: + $interval = 'P' . $factor . 'Y'; + break; + default: + $interval = 'P' . $factor . 'D'; + } + + $date_due = new DateTime(); + $date_due->setTimestamp($values['date_due']); + + $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval)); + + $values['date_due'] = $date_due->getTimestamp(); + } + } +} diff --git a/app/Model/TaskTagModel.php b/app/Model/TaskTagModel.php index 91dfd224..0553cc6c 100644 --- a/app/Model/TaskTagModel.php +++ b/app/Model/TaskTagModel.php @@ -20,6 +20,23 @@ class TaskTagModel extends Base const TABLE = 'task_has_tags'; /** + * Get all tags not available in a project + * + * @access public + * @param integer $task_id + * @param integer $project_id + * @return array + */ + public function getTagIdsByTaskNotAvailableInProject($task_id, $project_id) + { + return $this->db->table(TagModel::TABLE) + ->eq(self::TABLE.'.task_id', $task_id) + ->notIn(TagModel::TABLE.'.project_id', array(0, $project_id)) + ->join(self::TABLE, 'tag_id', 'id') + ->findAllByColumn(TagModel::TABLE.'.id'); + } + + /** * Get all tags associated to a task * * @access public @@ -82,6 +99,7 @@ class TaskTagModel extends Base public function save($project_id, $task_id, array $tags) { $task_tags = $this->getList($task_id); + $tags = array_filter($tags); return $this->associateTags($project_id, $task_id, $task_tags, $tags) && $this->dissociateTags($task_id, $task_tags, $tags); diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 82ccb8c8..99fed66f 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 111; +const VERSION = 112; + +function version_112(PDO $pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN hide_in_dashboard INT DEFAULT 0 NOT NULL'); +} function version_111(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 229cbd25..b982bcae 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 90; +const VERSION = 91; + +function version_91(PDO $pdo) +{ + $pdo->exec("ALTER TABLE columns ADD COLUMN hide_in_dashboard BOOLEAN DEFAULT '0'"); +} function version_90(PDO $pdo) { diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql index 92ca3686..67dd170a 100644 --- a/app/Schema/Sql/mysql.sql +++ b/app/Schema/Sql/mysql.sql @@ -45,6 +45,7 @@ CREATE TABLE `columns` ( `project_id` int(11) NOT NULL, `task_limit` int(11) DEFAULT '0', `description` text, + `hide_in_dashboard` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_title_project` (`title`,`project_id`), KEY `columns_project_idx` (`project_id`), @@ -414,6 +415,17 @@ CREATE TABLE `swimlanes` ( CONSTRAINT `swimlanes_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `tags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tags` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `project_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `project_id` (`project_id`,`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `task_has_external_links`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -479,6 +491,18 @@ CREATE TABLE `task_has_metadata` ( CONSTRAINT `task_has_metadata_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `task_has_tags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `task_has_tags` ( + `task_id` int(11) NOT NULL, + `tag_id` int(11) NOT NULL, + UNIQUE KEY `tag_id` (`tag_id`,`task_id`), + KEY `task_id` (`task_id`), + CONSTRAINT `task_has_tags_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE, + CONSTRAINT `task_has_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `tasks`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -647,7 +671,7 @@ CREATE TABLE `users` ( LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; -INSERT INTO `settings` VALUES ('api_token','e8a7a983f25efa80e203d44a832c9570a5083d3fefa91366989c00e931d0',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','296892f9c821909a92df539b028fdb384e47c9f7a34a8f9cad598e0edbba',0,0),('webhook_url','',0,0); +INSERT INTO `settings` VALUES ('api_token','19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','1d62395a742260738a406789366a84138ced50a1be62e8862c5cf8d0a561',0,0),('webhook_url','',0,0); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -676,4 +700,4 @@ UNLOCK TABLES; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$kliMGeKgDYtx9Igek9jGDu0eZM.KXivgzvqtnMuWMkjvZiIc.8p8S', 'app-admin');INSERT INTO schema_version VALUES ('110'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$g28mYPBdsf3/gX/ayd7A8.HSPBRQ/zM/PXlfijelJhXwhnukCRIDi', 'app-admin');INSERT INTO schema_version VALUES ('112'); diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql index 6c17c1b1..5b4142b7 100644 --- a/app/Schema/Sql/postgres.sql +++ b/app/Schema/Sql/postgres.sql @@ -98,7 +98,8 @@ CREATE TABLE "columns" ( "position" integer, "project_id" integer NOT NULL, "task_limit" integer DEFAULT 0, - "description" "text" + "description" "text", + "hide_in_dashboard" boolean DEFAULT false ); @@ -740,6 +741,36 @@ ALTER SEQUENCE "swimlanes_id_seq" OWNED BY "swimlanes"."id"; -- +-- Name: tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE "tags" ( + "id" integer NOT NULL, + "name" character varying(255) NOT NULL, + "project_id" integer NOT NULL +); + + +-- +-- Name: tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE "tags_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE "tags_id_seq" OWNED BY "tags"."id"; + + +-- -- Name: task_has_external_links; Type: TABLE; Schema: public; Owner: - -- @@ -874,6 +905,16 @@ ALTER SEQUENCE "task_has_subtasks_id_seq" OWNED BY "subtasks"."id"; -- +-- Name: task_has_tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE "task_has_tags" ( + "task_id" integer NOT NULL, + "tag_id" integer NOT NULL +); + + +-- -- Name: tasks; Type: TABLE; Schema: public; Owner: - -- @@ -1236,6 +1277,13 @@ ALTER TABLE ONLY "swimlanes" ALTER COLUMN "id" SET DEFAULT "nextval"('"swimlanes -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- +ALTER TABLE ONLY "tags" ALTER COLUMN "id" SET DEFAULT "nextval"('"tags_id_seq"'::"regclass"); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + ALTER TABLE ONLY "task_has_external_links" ALTER COLUMN "id" SET DEFAULT "nextval"('"task_has_external_links_id_seq"'::"regclass"); @@ -1545,6 +1593,22 @@ ALTER TABLE ONLY "swimlanes" -- +-- Name: tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY "tags" + ADD CONSTRAINT "tags_pkey" PRIMARY KEY ("id"); + + +-- +-- Name: tags_project_id_name_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY "tags" + ADD CONSTRAINT "tags_project_id_name_key" UNIQUE ("project_id", "name"); + + +-- -- Name: task_has_external_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1585,6 +1649,14 @@ ALTER TABLE ONLY "subtasks" -- +-- Name: task_has_tags_tag_id_task_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY "task_has_tags" + ADD CONSTRAINT "task_has_tags_tag_id_task_id_key" UNIQUE ("tag_id", "task_id"); + + +-- -- Name: tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2031,6 +2103,22 @@ ALTER TABLE ONLY "subtasks" -- +-- Name: task_has_tags_tag_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY "task_has_tags" + ADD CONSTRAINT "task_has_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE; + + +-- +-- Name: task_has_tags_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY "task_has_tags" + ADD CONSTRAINT "task_has_tags_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE; + + +-- -- Name: tasks_column_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2155,8 +2243,8 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_high INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '85b9f242e49f4c50176591a2f9b812c626384b89ff985a02068455a5be07', 0, 0); -INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '207d1aaeb9d6d5c01f9ef1e6d61baca86c4c66fdd0b95e76b5c5953681e4', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', 'c9a7c2a4523f1724b2ca047c5685f8e2b26bba47eb69baf4f22d5d50d837', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c57a6cb1789269547b616454e4e2f06d3de0514f83baf8fa5b5a8af44a08', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0); INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0); @@ -2225,4 +2313,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true); -- PostgreSQL database dump complete -- -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$kliMGeKgDYtx9Igek9jGDu0eZM.KXivgzvqtnMuWMkjvZiIc.8p8S', 'app-admin');INSERT INTO schema_version VALUES ('89'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$g28mYPBdsf3/gX/ayd7A8.HSPBRQ/zM/PXlfijelJhXwhnukCRIDi', 'app-admin');INSERT INTO schema_version VALUES ('91'); diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index dac348d4..2a7735ee 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 102; +const VERSION = 103; + +function version_103(PDO $pdo) +{ + $pdo->exec("ALTER TABLE columns ADD COLUMN hide_in_dashboard INTEGER DEFAULT 0 NOT NULL"); +} function version_102(PDO $pdo) { diff --git a/app/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php index f88d9b4f..5cf6231c 100644 --- a/app/ServiceProvider/ApiProvider.php +++ b/app/ServiceProvider/ApiProvider.php @@ -9,6 +9,8 @@ use Kanboard\Api\Procedure\BoardProcedure; use Kanboard\Api\Procedure\CategoryProcedure; use Kanboard\Api\Procedure\ColumnProcedure; use Kanboard\Api\Procedure\CommentProcedure; +use Kanboard\Api\Procedure\ProjectFileProcedure; +use Kanboard\Api\Procedure\TaskExternalLinkProcedure; use Kanboard\Api\Procedure\TaskFileProcedure; use Kanboard\Api\Procedure\GroupProcedure; use Kanboard\Api\Procedure\GroupMemberProcedure; @@ -57,6 +59,7 @@ class ApiProvider implements ServiceProviderInterface ->withObject(new CategoryProcedure($container)) ->withObject(new CommentProcedure($container)) ->withObject(new TaskFileProcedure($container)) + ->withObject(new ProjectFileProcedure($container)) ->withObject(new LinkProcedure($container)) ->withObject(new ProjectProcedure($container)) ->withObject(new ProjectPermissionProcedure($container)) @@ -65,6 +68,7 @@ class ApiProvider implements ServiceProviderInterface ->withObject(new SwimlaneProcedure($container)) ->withObject(new TaskProcedure($container)) ->withObject(new TaskLinkProcedure($container)) + ->withObject(new TaskExternalLinkProcedure($container)) ->withObject(new UserProcedure($container)) ->withObject(new GroupProcedure($container)) ->withObject(new GroupMemberProcedure($container)) diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index 751fe514..978bc05b 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -202,8 +202,10 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('SubtaskProcedure', '*', Role::PROJECT_MEMBER); $acl->add('SubtaskTimeTrackingProcedure', '*', Role::PROJECT_MEMBER); $acl->add('SwimlaneProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectFileProcedure', '*', Role::PROJECT_MEMBER); $acl->add('TaskFileProcedure', '*', Role::PROJECT_MEMBER); $acl->add('TaskLinkProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskExternalLinkProcedure', array('createExternalTaskLink', 'updateExternalTaskLink', 'removeExternalTaskLink'), Role::PROJECT_MEMBER); $acl->add('TaskProcedure', '*', Role::PROJECT_MEMBER); return $acl; diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index c0fb93bd..e32c0d43 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -55,16 +55,22 @@ class ClassProvider implements ServiceProviderInterface 'ProjectNotificationModel', 'ProjectMetadataModel', 'ProjectGroupRoleModel', + 'ProjectTaskDuplicationModel', + 'ProjectTaskPriorityModel', 'ProjectUserRoleModel', 'RememberMeSessionModel', 'SubtaskModel', 'SubtaskTimeTrackingModel', 'SwimlaneModel', + 'TagDuplicationModel', 'TagModel', 'TaskModel', 'TaskAnalyticModel', 'TaskCreationModel', 'TaskDuplicationModel', + 'TaskProjectDuplicationModel', + 'TaskProjectMoveModel', + 'TaskRecurrenceModel', 'TaskExternalLinkModel', 'TaskFinderModel', 'TaskFileModel', diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php index 75b7ff76..21cd3996 100644 --- a/app/Subscriber/RecurringTaskSubscriber.php +++ b/app/Subscriber/RecurringTaskSubscriber.php @@ -22,9 +22,9 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { if ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_FIRST_COLUMN && $this->columnModel->getFirstColumnId($event['project_id']) == $event['src_column_id']) { - $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']); + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } elseif ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_LAST_COLUMN && $this->columnModel->getLastColumnId($event['project_id']) == $event['dst_column_id']) { - $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']); + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } } } @@ -34,7 +34,7 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI $this->logger->debug('Subscriber executed: '.__METHOD__); if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_CLOSE) { - $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']); + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } } } diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php index dbe775d5..bc34363c 100644 --- a/app/Template/board/task_footer.php +++ b/app/Template/board/task_footer.php @@ -37,7 +37,7 @@ <?php endif ?> <?php if (! empty($task['date_due'])): ?> - <?php if (date('d') == date('d', $task['date_due'])): ?> + <?php if (date('Y-m-d') == date('Y-m-d', $task['date_due'])): ?> <span class="task-board-date task-board-date-today"> <?php elseif (time() > $task['date_due']): ?> <span class="task-board-date task-board-date-overdue"> diff --git a/app/Template/column/create.php b/app/Template/column/create.php index 023de525..812e9139 100644 --- a/app/Template/column/create.php +++ b/app/Template/column/create.php @@ -13,6 +13,8 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?> + <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php index a742e4b9..89487298 100644 --- a/app/Template/column/edit.php +++ b/app/Template/column/edit.php @@ -15,6 +15,8 @@ <?= $this->form->label(t('Task limit'), 'task_limit') ?> <?= $this->form->number('task_limit', $values, $errors) ?> + <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> + <?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> diff --git a/app/Template/dashboard/notifications.php b/app/Template/dashboard/notifications.php index e0e9b878..3b70b49f 100644 --- a/app/Template/dashboard/notifications.php +++ b/app/Template/dashboard/notifications.php @@ -36,10 +36,8 @@ <i class="fa fa-file-o fa-fw"></i> <?php endif ?> - <?php if ($this->text->contains($notification['event_name'], 'task.overdue')): ?> - <?php if (count($notification['event_data']['tasks']) > 1): ?> - <?= $notification['title'] ?> - <?php endif ?> + <?php if ($this->text->contains($notification['event_name'], 'task.overdue') && count($notification['event_data']['tasks']) > 1): ?> + <?= $notification['title'] ?> <?php else: ?> <?= $this->url->link($notification['title'], 'WebNotificationController', 'redirect', array('notification_id' => $notification['id'], 'user_id' => $user['id'])) ?> <?php endif ?> diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php index 01d06bab..d00883ba 100644 --- a/app/Template/project_creation/create.php +++ b/app/Template/project_creation/create.php @@ -23,9 +23,10 @@ <?php endif ?> <?= $this->form->checkbox('categoryModel', t('Categories'), 1, true) ?> + <?= $this->form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?> <?= $this->form->checkbox('actionModel', t('Actions'), 1, true) ?> <?= $this->form->checkbox('swimlaneModel', t('Swimlanes'), 1, true) ?> - <?= $this->form->checkbox('taskModel', t('Tasks'), 1, false) ?> + <?= $this->form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?> </div> <div class="form-actions"> diff --git a/app/Template/project_view/duplicate.php b/app/Template/project_view/duplicate.php index d2cd127a..d66ff591 100644 --- a/app/Template/project_view/duplicate.php +++ b/app/Template/project_view/duplicate.php @@ -15,10 +15,11 @@ <?php endif ?> <?= $this->form->checkbox('categoryModel', t('Categories'), 1, true) ?> + <?= $this->form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?> <?= $this->form->checkbox('actionModel', t('Actions'), 1, true) ?> <?= $this->form->checkbox('swimlaneModel', t('Swimlanes'), 1, false) ?> - <?= $this->form->checkbox('taskModel', t('Tasks'), 1, false) ?> <?= $this->form->checkbox('projectMetadataModel', t('Metadata'), 1, false) ?> + <?= $this->form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?> <div class="form-actions"> <button type="submit" class="btn btn-red"><?= t('Duplicate') ?></button> diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php index 5efe8ce6..667a576c 100644 --- a/app/Template/project_view/show.php +++ b/app/Template/project_view/show.php @@ -54,9 +54,10 @@ </div> <table class="table-stripped"> <tr> - <th class="column-60"><?= t('Column') ?></th> + <th class="column-40"><?= t('Column') ?></th> <th class="column-20"><?= t('Task limit') ?></th> <th class="column-20"><?= t('Active tasks') ?></th> + <th class="column-20"><?= t('Hide tasks in this column in the dashboard') ?></th> </tr> <?php foreach ($stats['columns'] as $column): ?> <tr> @@ -70,6 +71,13 @@ </td> <td><?= $column['task_limit'] ?: '∞' ?></td> <td><?= $column['nb_active_tasks'] ?></td> + <td> + <?php if ($column['hide_in_dashboard'] == 1): ?> + <?= t('Yes') ?> + <?php else: ?> + <?= t('No') ?> + <?php endif ?> + </td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/task_creation/show.php b/app/Template/task_creation/show.php index f799919a..57e77f37 100644 --- a/app/Template/task_creation/show.php +++ b/app/Template/task_creation/show.php @@ -27,6 +27,7 @@ <?= $this->task->selectColumn($columns_list, $values, $errors) ?> <?= $this->task->selectPriority($project, $values) ?> <?= $this->task->selectScore($values, $errors) ?> + <?= $this->task->selectReference($values, $errors) ?> <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> </div> diff --git a/app/Template/task_gantt_creation/show.php b/app/Template/task_gantt_creation/show.php index 5e4286bd..7521d805 100644 --- a/app/Template/task_gantt_creation/show.php +++ b/app/Template/task_gantt_creation/show.php @@ -23,6 +23,7 @@ <?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?> <?= $this->task->selectPriority($project, $values) ?> <?= $this->task->selectScore($values, $errors) ?> + <?= $this->task->selectReference($values, $errors) ?> <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> </div> diff --git a/app/Template/task_modification/show.php b/app/Template/task_modification/show.php index d747407e..cc38582c 100644 --- a/app/Template/task_modification/show.php +++ b/app/Template/task_modification/show.php @@ -21,6 +21,7 @@ <?= $this->task->selectCategory($categories_list, $values, $errors) ?> <?= $this->task->selectPriority($project, $values) ?> <?= $this->task->selectScore($values, $errors) ?> + <?= $this->task->selectReference($values, $errors) ?> <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> </div> diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php index b7a95f41..727f9109 100644 --- a/app/User/Avatar/LetterAvatarProvider.php +++ b/app/User/Avatar/LetterAvatarProvider.php @@ -142,7 +142,7 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface // Make hash more sensitive for short string like 'a', 'b', 'c' $str .= 'x'; - $max = intval(9007199254740991 / $seed2); + $max = intval(PHP_INT_MAX / $seed2); for ($i = 0, $ilen = mb_strlen($str, 'UTF-8'); $i < $ilen; $i++) { if ($hash > $max) { diff --git a/app/User/ReverseProxyUserProvider.php b/app/User/ReverseProxyUserProvider.php index 723b8155..34d2187d 100644 --- a/app/User/ReverseProxyUserProvider.php +++ b/app/User/ReverseProxyUserProvider.php @@ -22,14 +22,23 @@ class ReverseProxyUserProvider implements UserProviderInterface protected $username = ''; /** + * User profile if the user already exists + * + * @access protected + * @var array + */ + private $userProfile = array(); + + /** * Constructor * * @access public * @param string $username */ - public function __construct($username) + public function __construct($username, array $userProfile = array()) { $this->username = $username; + $this->userProfile = $userProfile; } /** @@ -84,7 +93,15 @@ class ReverseProxyUserProvider implements UserProviderInterface */ public function getRole() { - return REVERSE_PROXY_DEFAULT_ADMIN === $this->username ? Role::APP_ADMIN : Role::APP_USER; + if (REVERSE_PROXY_DEFAULT_ADMIN === $this->username) { + return Role::APP_ADMIN; + } + + if (isset($this->userProfile['role'])) { + return $this->userProfile['role']; + } + + return Role::APP_USER; } /** diff --git a/app/constants.php b/app/constants.php index 604f6acd..fc120692 100644 --- a/app/constants.php +++ b/app/constants.php @@ -21,7 +21,7 @@ defined('PLUGIN_INSTALLER') or define('PLUGIN_INSTALLER', true); defined('DEBUG') or define('DEBUG', strtolower(getenv('DEBUG')) === 'true'); // Logging drivers: syslog, stdout, stderr or file -defined('LOG_DRIVER') or define('LOG_DRIVER', getenv('LOG_DRIVER')); +defined('LOG_DRIVER') or define('LOG_DRIVER', ''); // Logging file defined('LOG_FILE') or define('LOG_FILE', DATA_DIR.DIRECTORY_SEPARATOR.'debug.log'); |