From 94b9235dcfd87f12f60a2e8ad07e39fcfd067f7c Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 23 Jun 2016 11:48:01 -0400 Subject: Merge manually PR #2367 and #2349 --- doc/es_ES/calendar-configuration.markdown | 43 +++++++++++ doc/es_ES/email-configuration.markdown | 115 ++++++++++++++++++++++++++++ doc/es_ES/kanban-vs-todo-and-scrum.markdown | 38 +++++++++ 3 files changed, 196 insertions(+) create mode 100644 doc/es_ES/calendar-configuration.markdown create mode 100644 doc/es_ES/email-configuration.markdown create mode 100644 doc/es_ES/kanban-vs-todo-and-scrum.markdown (limited to 'doc') diff --git a/doc/es_ES/calendar-configuration.markdown b/doc/es_ES/calendar-configuration.markdown new file mode 100644 index 00000000..ccd83204 --- /dev/null +++ b/doc/es_ES/calendar-configuration.markdown @@ -0,0 +1,43 @@ +Configuración de calendarios +============================ + +Ir al menu de configuraciones, despues elegir cofiguracion de calendarios que se encuentra al lado izquierdo + +![Configuración de calendarios](https://kanboard.net/screenshots/documentation/calendar-settings.png) + +Existe dos diferentes calendarios en kanboard : + +- Calendarios de projectos +- Calendario por usuario (disponible desde el dashboard) + +Calendario por projectos +------------------------ + +Este calendario visualiza las tareas que se le asignan fechas de vencimiento y las tareas estan basadas sobre +la fecha de creación o el inicio de fecha + +### Visualizar tareas basadas en la fecha de creacion + +- El inicio de fecha del evento del calendario es la fecha de creacion de la tarea +- El finalización de fecha del evento es cuendo se completa una tarea + +### Visualizar tareas basadas en las fechas de inicio + +- La fecha de inicio del evento del calendario is la fecha de incio de la tarea +- Esta fecha puede ser definida manualmente. +- La fecha de finalización del evento es la fecha de terminación +- Si no hay una fecha de inicio de la tarea no aparece en el calendario. + +Calendarios por usuarios +------------------------ + +Este calendario visualiza solo las tareas asignadas para el usuario y opcionalmente la información de las subtareas + +### Visualizar subtareas basadas en el tiempo de tracking + +- Despliega la información de las subtareas desde el calendario o en el registro de la tabla de seguimiento de tiempo +- La intersección con los usuarios timetable es calculad + +### Las estimaciones muestran las subtareas ( la previsión de los trabajos futuros ) + +- Mostrar la estimación de los trabajos futuros de las subtareas en estado de "todo" y con un valor definido " estimación " . diff --git a/doc/es_ES/email-configuration.markdown b/doc/es_ES/email-configuration.markdown new file mode 100644 index 00000000..576c62ea --- /dev/null +++ b/doc/es_ES/email-configuration.markdown @@ -0,0 +1,115 @@ +Configuración del Email +======================= + +Configuración de usuarios +------------------------- + +Para recibir notificaciones por email los usuarios de Kanboard deben tener + +- Activar las notificaciones de su perfil +- Tener una dirección valida de email en su perfil +- Ser miembro del proyecto y que este tenga activo la opción de notificaciones + +Nota: El usuario que genera una sesión y que realiza alguna acción no recibe ninguna notificación, sólo otros miembros del proyecto. + +Comunicación con correos electronicos +------------------------------------- + +There are several email transports available: + +- SMTP +- Sendmail +- PHP mail funcion nativa +- Otros métodos que pueden ser proporcionados por externos : Postmark, Sendgrid and Mailgun + +Configuración del servidor +-------------------------- + +Por default, Kanboard usa el bundled PHP mail function para el envio de emails. +Porque usualmente el servidor no requiere una configuración y así tu servidor puede estar listo para enviar emails. + +Sin embargo, es posible usar otros metodos, como el protocolo SMTP y Sendmail + +### Configuración SMTP + +Renombrar el archivo `config.default.php` a `config.php` y modificar estos valores: + +```php +// We choose "smtp" as mail transport +define('MAIL_TRANSPORT', 'smtp'); + +// We define our server settings +define('MAIL_SMTP_HOSTNAME', 'mail.example.com'); +define('MAIL_SMTP_PORT', 25); + +// Credentials for authentication on the SMTP server (not mandatory) +define('MAIL_SMTP_USERNAME', 'username'); +define('MAIL_SMTP_PASSWORD', 'super password'); +``` + +También es posible utilizar una conexión segura, TLS or SSL: + +```php +define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls" +``` + +### Configuración Sendmail + +Por default el comando para el sendmail esta `/usr/sbin/sendmail -bs` Pero usted puede personalizarlo en su archivo de configuración. + +Ejemplo: + +```php +// We choose "sendmail" as mail transport +define('MAIL_TRANSPORT', 'sendmail'); + +// If you need to change the sendmail command, replace the value +define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); +``` + +### PHP funcion nativa de email + +Esta es la configuración por default + +```php +define('MAIL_TRANSPORT', 'mail'); +``` + +### La dirección de correo electrónico del remitente + +Por default, los correos electrónicos utilizarán la dirección del remitente `notifications@kanboard.local`. +con este correo no es posible responderle + +Tu puedes personalizar esta direccion cambiando el valor de la constante `MAIL_FROM` en tu archivo de configuración + +```php +define('MAIL_FROM', 'kanboard@mydomain.tld'); +``` + +Esto puede ser útil si su configuracion del servidor SMTP no acepta una dirección por default. + +### Cómo mostrar un enlace a la tarea en las notificaciones ? + +Para hacer eso, tu tienes que especificar la URL de tu instalación de tu kanboard [Application Settings](https://kanboard.net/documentation/application-configuration). + +De manera predeterminada, no se define nada, por lo que no se mostrará los enlaces. + +Ejemplos : + +- http://demo.kanboard.net/ +- http://myserver/kanboard/ +- http://kanboard.mydomain.com/ + +No se olvide de la barra final `/`. + +Es necesario definir de forma manual debido a que Kanboard no puede adivinar la dirección URL de una secuencia de comandos de línea de comandos y algunas personas tienen una configuración muy específica. + +Solución de problemas +--------------------- + +Si no hay mensajes de correo electrónico se envían y que está seguro de que todo está configurado correctamente entonces: + +- Verificar el correo de spam +- Habilita el modo debug y verifique el archivo `data/debug.log`, Debería ver el error exacto +- Asegúrese de que el servidor o el proveedor de alojamiento le permite enviar mensajes de correo electrónico +- Si usa Selinux Permitir a PHP enviar emails diff --git a/doc/es_ES/kanban-vs-todo-and-scrum.markdown b/doc/es_ES/kanban-vs-todo-and-scrum.markdown new file mode 100644 index 00000000..ad9dd1a9 --- /dev/null +++ b/doc/es_ES/kanban-vs-todo-and-scrum.markdown @@ -0,0 +1,38 @@ +Kanban vs Todo lists and Scrum +============================== + +Kanban vs Todo lists +-------------------- + +### Todo lists (lista de tareas) : + +Fase unica (es solo una lista de tareas) +Multitarea posible (no eficiente) + +### Kanban: + +Multi fases, +Concentración absoluta para evitar multitareas por que se puede establecer un limite por columna para mejorar el progreso + + +Kanban vs Scrum +--------------- + +### Scrum: + +Los sprints son time-boxed, usualmente 2 o 4 semanas +No permitir cambios durante la iteración +La estimación es requerida +Utiliza la velocidad como métrica predeterminada +El tablero de Scrum se borra entre cada sprint +Scrum tiene funciones predefinidas como scrum master , los dueños del producto y el equipo +Una gran cantidad de reuniones: planeaciones, backlogs grooming, daily stand-up, retrospectiva + +### Kanban: + +- Fluido continuo +- Los cambios se pueden crear en cualquier momento +- La estimacion es opcional +- Usa la iniciativa del tiempo de ciclo para apresurar el performance +- el tablero Kanban board es persistente +- Kanban no impone estrictas restricciones y reuniones, el proceso es mas flexible -- cgit v1.2.3 From 75019b3a8e838f51bfac51bdbf9e6647faaaec1d Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 23 Jun 2016 12:27:34 -0400 Subject: Make embedded documentation available in multiple languages --- ChangeLog | 7 ++ app/Controller/DocumentationController.php | 70 +++++++++-- doc/es_ES/kanban-vs-todo-and-scrum.markdown | 22 ++-- doc/fr/2fa.markdown | 33 ----- doc/fr/analytics-tasks.markdown | 24 ---- doc/fr/analytics.markdown | 70 ----------- doc/fr/application-configuration.markdown | 41 ------- doc/fr/application-configuration.markup | 41 ------- doc/fr/automatic-actions.markdown | 133 --------------------- doc/fr/board-collapsed-expanded.markdown | 18 --- doc/fr/board-configuration.markdown | 24 ---- ...-horizontal-scrolling-and-compact-view.markdown | 11 -- doc/fr/board-show-hide-columns.markdown | 12 -- doc/fr/calendar-configuration.markdown | 43 ------- doc/fr/calendar.markdown | 20 ---- doc/fr/closing-tasks.markdown | 16 --- doc/fr/create-tasks-by-email.markdown | 45 ------- doc/fr/creating-projects.markdown | 39 ------ doc/fr/creating-tasks.markdown | 27 ----- doc/fr/currency-rate.markdown | 11 -- doc/fr/duplicate-move-tasks.markdown | 58 --------- doc/fr/editing-projects.markdown | 15 --- doc/fr/gantt-chart-projects.markdown | 17 --- doc/fr/gantt-chart-tasks.markdown | 20 ---- doc/fr/index.markdown | 63 ---------- doc/fr/kanban-vs-todo-and-scrum.markdown | 36 ------ doc/fr/keyboard-shortcuts.markdown | 37 ------ doc/fr/link-labels.markdown | 13 -- doc/fr/notifications.markdown | 45 ------- doc/fr/project-configuration.markdown | 42 ------- doc/fr/project-permissions.markdown | 22 ---- doc/fr/project-types.markdown | 14 --- doc/fr/project-views.markdown | 61 ---------- doc/fr/recurring-tasks.markdown | 24 ---- doc/fr/roles.markdown | 24 ---- doc/fr/screenshots.markdown | 26 ---- doc/fr/screenshots/automatic-action-creation.png | Bin 19900 -> 0 bytes doc/fr/screenshots/board-collapsed-mode.png | Bin 7074 -> 0 bytes doc/fr/screenshots/board-compact-mode.png | Bin 12765 -> 0 bytes doc/fr/screenshots/board-expanded-mode.png | Bin 11783 -> 0 bytes doc/fr/screenshots/board-task-limit.png | Bin 16848 -> 0 bytes doc/fr/screenshots/board-view.png | Bin 24371 -> 0 bytes doc/fr/screenshots/calendar-view.png | Bin 21691 -> 0 bytes doc/fr/screenshots/gantt-view.png | Bin 29367 -> 0 bytes doc/fr/screenshots/hide-column.png | Bin 9642 -> 0 bytes doc/fr/screenshots/list-view.png | Bin 23457 -> 0 bytes doc/fr/screenshots/new-project.png | Bin 20818 -> 0 bytes doc/fr/screenshots/new-user.png | Bin 26286 -> 0 bytes doc/fr/screenshots/project-disable-sharing.png | Bin 17706 -> 0 bytes doc/fr/screenshots/project-edition.png | Bin 40230 -> 0 bytes doc/fr/screenshots/project-enable-sharing.png | Bin 14297 -> 0 bytes doc/fr/screenshots/project-permissions.png | Bin 32926 -> 0 bytes doc/fr/screenshots/project-view.png | Bin 40868 -> 0 bytes doc/fr/screenshots/show-column.png | Bin 17940 -> 0 bytes doc/fr/screenshots/swimlane-configuration.png | Bin 14226 -> 0 bytes doc/fr/screenshots/swimlanes.png | Bin 25290 -> 0 bytes doc/fr/sharing-projects.markdown | 35 ------ doc/fr/subtasks.markdown | 43 ------- doc/fr/swimlanes.markdown | 29 ----- doc/fr/task-links.markdown | 22 ---- doc/fr/time-tracking.markdown | 44 ------- doc/fr/transitions.markdown | 20 ---- doc/fr/usage-examples.markdown | 69 ----------- doc/fr/user-management.markdown | 35 ------ doc/fr/what-is-kanban.markdown | 34 ------ doc/fr_FR/2fa.markdown | 33 +++++ doc/fr_FR/analytics-tasks.markdown | 24 ++++ doc/fr_FR/analytics.markdown | 70 +++++++++++ doc/fr_FR/application-configuration.markdown | 41 +++++++ doc/fr_FR/application-configuration.markup | 41 +++++++ doc/fr_FR/automatic-actions.markdown | 133 +++++++++++++++++++++ doc/fr_FR/board-collapsed-expanded.markdown | 18 +++ doc/fr_FR/board-configuration.markdown | 24 ++++ ...-horizontal-scrolling-and-compact-view.markdown | 11 ++ doc/fr_FR/board-show-hide-columns.markdown | 12 ++ doc/fr_FR/calendar-configuration.markdown | 43 +++++++ doc/fr_FR/calendar.markdown | 20 ++++ doc/fr_FR/closing-tasks.markdown | 16 +++ doc/fr_FR/create-tasks-by-email.markdown | 45 +++++++ doc/fr_FR/creating-projects.markdown | 39 ++++++ doc/fr_FR/creating-tasks.markdown | 27 +++++ doc/fr_FR/currency-rate.markdown | 11 ++ doc/fr_FR/duplicate-move-tasks.markdown | 58 +++++++++ doc/fr_FR/editing-projects.markdown | 15 +++ doc/fr_FR/gantt-chart-projects.markdown | 17 +++ doc/fr_FR/gantt-chart-tasks.markdown | 20 ++++ doc/fr_FR/index.markdown | 63 ++++++++++ doc/fr_FR/kanban-vs-todo-and-scrum.markdown | 36 ++++++ doc/fr_FR/keyboard-shortcuts.markdown | 37 ++++++ doc/fr_FR/link-labels.markdown | 13 ++ doc/fr_FR/notifications.markdown | 45 +++++++ doc/fr_FR/project-configuration.markdown | 42 +++++++ doc/fr_FR/project-permissions.markdown | 22 ++++ doc/fr_FR/project-types.markdown | 14 +++ doc/fr_FR/project-views.markdown | 61 ++++++++++ doc/fr_FR/recurring-tasks.markdown | 24 ++++ doc/fr_FR/roles.markdown | 24 ++++ doc/fr_FR/screenshots.markdown | 26 ++++ .../screenshots/automatic-action-creation.png | Bin 0 -> 19900 bytes doc/fr_FR/screenshots/board-collapsed-mode.png | Bin 0 -> 7074 bytes doc/fr_FR/screenshots/board-compact-mode.png | Bin 0 -> 12765 bytes doc/fr_FR/screenshots/board-expanded-mode.png | Bin 0 -> 11783 bytes doc/fr_FR/screenshots/board-task-limit.png | Bin 0 -> 16848 bytes doc/fr_FR/screenshots/board-view.png | Bin 0 -> 24371 bytes doc/fr_FR/screenshots/calendar-view.png | Bin 0 -> 21691 bytes doc/fr_FR/screenshots/gantt-view.png | Bin 0 -> 29367 bytes doc/fr_FR/screenshots/hide-column.png | Bin 0 -> 9642 bytes doc/fr_FR/screenshots/list-view.png | Bin 0 -> 23457 bytes doc/fr_FR/screenshots/new-project.png | Bin 0 -> 20818 bytes doc/fr_FR/screenshots/new-user.png | Bin 0 -> 26286 bytes doc/fr_FR/screenshots/project-disable-sharing.png | Bin 0 -> 17706 bytes doc/fr_FR/screenshots/project-edition.png | Bin 0 -> 40230 bytes doc/fr_FR/screenshots/project-enable-sharing.png | Bin 0 -> 14297 bytes doc/fr_FR/screenshots/project-permissions.png | Bin 0 -> 32926 bytes doc/fr_FR/screenshots/project-view.png | Bin 0 -> 40868 bytes doc/fr_FR/screenshots/show-column.png | Bin 0 -> 17940 bytes doc/fr_FR/screenshots/swimlane-configuration.png | Bin 0 -> 14226 bytes doc/fr_FR/screenshots/swimlanes.png | Bin 0 -> 25290 bytes doc/fr_FR/sharing-projects.markdown | 35 ++++++ doc/fr_FR/subtasks.markdown | 43 +++++++ doc/fr_FR/swimlanes.markdown | 29 +++++ doc/fr_FR/task-links.markdown | 22 ++++ doc/fr_FR/time-tracking.markdown | 44 +++++++ doc/fr_FR/transitions.markdown | 20 ++++ doc/fr_FR/usage-examples.markdown | 69 +++++++++++ doc/fr_FR/user-management.markdown | 35 ++++++ doc/fr_FR/what-is-kanban.markdown | 34 ++++++ 127 files changed, 1531 insertions(+), 1480 deletions(-) delete mode 100644 doc/fr/2fa.markdown delete mode 100644 doc/fr/analytics-tasks.markdown delete mode 100644 doc/fr/analytics.markdown delete mode 100644 doc/fr/application-configuration.markdown delete mode 100644 doc/fr/application-configuration.markup delete mode 100644 doc/fr/automatic-actions.markdown delete mode 100644 doc/fr/board-collapsed-expanded.markdown delete mode 100644 doc/fr/board-configuration.markdown delete mode 100644 doc/fr/board-horizontal-scrolling-and-compact-view.markdown delete mode 100644 doc/fr/board-show-hide-columns.markdown delete mode 100644 doc/fr/calendar-configuration.markdown delete mode 100644 doc/fr/calendar.markdown delete mode 100644 doc/fr/closing-tasks.markdown delete mode 100644 doc/fr/create-tasks-by-email.markdown delete mode 100644 doc/fr/creating-projects.markdown delete mode 100644 doc/fr/creating-tasks.markdown delete mode 100644 doc/fr/currency-rate.markdown delete mode 100644 doc/fr/duplicate-move-tasks.markdown delete mode 100644 doc/fr/editing-projects.markdown delete mode 100644 doc/fr/gantt-chart-projects.markdown delete mode 100644 doc/fr/gantt-chart-tasks.markdown delete mode 100644 doc/fr/index.markdown delete mode 100644 doc/fr/kanban-vs-todo-and-scrum.markdown delete mode 100644 doc/fr/keyboard-shortcuts.markdown delete mode 100644 doc/fr/link-labels.markdown delete mode 100644 doc/fr/notifications.markdown delete mode 100644 doc/fr/project-configuration.markdown delete mode 100644 doc/fr/project-permissions.markdown delete mode 100644 doc/fr/project-types.markdown delete mode 100644 doc/fr/project-views.markdown delete mode 100644 doc/fr/recurring-tasks.markdown delete mode 100644 doc/fr/roles.markdown delete mode 100644 doc/fr/screenshots.markdown delete mode 100644 doc/fr/screenshots/automatic-action-creation.png delete mode 100644 doc/fr/screenshots/board-collapsed-mode.png delete mode 100644 doc/fr/screenshots/board-compact-mode.png delete mode 100644 doc/fr/screenshots/board-expanded-mode.png delete mode 100644 doc/fr/screenshots/board-task-limit.png delete mode 100644 doc/fr/screenshots/board-view.png delete mode 100644 doc/fr/screenshots/calendar-view.png delete mode 100644 doc/fr/screenshots/gantt-view.png delete mode 100644 doc/fr/screenshots/hide-column.png delete mode 100644 doc/fr/screenshots/list-view.png delete mode 100644 doc/fr/screenshots/new-project.png delete mode 100644 doc/fr/screenshots/new-user.png delete mode 100644 doc/fr/screenshots/project-disable-sharing.png delete mode 100644 doc/fr/screenshots/project-edition.png delete mode 100644 doc/fr/screenshots/project-enable-sharing.png delete mode 100644 doc/fr/screenshots/project-permissions.png delete mode 100644 doc/fr/screenshots/project-view.png delete mode 100644 doc/fr/screenshots/show-column.png delete mode 100644 doc/fr/screenshots/swimlane-configuration.png delete mode 100644 doc/fr/screenshots/swimlanes.png delete mode 100644 doc/fr/sharing-projects.markdown delete mode 100644 doc/fr/subtasks.markdown delete mode 100644 doc/fr/swimlanes.markdown delete mode 100644 doc/fr/task-links.markdown delete mode 100644 doc/fr/time-tracking.markdown delete mode 100644 doc/fr/transitions.markdown delete mode 100644 doc/fr/usage-examples.markdown delete mode 100644 doc/fr/user-management.markdown delete mode 100644 doc/fr/what-is-kanban.markdown create mode 100644 doc/fr_FR/2fa.markdown create mode 100644 doc/fr_FR/analytics-tasks.markdown create mode 100644 doc/fr_FR/analytics.markdown create mode 100644 doc/fr_FR/application-configuration.markdown create mode 100644 doc/fr_FR/application-configuration.markup create mode 100644 doc/fr_FR/automatic-actions.markdown create mode 100644 doc/fr_FR/board-collapsed-expanded.markdown create mode 100644 doc/fr_FR/board-configuration.markdown create mode 100644 doc/fr_FR/board-horizontal-scrolling-and-compact-view.markdown create mode 100644 doc/fr_FR/board-show-hide-columns.markdown create mode 100644 doc/fr_FR/calendar-configuration.markdown create mode 100644 doc/fr_FR/calendar.markdown create mode 100644 doc/fr_FR/closing-tasks.markdown create mode 100644 doc/fr_FR/create-tasks-by-email.markdown create mode 100644 doc/fr_FR/creating-projects.markdown create mode 100644 doc/fr_FR/creating-tasks.markdown create mode 100644 doc/fr_FR/currency-rate.markdown create mode 100644 doc/fr_FR/duplicate-move-tasks.markdown create mode 100644 doc/fr_FR/editing-projects.markdown create mode 100644 doc/fr_FR/gantt-chart-projects.markdown create mode 100644 doc/fr_FR/gantt-chart-tasks.markdown create mode 100644 doc/fr_FR/index.markdown create mode 100644 doc/fr_FR/kanban-vs-todo-and-scrum.markdown create mode 100644 doc/fr_FR/keyboard-shortcuts.markdown create mode 100644 doc/fr_FR/link-labels.markdown create mode 100644 doc/fr_FR/notifications.markdown create mode 100644 doc/fr_FR/project-configuration.markdown create mode 100644 doc/fr_FR/project-permissions.markdown create mode 100644 doc/fr_FR/project-types.markdown create mode 100644 doc/fr_FR/project-views.markdown create mode 100644 doc/fr_FR/recurring-tasks.markdown create mode 100644 doc/fr_FR/roles.markdown create mode 100644 doc/fr_FR/screenshots.markdown create mode 100644 doc/fr_FR/screenshots/automatic-action-creation.png create mode 100644 doc/fr_FR/screenshots/board-collapsed-mode.png create mode 100644 doc/fr_FR/screenshots/board-compact-mode.png create mode 100644 doc/fr_FR/screenshots/board-expanded-mode.png create mode 100644 doc/fr_FR/screenshots/board-task-limit.png create mode 100644 doc/fr_FR/screenshots/board-view.png create mode 100644 doc/fr_FR/screenshots/calendar-view.png create mode 100644 doc/fr_FR/screenshots/gantt-view.png create mode 100644 doc/fr_FR/screenshots/hide-column.png create mode 100644 doc/fr_FR/screenshots/list-view.png create mode 100644 doc/fr_FR/screenshots/new-project.png create mode 100644 doc/fr_FR/screenshots/new-user.png create mode 100644 doc/fr_FR/screenshots/project-disable-sharing.png create mode 100644 doc/fr_FR/screenshots/project-edition.png create mode 100644 doc/fr_FR/screenshots/project-enable-sharing.png create mode 100644 doc/fr_FR/screenshots/project-permissions.png create mode 100644 doc/fr_FR/screenshots/project-view.png create mode 100644 doc/fr_FR/screenshots/show-column.png create mode 100644 doc/fr_FR/screenshots/swimlane-configuration.png create mode 100644 doc/fr_FR/screenshots/swimlanes.png create mode 100644 doc/fr_FR/sharing-projects.markdown create mode 100644 doc/fr_FR/subtasks.markdown create mode 100644 doc/fr_FR/swimlanes.markdown create mode 100644 doc/fr_FR/task-links.markdown create mode 100644 doc/fr_FR/time-tracking.markdown create mode 100644 doc/fr_FR/transitions.markdown create mode 100644 doc/fr_FR/usage-examples.markdown create mode 100644 doc/fr_FR/user-management.markdown create mode 100644 doc/fr_FR/what-is-kanban.markdown (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index ebd6e38e..20ffbca1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +Version 1.0.31 (unreleased) +-------------- + +Improvements: + +* Make embedded documentation available in multiple languages + Version 1.0.30 -------------- diff --git a/app/Controller/DocumentationController.php b/app/Controller/DocumentationController.php index d86fb3c8..0d02ebda 100644 --- a/app/Controller/DocumentationController.php +++ b/app/Controller/DocumentationController.php @@ -20,16 +20,7 @@ class DocumentationController extends BaseController $page = 'index'; } - if ($this->languageModel->getCurrentLanguage() === 'fr_FR') { - $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown'; - } else { - $filename = __DIR__ . '/../../doc/' . $page . '.markdown'; - } - - if (!file_exists($filename)) { - $filename = __DIR__.'/../../doc/index.markdown'; - } - + $filename = $this->getPageFilename($page); $this->response->html($this->helper->layout->app('doc/show', $this->render($filename))); } @@ -83,10 +74,63 @@ class DocumentationController extends BaseController */ public function replaceImageUrl(array $matches) { - if ($this->languageModel->getCurrentLanguage() === 'fr_FR') { - return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')'; + return '('.$this->getFileBaseUrl($matches[1]).')'; + } + + /** + * Get Markdown file according to the current language + * + * @access private + * @param string $page + * @return string + */ + private function getPageFilename($page) + { + return $this->getFileLocation($page . '.markdown') ?: + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', 'index.markdown')); + } + + /** + * Get base URL for Markdown links + * + * @access private + * @param string $filename + * @return string + */ + private function getFileBaseUrl($filename) + { + $language = $this->languageModel->getCurrentLanguage(); + $path = $this->getFileLocation($filename); + + if (strpos($path, $language) !== false) { + $url = implode('/', array('doc', $language, $filename)); + } else { + $url = implode('/', array('doc', $filename)); + } + + return $this->helper->url->base().$url; + } + + /** + * Get file location according to the current language + * + * @access private + * @param string $filename + * @return string + */ + private function getFileLocation($filename) + { + $files = array( + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', $this->languageModel->getCurrentLanguage(), $filename)), + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', $filename)), + ); + + foreach ($files as $filename) { + if (file_exists($filename)) { + return $filename; + } } - return '('.$this->helper->url->base().'doc/'.$matches[1].')'; + return ''; } } diff --git a/doc/es_ES/kanban-vs-todo-and-scrum.markdown b/doc/es_ES/kanban-vs-todo-and-scrum.markdown index ad9dd1a9..6e8d9e6c 100644 --- a/doc/es_ES/kanban-vs-todo-and-scrum.markdown +++ b/doc/es_ES/kanban-vs-todo-and-scrum.markdown @@ -6,13 +6,13 @@ Kanban vs Todo lists ### Todo lists (lista de tareas) : -Fase unica (es solo una lista de tareas) -Multitarea posible (no eficiente) +- Fase unica (es solo una lista de tareas) +- Multitarea posible (no eficiente) ### Kanban: -Multi fases, -Concentración absoluta para evitar multitareas por que se puede establecer un limite por columna para mejorar el progreso +- Multi fases, +- Concentración absoluta para evitar multitareas por que se puede establecer un limite por columna para mejorar el progreso Kanban vs Scrum @@ -20,13 +20,13 @@ Kanban vs Scrum ### Scrum: -Los sprints son time-boxed, usualmente 2 o 4 semanas -No permitir cambios durante la iteración -La estimación es requerida -Utiliza la velocidad como métrica predeterminada -El tablero de Scrum se borra entre cada sprint -Scrum tiene funciones predefinidas como scrum master , los dueños del producto y el equipo -Una gran cantidad de reuniones: planeaciones, backlogs grooming, daily stand-up, retrospectiva +- Los sprints son time-boxed, usualmente 2 o 4 semanas +- No permitir cambios durante la iteración +- La estimación es requerida +- Utiliza la velocidad como métrica predeterminada +- El tablero de Scrum se borra entre cada sprint +- Scrum tiene funciones predefinidas como scrum master , los dueños del producto y el equipo +- Una gran cantidad de reuniones: planeaciones, backlogs grooming, daily stand-up, retrospectiva ### Kanban: diff --git a/doc/fr/2fa.markdown b/doc/fr/2fa.markdown deleted file mode 100644 index 2ecaa10b..00000000 --- a/doc/fr/2fa.markdown +++ /dev/null @@ -1,33 +0,0 @@ -Authentification à deux facteurs -========================= - -Chaque utilisateur peut activer [l'authentification à deux facteurs](http://en.wikipedia.org/wiki/Two_factor_authentication). -Après s’être connecté, un code à usage unique (6 caractères) est demandé à l'utilisateur pour lui autoriser l’accès à Kanboard. - -Ce code doit être fourni par un logiciel compatible, généralement installé sur votre smartphone. - -Kanboard utilise le [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) défini dans la [RFC 6238](http://tools.ietf.org/html/rfc6238). - -Il existe de nombreux logiciels compatibles avec le standard TOTP system. -Par exemple, vous pouvez utilisez ces applications libres et open source : - -- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry) -- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS) -- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (utilitaire en ligne de commande sur Unix/Linux) - -Ce système peut fonctionner hors ligne et vous n'avez pas l'obligation d'avoir un téléphone portable. - -Paramétrage ------ - -1. Allez dans le profil utilisateur. -2. Sur la gauche, cliquez sur **Authentification à deux facteurs** et cochez la case. -3. Une clef secrète est générée pour vous. - -![2FA](https://kanboard.net/screenshots/documentation/2fa.png) - -- Vous devez sauvegarder votre clef dans votre logiciel TOTP. Si vous utilisez un smartphone, la solution la plus simple est de scanner le QR code avec FreeOTP ou Google Authenticator -- À chaque ouverture de session, un nouveau code sera demandé -- N'oubliez pas de tester votre appareil avant de quitter votre session - -Une nouvelle clef est générée à chaque fois que vous activez/désactivez cette fonction diff --git a/doc/fr/analytics-tasks.markdown b/doc/fr/analytics-tasks.markdown deleted file mode 100644 index 0eb89e34..00000000 --- a/doc/fr/analytics-tasks.markdown +++ /dev/null @@ -1,24 +0,0 @@ -Analytique des tâches -=================== - -Chaque tâche possède une section analytique accessible à partir du menu à gauche dans la page des tâches - -Lead et cycle time -------------------- - -![Lead and cycle time](https://kanboard.net/screenshots/documentation/task-lead-cycle-time.png) - -- Le lead time est la durée entre la création de la tâche et son achèvement (tâche fermée). -- Le cycle time est la durée entre la date de début et l'achèvement. -- Si la tâche n’est pas fermée, l’heure courante est utilisée à la place de la date d'achèvement. -- Si la date de départ n'est pas spécifiée, le cycle time n'est pas calculé. - -Remarque : vous pouvez configurer une action pour définir automatiquement que la date de départ sera le moment où vous déplacez une tâche vers une colonne de votre choix - -Temps passé dans chaque colonne ---------------------------- - -![Temps passé dans chaque colonne](https://kanboard.net/screenshots/documentation/time-into-each-column.png) - -- Ce graphique montre le temps total passé dans chaque colonne pour la tâche -- Le temps passé est calculé jusqu’à ce que la tâche soit fermée diff --git a/doc/fr/analytics.markdown b/doc/fr/analytics.markdown deleted file mode 100644 index 0b94f272..00000000 --- a/doc/fr/analytics.markdown +++ /dev/null @@ -1,70 +0,0 @@ -Analytique -========= - -Chaque projet dispose d'une section analytique. En fonction de la façon dont vous utilisez Kanboard, vous pourrez voir les rapports suivants : - -Répartition des utilisateurs ----------------- - -![Répartition des utilisateurs](https://kanboard.net/screenshots/documentation/user-repartition.png) - -Ce graphique circulaire affiche le nombre de tâches assignées par utilisateur. - -Distribution des tâches ------------------ - -![Distribution des tâches](https://kanboard.net/screenshots/documentation/task-distribution.png) - -Ce graphique circulaire donne une vue d'ensemble du nombre de tâches ouvertes par colonne. - -Diagramme de flux cumulé ------------------------ - -![Diagramme de flux cumulé](https://kanboard.net/screenshots/documentation/cfd.png) - -- Ce graphique affiche le nombre de tâches de façon cumulée pour chaque colonne en fonction du temps passé. -- Chaque jour, le nombre total de tâches est enregistré pour chaque colonne. -- Si vous souhaitez exclure les tâches terminées, modifiez les [paramètres du projet global](project-configuration.markdown). - -Remarque : il faut au moins deux jours de données pour que le graphique apparaisse. - -Graphique d'avancement --------------- - -![Graphique d'avancement](https://kanboard.net/screenshots/documentation/burndown-chart.png) - -Un [graphique d'avancement](http://en.wikipedia.org/wiki/Burn_down_chart) est disponible pour chaque projet. - -- Il s'agit de la représentation graphique du travail qui reste à faire en fonction du temps restant. -- Kanboard utilise la complexité des estimations d'achèvement pour créer le graphique. -- Chaque jour, la somme des estimations pour chaque colonne est calculée. - -Temps moyen passé pour chaque colonne ------------------------------------ - -![Temps moyen passé pour chaque colonne](https://kanboard.net/screenshots/documentation/average-time-spent-into-each-column.png) - -Ce graphique affiche le temps moyen passé pour chaque colonne pour les 1000 dernière tâches. - -- Kanboard utilise les transitions entre tâches pour calculer les données. -- Le temps passé est calculé jusqu'à la fin de la tâche. - -Temps moyen de Lead et Cycle ---------------------------- - -![Temps moyen passé pour chaque colonne](https://kanboard.net/screenshots/documentation/average-lead-cycle-time.png) - -Ce graphique affiche le temps moyen de lead et cycle pour les 1000 dernières tâches au cours du temps. - -- Le *lead time* est le temps passé entre la création de la tâche et sa date d'achèvement. -- Le *cycle time* est le temps passé entre la date de début spécifiée et la date d'achèvement de la tâche. -- Si la tâche n'est pas close, la date courante est utilisée à la place de la date d'achèvement. - -Ces métriques sont calculées et enregistrées chaque jour pour l'ensemble du projet. - -N'oubliez pas de lancer chaque jour le calcul statistique -------------------------------------------------------- - -Pour générer des données analytique précises, vous devriez lancer chaque jour le cronjob **statistiques quotidiennes du projet**. - -[Consultez la documentation sur la ligne de commande avec Kanboard](cli.markdown) diff --git a/doc/fr/application-configuration.markdown b/doc/fr/application-configuration.markdown deleted file mode 100644 index 12768f03..00000000 --- a/doc/fr/application-configuration.markdown +++ /dev/null @@ -1,41 +0,0 @@ -Paramètres de l'application -==================== - -Certains paramètres de l'application peuvent être modifiés sur la page des paramètres. -Seuls les administrateurs peuvent modifier ces paramètres. - -Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche. - -![Paramètres de l'application](https://kanboard.net/screenshots/documentation/application-settings.png) - -### URL de l'application - -Ce paramètre est utilisé pour les notifications par mail. -Le pied de page du mail contiendra un lien vers la tâche du Kanboard. - -### Langue - -La langue de l'application peut être modifiée à tout moment. -Elle sera définie pour tous les utilisateurs. - -### Fuseau horaire - -Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire. -La liste contient tous les fuseaux horaires pris en charge par votre serveur web. - -### Format de date - -Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches. - -Kanboard propose 4 différents formats: - -- JJ/MM/AAAA -- MM/JJ/AAAA (par défaut) -- AAAA/MM/JJ -- MM.JJ.AAAA - -Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ). - -### Feuille de style personnalisée - -Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard. diff --git a/doc/fr/application-configuration.markup b/doc/fr/application-configuration.markup deleted file mode 100644 index 12768f03..00000000 --- a/doc/fr/application-configuration.markup +++ /dev/null @@ -1,41 +0,0 @@ -Paramètres de l'application -==================== - -Certains paramètres de l'application peuvent être modifiés sur la page des paramètres. -Seuls les administrateurs peuvent modifier ces paramètres. - -Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche. - -![Paramètres de l'application](https://kanboard.net/screenshots/documentation/application-settings.png) - -### URL de l'application - -Ce paramètre est utilisé pour les notifications par mail. -Le pied de page du mail contiendra un lien vers la tâche du Kanboard. - -### Langue - -La langue de l'application peut être modifiée à tout moment. -Elle sera définie pour tous les utilisateurs. - -### Fuseau horaire - -Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire. -La liste contient tous les fuseaux horaires pris en charge par votre serveur web. - -### Format de date - -Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches. - -Kanboard propose 4 différents formats: - -- JJ/MM/AAAA -- MM/JJ/AAAA (par défaut) -- AAAA/MM/JJ -- MM.JJ.AAAA - -Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ). - -### Feuille de style personnalisée - -Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard. diff --git a/doc/fr/automatic-actions.markdown b/doc/fr/automatic-actions.markdown deleted file mode 100644 index f136b98c..00000000 --- a/doc/fr/automatic-actions.markdown +++ /dev/null @@ -1,133 +0,0 @@ -Actions automatiques -==================== - -Pour réduire au minimum l'interaction avec les utilisateurs, Kanboard dispose d'actions automatiques. - -Chaque action automatique est définie ainsi : - -- Un événement à suivre -- Une action associée à cet évènement -- Éventuellement quelques paramètres à définir - -Chaque projet a une série d'actions automatisées qui lui sont propres, le panneau de configuration est situé sur la page qui liste les projets, il vous suffit de cliquer sur le lien **Actions automatiques**. - -Ajouter une nouvelle action ---------------------------- - -Cliquez sur le lien **Ajouter une nouvelle action**. - -![Action automatique](screenshots/automatic-action-creation.png) - -- Commencez par choisir une action -- Ensuite, sélectionnez un évènement -- Et pour finir, les paramètres de l'action - -Liste des évènements disponibles --------------------------------- - -- Déplacement d'une tâche vers une autre colonne -- Déplacement d'une tâche à un autre emplacement de la même colonne -- Modification d'une tâche -- Création d'une tâche -- Réouverture d'une tâche -- Fermeture d'une tâche -- Création ou modification d'une tâche -- Changement d'assigné à une tâche -- Création ou mise à jour du lien vers une tâche -- Réception d'un *commit* de Github -- Ouverture d'une *issue* de Github -- Fermeture d'une *issue* de Github -- Réouverture d'une *issue* de Github -- Modification de l'assigné à une *issue* de Github -- Modification de l'étiquette d'une *issue* de Github -- Création d'un commentaire d'une *issue* de Github -- Ouverture d'une *issue* de Gitlab -- Fermeture d'une *issue* de Gitlab -- Réception d'un *commit* de Gitlab -- Réception d'un *commit* de Bitbucket -- Ouverture d'une *issue* de Bitbucket -- Fermeture d'une *issue* de Bitbucket -- Réouverture d'une *issue* de Bitbucket -- Modification de l'assigné à une *issue* de Bitbucket issue assignee change -- Création d'un commentaire d'une *issue* de Bitbucket - -Liste des actions disponibles ------------------------------ - -- Fermer une tâche -- Ouvrir une tâche -- Assigner la tâche à un utilisateur particulier -- Assigner la tâche à la personne qui fait l'action -- Cloner la tâche depuis un autre projet -- Déplacer la tâche vers un autre projet -- Déplacer la tâche vers une autre colonne quand elle est assignée à un utilisateur -- Déplacer la tâche vers une autre colonne quand quand l'assigné est supprimé -- Assigner une couleur quand la tâche est déplacée vers une colonne particulière -- Assigner une couleur à un utilisateur particulier -- Assigner automatiquement une couleur selon la catégorie -- Assigner automatiquement une catégorie en fonction d'une couleur -- Créer un commentaire depuis un fournisseur externe -- Créer une tâche depuis un fournisseur externe -- Ajouter un journal de commentaires quand on change une tâche de colonne -- Modifier l'assigné en fonction d'un nom d'utilisateur externe -- Modifier la catégorie en fonction d'une étiquette externe -- Mettre à jour automatiquement la date de début -- Déplacer la tâche vers une autre colonne quand la catégorie a changé -- Envoyer une tâche par mail à quelqu'un -- Modifier la couleur de la tâche quand on utilise un lien particulier pour cette tâche - -Exemples --------- -Voici quelques exemples d'utilisation dans la vraie vie : - -### Quand je déplace une tâche vers la colonne "Terminer", fermer automatiquement cette tâche - -- Choisir l'action : **Fermer la tâche** -- Choisir l'évènement : **Déplacement d'une tâche vers une autre colonne** -- Définir le paramètre de l'action : **Colonne = Terminé** (c'est la colonne de destination) - -### Quand je déplace une tâche vers la colonne "À valider", assigner cette tâche à un utilisateur particulier - -- Choisir l'action : **Assigner la tâche à un utilisateur particulier** -- Choisir l'évènement : **Déplacer une tâche vers une nouvelle colonne** -- Définir les paramètres de l'action :**Colonne = À valider** et **Utilisateur = Adrien** (Adrien est par exemple un testeur) - -### Quand je déplace une tâche vers la colonne "Travail en cours", assigner cette tâche à l'utilisateur courant - -- Choisir l'action : **Assigner la tâche à la personne qui fait cette action** -- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** -- Définir le paramètre de l'action : **Colonne = Travail en cours** - -### Quand une tâche est terminée, dupliquer cette tâche vers un autre projet - -Supposons que nous ayons deux projets : "Commande du client" et "Production". Une fois validée la commande, la basculer vers le projet "Production". - -- Choisir l'action : **Dupliquer la tâche vers un autre projet** -- Choisir l'évènement : **Fermer une tâche** -- Définir les paramètres de l'action : **Colonne = Validé** et **Projet = Production** - -### Quand une tâche est déplacée vers la toute dernière colonne, déplacer la même tâche exactement vers un autre projet - -Supposons que nous ayons deux projets : "Idées" et "Développement". Une fois validée l'idée, la basculer vers le projet "Développement". - -- Choisir l'action : **Déplacer la tâche vers un autre projet** -- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** -- Définir les paramètres de l'action : **Colonne = Validé** et **Projet = Développement** - -### Je veux assigner automatiquement une couleur à l'utilisateur Adrien - -- Choisir l'action : **Assigner une couleur à un utilisateur particulier** -- Choisir l'évènement : **Modification de l'assigné à une tâche** -- Définir les paramètres de l'action :**Couleur = Vert** et **Assigné = Adrien** - -### Je veux assigner automatiquement une couleur à la catégorie "Demande de fonctionnalité" - -- Choisir l'action : **Assigner automatiquement une couleur à une catégorie particulière** -- Choisir l'évènement : **Création ou modification d'une tâche** -- Définir les paramètres de l'action : **Couleur = Bleu** et **Catégorie = Demande de fonctionnalité** - -### Je veux régler automatiquement la date de début quand la tâche est déplacée dans la colonne "Travail en cours" - -- Choisir l'action : **Mettre à jour automatiquement la date de début** -- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** -- Définir les paramètres de l'action : **Colonne= Travail en cours** diff --git a/doc/fr/board-collapsed-expanded.markdown b/doc/fr/board-collapsed-expanded.markdown deleted file mode 100644 index 29396772..00000000 --- a/doc/fr/board-collapsed-expanded.markdown +++ /dev/null @@ -1,18 +0,0 @@ -Mode replié et déplié -===================== - -Les tâches peuvent être affichées sur le tableau en mode replié ou déplié. -Basculer d'un mode à l'autre peut être fait à l'aide du raccourci clavier **« s »** ou en utilisant le menu déroulant sur la gauche. - -Mode replié ------------ - -![Tâches repliées](screenshots/board-collapsed-mode.png) - -- Si la tâche est affectée à quelqu'un, les initiales de la personne sont affichées à côté du numéro de la tâche. -- Si le titre de la tâche est trop long, mettez le curseur de la souris au-dessus de la tâche pour voir une boite flottante avec le titre entier. - -Mode déplié ------------ - -![Tâches dépliées](screenshots/board-expanded-mode.png) diff --git a/doc/fr/board-configuration.markdown b/doc/fr/board-configuration.markdown deleted file mode 100644 index f7f8be33..00000000 --- a/doc/fr/board-configuration.markdown +++ /dev/null @@ -1,24 +0,0 @@ -Paramètres du tableau -============== - -Allez dans le menu **Paramètres** puis choisissez *Paramètres du tableau** sur la gauche - -![Paramètres du tableau](https://kanboard.net/screenshots/documentation/board-settings.png) - -### Mise en avant d'une tâche - -Cette fonctionnalité affiche une ombre autour de la tâche lorsqu'une tâche à été déplacée récemment. - -Initialisez la fonctionnalité à 0 pour la désactiver, par défaut 2 jours (172800 secondes). - -Toutes les tâches qui ont été déplacées depuis 2 jours seront entourées d'une ombre. - -### Intervalle pour rafraîchir un tableau public - - Lorsque vous partagez un tableau, la page sera, par défaut, automatiquement rafraîchie toutes les 60 secondes. - -### Intervalle pour rafraîchir un tableau privé - - Lorsque votre navigateur web est ouvert sur un tableau, Kanboard vérifie toutes les 10 secondes si quelque chose à été modifié par un autre utilisateur. - - Techniquement, ce processus est fait par Ajax polling. diff --git a/doc/fr/board-horizontal-scrolling-and-compact-view.markdown b/doc/fr/board-horizontal-scrolling-and-compact-view.markdown deleted file mode 100644 index 7ad9c23c..00000000 --- a/doc/fr/board-horizontal-scrolling-and-compact-view.markdown +++ /dev/null @@ -1,11 +0,0 @@ -Défilement horizontal et mode compact -===================================== - -Lorsque le tableau ne loge pas dans votre écran, une barre de défilement horizontal appaîtra en bas de l'écran. - -Cependant, il est possible de basculer vers la vue compacte pour afficher toutes les colonnes dans votre écran. - -![Tableau en mode compact](screenshots/board-compact-mode.png) - -Basculer entre le défilement horizontal et la vue compacte s'effectue avec le raccourci clavier **« c »** ou en utilisant le menu déroulant sur la gauche. - diff --git a/doc/fr/board-show-hide-columns.markdown b/doc/fr/board-show-hide-columns.markdown deleted file mode 100644 index 8eac0b2c..00000000 --- a/doc/fr/board-show-hide-columns.markdown +++ /dev/null @@ -1,12 +0,0 @@ -Afficher ou cacher des colonnes dans le tableau -=============================================== - -Vous pouvez très facilement cacher ou afficher des colonnes dans le tableau : - -![Cacher une colonne](screenshots/hide-column.png) - -Pour cacher une colonne, ouvrez le menu déroulant de la colonne. - -![Afficher une colonne](screenshots/show-column.png) - -Pour afficher de nouveau la colonne, cliquez sur l'icône avec le « plus ». diff --git a/doc/fr/calendar-configuration.markdown b/doc/fr/calendar-configuration.markdown deleted file mode 100644 index 6494568a..00000000 --- a/doc/fr/calendar-configuration.markdown +++ /dev/null @@ -1,43 +0,0 @@ -Paramètres du calendrier -================= - -Allez au menu **Paramètres**, puis choisissez **Paramètres du calendrier** sur la gauche. - -![Paramètres du calendrier](https://kanboard.net/screenshots/documentation/calendar-settings.png) - -il existe deux calendriers distincts dans Kanboard : - -- le calendrier du projet -- le calendrier de l'utilisateur, disponible dans le tableau de bord - -Le calendrier du projet ----------------- - -Ce calendrier affiche les tâches avec les dates d'échéance et les tâches selon leur date de création ou de début. - -### Afficher les tâches selon leur date de création - -- La date de début d'un évènement du calendrier est la date de création de la tâche. -- la date de fin de l'évènement est la date d'achèvement de la tâche. - -### Afficher les tâches selon leur date de début - -- La date de début d'un évènement du calendrier est la date du démarrage effectif de la tâche. -- Cette date ne peut pas être définie manuellement. -- La date de fin de l'évènement est la date de l'achèvement de la tâche. -- S'il n'existe pas de date de début la tâche ne figurera pas sur le calendrier . - -Calendrier de l'utilisateur -------------- - -Ce calendrier n'affiche que les tâches assignées à l'utilisateur et de façon facultative des informations sur les sous-tâches. - -### Afficher les sous-tâches selon le suivi du temps passé - -- Affiche les sous-tâches dans le calendrier d'après les informations recueillies dans l afeuille de suivi du temps. -- Le croisement des données avec l'emploi du temps de l'utilisateur est également calculé. - -### Afficher les estimations des sous-tâches (anticipation sur le travail à venir) - -- Affiche l'estimation du travail à venir pour les sous-tâches qui ont le statut « à faire » et avec une valeur définie à « estimé ». - diff --git a/doc/fr/calendar.markdown b/doc/fr/calendar.markdown deleted file mode 100644 index 2ceeeaa4..00000000 --- a/doc/fr/calendar.markdown +++ /dev/null @@ -1,20 +0,0 @@ -Calendriers -======== - -il existe deux visualisations différentes des calendriers : - -- La vue du projet avec des filtres (disponibles depuis le tableau) -- La vue utilisateur (disponible depuis le tableau de bord de l'utilisateur) - -Pour l'instant le calendrier permet d'afficher les informations suivantes : - -- Les tâches avec une date d'échéance, affichée en haut. **La date d'échéance peut être modifiée en déplaçant la tâche vers un autre jour**. -- les tâches basées sur la date de création ou la date de début. **Ces évènements ne peuvent pas être modifiés avec le calendrier**. -- Le suivi dans le temps de sous-tâches, tous les segments temporels sont affichés dans le calendrier. -- Les estimations pour les sous-tâches, les prévisions et le travail restant - -![Calendrier](https://kanboard.net/screenshots/documentation/calendar.png) - -La configuration du calendrier peut être modifiée dans la page des paramètres. - -Remarque : la date d'échéance n'inclut pas d'information temporelle. diff --git a/doc/fr/closing-tasks.markdown b/doc/fr/closing-tasks.markdown deleted file mode 100644 index 022a1dfd..00000000 --- a/doc/fr/closing-tasks.markdown +++ /dev/null @@ -1,16 +0,0 @@ -Fermer des tâches -============= - -Quand une tâche est fermée, elle n'est plus visible sur le tableau. - -Toutefois, vous pouvez toujours accéder à la liste des tâches closes en utilisant la requête **status:closed** dans un formulaire de recherche, ou bien choisissez simplement **Tâches fermées** dans le menu déroulant des filtres. - -Il existe deux façons différentes de fermer une tâche, depuis le menu déroulant des tâches sur le tableau : - -![Fermer une tâche par le menu déroulant](https://kanboard.net/screenshots/documentation/menu-close-task.png) - -…ou bien depuis la barre latérale dans la vue détaillée des tâches - -![Fermer une tâche](https://kanboard.net/screenshots/documentation/closing-tasks.png) - -Remarque : quand vous fermez une tâche, toutes les sous-tâches qui ne sont pas achevées verront leur statut passer à "Terminé". diff --git a/doc/fr/create-tasks-by-email.markdown b/doc/fr/create-tasks-by-email.markdown deleted file mode 100644 index dd06a1c4..00000000 --- a/doc/fr/create-tasks-by-email.markdown +++ /dev/null @@ -1,45 +0,0 @@ -Créer des tâches par email -===================== - -Vous pouvez créer des tâches directement en envoyant un message. - -Pour le moment, Kanboard fonctionne avec 3 services externes : - -- [Mailgun](https://kanboard.net/documentation/mailgun) -- [Sendgrid](https://kanboard.net/documentation/sendgrid) -- [Postmark](https://kanboard.net/documentation/postmark) - -Ces services gèrent le courrier entrant sans qu'on ait à configurer un serveur SMTP. - -À la réception d'un email par l'un de ces services, le message qu'il contenait est transmis et traité automatiquement par Kanboard. -Toutes les opérations complexes sont prises en charge par ces services. - -Processus de réception du courrier entrant ------------------------- - -1. Vous envoyez un mail à une adresse spécifique, par exemple **quelquechose+monprojet@inbound.mondomaine.tld** -2. Votre mail est envoyé sur les serveurs tiers SMTP -3. Le fournisseur de SMTP appelle Kanboard via un webhook avec le mail en JSON ou aux formats multipart/form-data -4. Kanboard analyse le mail reçu et crée la tâche dans le bon projet - -Remarque : les nouvelles tâches sont automatiquement créées dans la première colonne. - -Format du mail ------------- - -- La partie locale de l'adresse mail doit utiliser le signe + comme séparateur, par exemple **kanboard+projet123** -- La chaîne de caractères définie après le signe + doit correspondre à l'identifiant d'un projet, par exemple **projet123** est l'identifiant du projet **Projet 123** -- le sujet de l'email devient le titre de la tâche -- Le corps du message devient la description de la tâche (au format Markdown) - -Les courriers entrants peuvent être écrits aux formats .txt ou .HTML. -**Kanboard peut convertir en Markdown les messages écrits en simple HTML**. - -Sécurité et prérequis -------------------------- - -- Le webhook de Kanboard est protégé par un jeton aléatoire -- L'adresse de l'expéditeur doit correspondre à celle d'un utilisateur de Kanboard -- L'utilisateur de Kanboard doit être un membre du projet -- Le projet Kanboard doit avoir un identifiant unique, par exemple **MONPROJET** - diff --git a/doc/fr/creating-projects.markdown b/doc/fr/creating-projects.markdown deleted file mode 100644 index e5da7cc6..00000000 --- a/doc/fr/creating-projects.markdown +++ /dev/null @@ -1,39 +0,0 @@ -Créer des projets -================= - -Kanboard peut gérer de multiples projets. Voici deux sortes de projets : - -- Les projets multi-utilisateurs (pour le travail collaboratif, en équipe) -- Les projets privés, réservés à un seul utilisateur - -Créer des projets multi-utilisateurs ------------------------------------- - -- Seuls les administrateurs et les gestionnaires de projets peuvent créer ce type de projets -- La gestion des utilisateurs est disponible - -Depuis le tableau de bord, cliquez sur le lien **Nouveau projet** : - -![Formulaire de création de projet](screenshots/new-project.png) - -C'est vraiment très simple, il vous suffit de trouver un nom pour votre projet ! - -Créer un projet privé ---------------------- - -- Tout le monde peut créer un projet privé (sauf si désactivé par l'administrateur) -- Il n'y a **pas** de gestion des utilisateurs -- Seuls le propriétaire et les administrateurs peuvent accéder au projet - -Depuis le tableau principal, cliquez sur le lien **Nouveau projet privé**. - -Créer un projet depuis un autre projet --------------------------------------- - -Lorsque vous créez un nouveau projet, vous pouvez choisir de dupliquer les propriétés d'un projet existant : - -- Permissions -- Catégories -- Actions -- Swimlanes -- Tâches diff --git a/doc/fr/creating-tasks.markdown b/doc/fr/creating-tasks.markdown deleted file mode 100644 index 9b7fa274..00000000 --- a/doc/fr/creating-tasks.markdown +++ /dev/null @@ -1,27 +0,0 @@ -Créer des tâches -============== - -Depuis le tableau, cliquez sur le signe plus + à côté du nom de la colonne : - -![Création de tâche à partir du tableau](https://kanboard.net/screenshots/documentation/task-creation-board.png) - -Le formulaire de création de tâche apparaît : - -![Formulaire de création de tâche](https://kanboard.net/screenshots/documentation/task-creation-form.png) - -Le seul champ obligatoire est le titre. - -Description des champs : - -- **Titre** : le titre de votre tâche, tel qu'il sera affiché sur le tableau. -- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](https://kanboard.net/documentation/syntax-guide). -- **Créer une autre tâche** : cochez cette case si vous souhaitez créer une tâche similaire (les champs seront pré-remplis). -- **Assigné** : la personne qui va travailler sur la tâche. -- **Catégorie** : une seule catégorie peut être assignée à une tâche. -- **Colonne** : la colonne dans laquelle la tâche sera créée. La tâche sera positionnée en bas de cette colonne. -- **Couleur** : Choisissez la couleur de la carte. -- **Complexité** : utilisée dans la gestion de projet agile (Scrum), la complexité des points d'étape est un nombre qui montre à l'équipe le degré de difficulté de l'avancement du projet. Les utilisateurs se servent souvent des suites de Fibonacci. -- **Estimation originale** : estimation du nombre d'heures nécessaire pour terminer les tâches. -- **Date d'échéance** : les tâches dont la date d'échéance est dépassée auront une date d'échéance en rouge et les dates suivantes seront en noir dans le tableau. Plusieurs formats de date sont acceptés, outre le sélecteur de date. - -Avec le lien d'aperçu (« Prévisualiser »), vous pouvez voir la description de la tâche convertie depuis la syntaxe Markdown. diff --git a/doc/fr/currency-rate.markdown b/doc/fr/currency-rate.markdown deleted file mode 100644 index e84acd31..00000000 --- a/doc/fr/currency-rate.markdown +++ /dev/null @@ -1,11 +0,0 @@ -Taux de change des devises -============== - -Chaque utilisateur peut avoir un taux horaire prédéfini dans différentes devises. -Si vous avez à manipuler plusieurs devises, vous pouvez définir ici le taux en fonction de la devise de référence. - -Cette fonctionnalité est utilisée pour calculer le budget du projet. - -![Currency Rate](https://kanboard.net/screenshots/documentation/currency-rate.png) - -Les paramètres pour le taux de change des devises sont situés dans **Paramètres > Taux de change** diff --git a/doc/fr/duplicate-move-tasks.markdown b/doc/fr/duplicate-move-tasks.markdown deleted file mode 100644 index 07c863d0..00000000 --- a/doc/fr/duplicate-move-tasks.markdown +++ /dev/null @@ -1,58 +0,0 @@ -Dupliquer et déplacer des tâches -======================== - -Dupliquer une tâche dans le même projet --------------------------------------- - -Allez à la vue par tâche et choisissez **Dupliquer** sur la gauche. - -![Duplication de tâche](https://kanboard.net/screenshots/documentation/task-duplication.png) - -Une nouvelle tâche sera créée avec les mêmes propriétés que celles de la tâche originale. - -Dupliquer une tâche vers un autre projet ------------------------------------ - -Allez à la vue par tâches et choisissez **Dupliquer dans un autre projet**. - -![Duplication d'une tâche dans un autre projet](https://kanboard.net/screenshots/documentation/task-duplication-another-project.png) - -Seuls les projets dont vous êtes membre apparaîtront dans le menu déroulant. - -Avant de copier les tâches, Kanboard vous demandera les propriétés de la destination qui ne sont pas communes entre les projets source et destination. - -Vous devez essentiellement définir : - -- La swimlane de destination -- La colonne -- La catégorie -- L'assigné - -Déplacer une tâche vers un autre projet ------------------------------- - -Allez à la vue par tâches et choisissez **Déplacer vers un autre projet**. - -Déplacer vers un autre projet est semblable à l'opération de duplication, vous devez choisir les nouvelles propriétés de la tâche. - -Liste des champs dupliqués -------------------------- -Voici la liste des champs dupliqués : - -- title -- description -- date_due -- color_id -- project_id -- column_id -- owner_id -- score -- category_id -- time_estimated -- swimlane_id -- recurrence_status -- recurrence_trigger -- recurrence_factor -- recurrence_timeframe -- recurrence_basedate - diff --git a/doc/fr/editing-projects.markdown b/doc/fr/editing-projects.markdown deleted file mode 100644 index 2186a1b9..00000000 --- a/doc/fr/editing-projects.markdown +++ /dev/null @@ -1,15 +0,0 @@ -Modifier des projets -==================== - -Les projets peuvent être renommés et désactivés à tout moment. - -Pour renommer un projet, il suffit de cliquer sur le lien « Modifier un projet » sur la gauche. - -![Modification de projet](screenshots/project-edition.png) - -- Les dates de début et de fin sont utilisées pour créer le diagramme de Gantt du projet -- La description est visible en infobulle sur le tableau et sur la page qui liste les projets -- Les administrateurs et administrateurs de projets peuvent convertir un projet privé en projet multi-utilisateur en décochant la case « Projet privé ». -- Vous pouvez également convertir un projet multi-utilisateur en projet privé. - -Remarque : quand vous rendez un projet privé, tous les utilisateurs existants auront accès au projet. Ajustez la liste des utilisateurs selon vos besoins. diff --git a/doc/fr/gantt-chart-projects.markdown b/doc/fr/gantt-chart-projects.markdown deleted file mode 100644 index 3801dc88..00000000 --- a/doc/fr/gantt-chart-projects.markdown +++ /dev/null @@ -1,17 +0,0 @@ -Diagramme de Gantt pour tous les projets -============================ - -Le but de ce diagramme de Gantt est d'afficher une vue d'ensemble de tous les projets basée sur les dates de début et de fin. - -- Ce diagramme de Gantt est disponible dans la section de gestion du projet -- Seuls les administrateurs et administrateurs de projet peuvent accéder à cette section -- Les administrateurs de projet ne verront que les projets dans lesquels il y a des membres -- Les objets privés ne sont pas affichés dans ce graphique - -![Diagramme de Gantt pour tous les projets](https://kanboard.net/screenshots/documentation/gantt-chart-all-projects.png) - -- La **date de début** et la **date de fin** des projets est utilisée pour construire le graphique -- Les barres horizontales peuvent être redimensionnées et déplacées latéralement avec votre souris -- Il n'y a pas de glisser-déposer vertical -- Les barres de projet sont affichées en noir quand il n'y a ni date de début ni date de fin définies -- L'infobulle affiche la liste des gestionnaires de projets et les membres ordinaires diff --git a/doc/fr/gantt-chart-tasks.markdown b/doc/fr/gantt-chart-tasks.markdown deleted file mode 100644 index fbd1b587..00000000 --- a/doc/fr/gantt-chart-tasks.markdown +++ /dev/null @@ -1,20 +0,0 @@ -Diagramme de Gantt pour les tâches -====================== - -Le but de ce diagramme de Gantt est de montrer une vue d'ensemble du temps utilisé en fonction de l'ensemble des tâches d'un projet donné. - -- Le diagramme de Gantt est disponible depuis le « sélecteur de vue » -- Seuls les gestionnaires de projet peuvent accéder à cette section - -![Gantt Chart](https://kanboard.net/screenshots/documentation/gantt-chart-project.png) - -- La **date de début** et la **date de fin** des tâches sont utilisées pour créer le graphique -- Les tâches peuvent être redimensionnées et déplacées horizontalement avec votre souris -- Il n'y a pas de glisser-déposer vertical -- La barre est de la même couleur que la tâche -- Chaque barre affiche un niveau de progression en pourcentage, qui est calculé en utilisant la position de la colonne dans le tableau -- Pour correspondre au modèle du Kanban, les tâches peuvent être ordonnées suivant leur position dans le tableau ou suivant les dates de début -- Les nouvelles tâches crées avec cette vue seront affichées sur le tableau en position 1 de la première colonne -- Les tâches sont affichées en noir quand il n'existe ni date de début ni date d'échéance définies - -![Tâche non définie](https://kanboard.net/screenshots/documentation/gantt-chart-not-defined.png) diff --git a/doc/fr/index.markdown b/doc/fr/index.markdown deleted file mode 100644 index f74c3fce..00000000 --- a/doc/fr/index.markdown +++ /dev/null @@ -1,63 +0,0 @@ -Documentation -============= - -Utiliser Kanboard ------------------ - -### Introduction - -- [Qu'est-ce que Kanban ?](what-is-kanban.markdown) -- [Comparons Kanban aux Todo listes et à Scrum](kanban-vs-todo-and-scrum.markdown) -- [Exemples d'utilisation](usage-examples.markdown) - -### Utiliser un tableau - -- [Vues Tableau, Agenda et Liste](project-views.markdown) -- [Mode Replié et Déplié](board-collapsed-expanded.markdown) -- [Défilement horizontal et mode compact](board-horizontal-scrolling-and-compact-view.markdown) -- [Afficher ou cacher des colonnes dans le tableau](board-show-hide-columns.markdown) - -### Travailler avec les projets - -- [Types de projets](project-types.markdown) -- [Créer des projets](creating-projects.markdown) -- [Modifier des projets](editing-projects.markdown) -- [Partager des tableaux et des tâches](sharing-projects.markdown) -- [Actions automatiques](automatic-actions.markdown) -- [Permissions des projets](project-permissions.markdown) -- [Swimlanes](swimlanes.markdown) -- [Calendriers](calendar.markdown) -- [Analytique](analytics.markdown) -- [Diagramme de Gantt pour les tâches](gantt-chart-tasks.markdown) -- [Diagramme de Gantt pour tous les projets](gantt-chart-projects.markdown) - -### Travailler avec les tâches - -- [Créer des tâches](creating-tasks.markdown) -- [Fermer des tâches](closing-tasks.markdown) -- [Dupliquer et déplacer des tâches](duplicate-move-tasks.markdown) -- [Ajouter des captures d'écran](screenshots.markdown) -- [Liens entre les tâches](task-links.markdown) -- [Transitions](transitions.markdown) -- [Suivi du temps](time-tracking.markdown) -- [Tâches récurrentes](recurring-tasks.markdown) -- [Créer des tâches par email](create-tasks-by-email.markdown) -- [Sous-tâches](subtasks.markdown) -- [Analytique des tâches](analytics-tasks.markdown) - -### Travailler avec les utilisateurs - -- [Rôles](roles.markdown) -- [Gestion des utilisateurs](user-management.markdown) -- [Notifications](notifications.markdown) -- [Authentification à deux facteurs](2fa.markdown) - -### Paramètres - -- [Raccourcis clavier](keyboard-shortcuts.markdown) -- [Paramètres de l'application](application-configuration.markdown) -- [Paramètres du projet](project-configuration.markdown) -- [Paramètres du tableau](board-configuration.markdown) -- [Paramètres du calendrier](calendar-configuration.markdown) -- [Paramètres du lien](link-labels.markdown) -- [Taux de change](currency-rate.markdown) diff --git a/doc/fr/kanban-vs-todo-and-scrum.markdown b/doc/fr/kanban-vs-todo-and-scrum.markdown deleted file mode 100644 index b6f5bc1f..00000000 --- a/doc/fr/kanban-vs-todo-and-scrum.markdown +++ /dev/null @@ -1,36 +0,0 @@ -Comparons Kanban aux Todo listes et à Scrum -============================== - -Kanban et les Todo listes --------------------- - -### Todo listes : - -- Une seule phase (une simple liste d'éléments) -- La possibilité de multitâche (moins efficace) - -### Kanban: - -- Multiples phases, chaque colonne représente une étape -- Permet de se concentrer sans se disperser sur de multiples tâches, puisque l'on peut poser une limite au travail en cours par colonne - -Kanban et Scrum ---------------- -### Scrum : - -- Limite les Sprints dans le temps, généralement à 2 ou 4 semaines -- N'accepte pas de modifications pendant l'itération -- Nécessite une estimation -- Utilise la vélocité comme métrique par défaut -- Le tableau Scrum est remis à zéro entre chaque Sprint -- Scrum a des rôles prédéfinis comme Scrum Master, Product Owner et l'équipe -- Beaucoup de réunions : planification, consolidation du backlog, quotidienne, rétrospective - -### Kanban : -- Flux continu -- Des modifications peuvent arriver à n'importe quel moment -- L'estimation est facultative -- Utilise le temps *lead* et *cycle* pour mesurer l'efficacité -- Le tableau Kanban est permanent -- Kanban n'impose aucune contrainte stricte ni de réunion, le processus est plus flexible - diff --git a/doc/fr/keyboard-shortcuts.markdown b/doc/fr/keyboard-shortcuts.markdown deleted file mode 100644 index 28a131d8..00000000 --- a/doc/fr/keyboard-shortcuts.markdown +++ /dev/null @@ -1,37 +0,0 @@ -Raccourcis clavier -================== - -La disponibilité des raccourcis clavier dépend de la page sur laquelle vous êtes couramment. - -Vues par projets (Tableau, Agenda, Liste, Gantt) --------------------------------------------- - -- Passer à la vue tableau = **v b** (appuyer sur **v** puis **b**) -- Passer à la vue agenda = **v c** -- Passer à la vue liste = **v l** -- Passer à la vue Gantt = **v g** - -Vue tableau ----------- - -- Nouvelle tâche = **n** -- Étendre / replier une tâche = **s** -- Vue compacte / vue étendue = **c** - -Vue détaillée d'une tâche -------------------------- - -- Modifier une tâche = **e** -- Nouvelle sous-tâche = **s** -- Nouveau commentaire = **c** -- Nouveau lien interne = **l** - -Application ------------ - -- Afficher la liste des raccourcis clavier = **?** -- Ouvrir le changement de tableau = **b** -- Aller au moteur de recherche = **f** -- Restaurer la boîte de recherche = **r** -- Fermer la fenêtre de dialogue = **ESC** -- Soumettre un formulaire = **CTRL+ENTER** ou **⌘+ENTER** diff --git a/doc/fr/link-labels.markdown b/doc/fr/link-labels.markdown deleted file mode 100644 index 9c266b5a..00000000 --- a/doc/fr/link-labels.markdown +++ /dev/null @@ -1,13 +0,0 @@ -Paramètres des liens -============= - -Les relations entre les tâches peuvent être modifiées depuis les paramètres de l'application (**Paramètres > Paramètres des liens**) - -![Libellé des liens](https://kanboard.net/screenshots/documentation/link-labels.png) - -Chaque nom du libellé peut avoir un nom du libellé opposé. - -Si il n'y a pas d'opposé, le nom du libellé sera considéré comme étant bidirectionnel. - -![Création d'un libellé de lien](https://kanboard.net/screenshots/documentation/link-label-creation.png) - diff --git a/doc/fr/notifications.markdown b/doc/fr/notifications.markdown deleted file mode 100644 index 43f34a8e..00000000 --- a/doc/fr/notifications.markdown +++ /dev/null @@ -1,45 +0,0 @@ -Notifications -============= - -Kanboard est capable d'envoyer des notifications via différents canaux : - -- Email -- Web (Liste de message non lus) - -Vous pouvez ajouter d'autres canaux en ajoutant des extensions comme par exemple Hipchat, Slack ou encore Jabber. - -Configuration --------------- - -Chaque utilisateur doit autoriser les notifications dans son profil : **Profil Utilisateur > Notifications**. Cette option est désactivée par défaut. - -Vous devez, bien sûr, avoir renseigné une adresse email valide dans votre profil et l'application doit être configurée pour envoyer des emails. - -![Notifications](https://kanboard.net/screenshots/documentation/notifications.png) - -Vous pouvez choisir votre méthode favorite de notification : - -- Email -- Web - -Pour chaque projet dont vous êtes membre, vous pouvez choisir de recevoir des notifications pour : - -- Toutes les tâches -- Seulement les tâches qui vous sont assignées -- Seulement les tâches que vous avez créées -- Seulement les tâches que vous avez créées et celles qui vous sont assignées - -Vous pouvez aussi sélectionner certain projets, par défaut tous les projets dont vous êtes membre sont sélectionnés. - -Notifications web ------------------ - -Les notifications web sont accessibles depuis le tableau de bord ou depuis l'icône en haut de la page : - -![Icône des notifications web](https://kanboard.net/screenshots/documentation/web-notifications-icon.png) - -Les notifications sont affichées sous forme de liste. Vous pouvez marquer comme lu chacune d'entre-elle ou toutes en même temps. - -![Notifications web](https://kanboard.net/screenshots/documentation/web-notifications.png) - -Avec cette méthode vous pouvez quand même rester avertis de ce que se passe sans pour autant être inondé d'emails. diff --git a/doc/fr/project-configuration.markdown b/doc/fr/project-configuration.markdown deleted file mode 100644 index 22db5bf1..00000000 --- a/doc/fr/project-configuration.markdown +++ /dev/null @@ -1,42 +0,0 @@ - -Paramètres du projet -================ - -Aller dans le menu **Préférences**; puis choisissez **Paramètres du projet** sur la gauche - -![Paramètres du projet](https://kanboard.net/screenshots/documentation/project-settings.png) - -###Colonnes par défaut pour les nouveaux projets - -Vous pouvez changer le nom des colonnes par défaut. -C'est utile si vous créez toujours des projets comprenant les même colonnes - -Chaque nom de colonne doit être séparé par une virgule. - -Par défaut, Kanboard utilise les noms de colonne suivants : en attente, prêt, en cours, terminé. - -###Catégories par défaut pour les nouveaux projets - -Les catégories ne sont pas globales à l'application mais rattachées à un projet. -Chaque projet peut avoir plusieurs catégories. - -De plus, si vous créez toujours la même catégorie pour tous vos projets, vous pouvez définir ici la liste des catégories à créer automatiquement - -### Autoriser une seule sous-tâche en cours à la fois pour un utilisateur - -Lorsque cette option est sélectionnée, un utilisateur ne peut travailler que sur une seule sous-tâche à la fois - -Si une autre sous-tâche possède le statut « en cours », l'utilisateur verra cette boite de dialogue : - -![Limite des sous-tâches pour l'utilisateur](https://kanboard.net/screenshots/documentation/subtask-user-restriction.png) - -### Déclencher automatiquement le suivi du temps pour les sous-tâches - -- Si activé, lorsque le statut d'une sous-tâche devient « en cours », le chrono va démarrer automatiquement -- Désactivez cette option si vous n'utilisez pas le suivi du temps. - -### Inclure les tâches fermées dans le diagramme de flux cumulé - -- Si l'option est activée, les tâches fermées seront incluses dans le diagramme de flux cumulé -- Si l'option est désactivée, seules les tâches ouvertes seront incluses dans le diagramme de flux cumulé -- Cette option affecte la colonne "total" de la table "project_daily_column_stats" diff --git a/doc/fr/project-permissions.markdown b/doc/fr/project-permissions.markdown deleted file mode 100644 index c4ef4df4..00000000 --- a/doc/fr/project-permissions.markdown +++ /dev/null @@ -1,22 +0,0 @@ -Permissions des projets -======================= - -Chaque projet est isolé des autres. -Les accès au projet doivent être autorisés par le chef de projet. - -Chaque utilisateur et chaque groupe peut avoir un rôle différent. -Il y a 3 types de [rôles pour les projets](roles.markdown) : - -- Chef de projet -- Membre du projet -- Visualiseur - -L'assignation des rôles est disponible depuis **Paramètres du projet > Permissions**: - -![Permissions du projet](screenshots/project-permissions.png) - -Si vous choisissez d'autoriser tout le monde, tous les utilisateurs de Kanboard seront considérés comme **Membre du projet**. -Ce qui signifie qu'il n'y a plus des gestion de rôles. -Les permissions par utilisateur ou par groupe ne peuvent plus être appliquées. - -Les projets privés ne peuvent pas définir de permissions. diff --git a/doc/fr/project-types.markdown b/doc/fr/project-types.markdown deleted file mode 100644 index 70434ec8..00000000 --- a/doc/fr/project-types.markdown +++ /dev/null @@ -1,14 +0,0 @@ -Types de projets -================ - -Il y a deux types de projets : - -| Type | Description | -|-------------------|-------------------------------------------------------------------------------------| -| Projet d'équipe | La gestion des utilisateurs est activée | -| Projet privé | Projet qui appartient à une seule personne, il n'y a pas de gestion d'utilisateurs | - -- Seulement les administrateurs et les gestionnaires peuvent créer des projets d'équipe. -- Les projets privés peuvent être créé par tout le monde. - -[Lire la documentation à propos des rôles dans Kanboard](roles.markdown) diff --git a/doc/fr/project-views.markdown b/doc/fr/project-views.markdown deleted file mode 100644 index 603108f6..00000000 --- a/doc/fr/project-views.markdown +++ /dev/null @@ -1,61 +0,0 @@ -Vues Tableau, Agenda et Liste -============================= - -Pour chaque projet, les tâches peuvent être visualisées dans différentes vues : **Tableau, Agenda, Liste ou Gantt**. -Chaque vue affiche le résultat filtré par le champ de recherche en haut de page. -Le moteur de recherche utilise la [syntaxe avancée](search.markdown). - -Vue Tableau ------------ - -![Vue Tableau](screenshots/board-view.png) - -- Dans cette vue, il est possible de glisser-déposer facilement des tâches d'une colonne à l'autre. -- Il est également possible d'utiliser le raccourci clavier **« v b »** pour afficher la vue Tableau. -- Les tâches avec une ombre ont été modifiées récemment. - -![Tableau Limite de tâches](screenshots/board-task-limit.png) - -Lorsque la limite de tâches est atteinte pour une colonne, l'arrière-plan devient rouge. -Ce qui signifie qu'il y a trop de tâches en cours en même temps. - -[En apprendre plus sur la configuration du Tableau](board-configuration.markdown) - -Vue Agenda ----------- - -![Vue Agenda](screenshots/calendar-view.png) - -- Dans cette vue, il est possible de voir les tâches avec des dates d'échéance. -- Selon les paramètres, il est également possible de voir les tâches en cours. -- Il est également possible d'utiliser le raccourci clavier **« v c »** pour afficher la vue Agenda. -- [En apprendre plus sur la configuration de l'Agenda](calendar-configuration.markdown) - -Vue Liste ---------- - -![Vue liste](screenshots/list-view.png) - -- Dans cette vue, tous les résultats de votre recherche sont affichés dans un tableau. -- Il est également possible d'utiliser le raccourci clavier **« v l »** pour afficher la vue Liste. - -Vue Gantt ---------- - -![Vue Gantt](screenshots/gantt-view.png) - -- La vue Gantt affiche les tâches dans une fresque horizontale -- Le diagramme utilise la date de début et la date d'échéance pour afficher les tâches -- Il est également possible d'utiliser le raccourci clavier **« v g »** pour afficher la vue Gantt. - -Aperçu du projet ----------------- - -![Aperçu du projet](screenshots/project-view.png) - -Ce mode permet d'afficher une vue d'ensemble du projet : - -- Vous pouvez voir la description du projet -- Attacher et visualiser des pièces-jointes au projet -- Visualiser la liste des membres -- Voir les dernières activités du projet diff --git a/doc/fr/recurring-tasks.markdown b/doc/fr/recurring-tasks.markdown deleted file mode 100644 index 95f24c40..00000000 --- a/doc/fr/recurring-tasks.markdown +++ /dev/null @@ -1,24 +0,0 @@ -Tâches récurrentes -=============== - -Pour convenir à ma méthodologie de Kanban, les tâches récurrentes ne sont pas basées sur une date mais sur les évènements du tableau. - -- Les tâches récurrentes sont dupliquées dans la première colonne du tableau quand les évènements sélectionnés se produisent -- La date d'échéance peut être automatiquement recalculée -- Chaque tâche enregistre l'identifiant de tâche de la tâche parente qui l'a créée et la tâche enfant qui a été créée. - -Configuration -------------- - -Allez à la page de vue par tâches ou utilisez le menu déroulant du tableau, puis choisissez **Modifier la récurrence**. - -![Tâche récurrente](https://kanboard.net/screenshots/documentation/recurring-tasks.png) - -il existe trois façons de déclencher la création d'une nouvelle tâche récurrente : - -- Déplacer une tâche depuis la première colonne -- Déplacer une tâche vers la dernière colonne -- Fermer la tâche - -Les dates d'échéance, si elles concernent la tâche courante, peuvent être recalculées en fonction d'un nombre donné de jours, mois ou années. -La date de base pour le calcul de la nouvelle date d'échéance peut être soit la date d'échéance existante, soit la date de l'action. diff --git a/doc/fr/roles.markdown b/doc/fr/roles.markdown deleted file mode 100644 index e55a3969..00000000 --- a/doc/fr/roles.markdown +++ /dev/null @@ -1,24 +0,0 @@ -Rôles des utilisateurs -====================== - -Rôles au niveau de l'application --------------------------------- - -Chaque utilisateur possède un de ces rôles : - -| Rôle | Description | -|----------------|----------------------------------------------------------------------------------------| -| Administrateur | Accès à tout | -| Gestionnaire | Peut créer des projets d'équipe mais ne peut pas changer les réglages de l'application | -| Utilisateur | Peut créer des projets privés | - -Rôles au niveau des projets ---------------------------- - -Chaque membre d'un projet peut avoir un rôle différent : - -| Rôle | Description | -|------------------------|----------------------------------------------------------------------| -| Chef de projet | Peut changer les paramètres du projet, accéder aux rapports | -| Membre du projet | Peut créer des tâches et utiliser le tableau Kanban | -| Visualiseur de projet | Accès en lecture seule au projet | diff --git a/doc/fr/screenshots.markdown b/doc/fr/screenshots.markdown deleted file mode 100644 index e634bd1b..00000000 --- a/doc/fr/screenshots.markdown +++ /dev/null @@ -1,26 +0,0 @@ -Ajouter des captures d'écran -================== - -Vous pouvez copier-coller des images directement dans Kanboard pour gagner du temps. -Ces images sont mises en ligne en tant que pièces jointes à une tâche. - -Ceci est particulièrement utile pour prendre des captures d'écran, quand il faut par exemple décrire un problème. - -Vous pouvez ajouter directement des captures depuis le tableau en cliquant sur le menu déroulant ou sur la page de visualisation des tâches. - -![La capture d'écran dans le menu déroulant](https://kanboard.net/screenshots/documentation/dropdown-screenshot.png) - -Pour ajouter une nouvelle image, prenez votre capture et collez-la avec CTRL+V ou Command+V: - -![Page de capture](https://kanboard.net/screenshots/documentation/task-screenshot.png) - -Avec Mac OS X, vous pouvez utiliser les raccourcis suivants pour prendre des captures d'écran : - -- Command-Control-Maj-3 : prend une capture de l'écran entier et l'enregistre dans le presse-papiers -- Command-Control-Maj-4, puis choix d'une zone : prend une capture d'une zone définie et l'enregistre dans le presse-papiers -- Command-Control-Maj-4, puis touche espace, puis clic sur une fenêtre : prend une capture d'une fenêtre et l'enregistre dans le presse-papiers - -Il existe plusieurs applications tierces qui peuvent être utilisées pour prendre des captures d'écran avec des annotations et un choix de formes. - -**Remarque : cette fonctionnalité n'est pas disponible sur tous les navigateurs.** Elle n'existe pas pour Safari en raison de ce bug : https://bugs.webkit.org/show_bug.cgi?id=49141 - diff --git a/doc/fr/screenshots/automatic-action-creation.png b/doc/fr/screenshots/automatic-action-creation.png deleted file mode 100644 index ad90590d..00000000 Binary files a/doc/fr/screenshots/automatic-action-creation.png and /dev/null differ diff --git a/doc/fr/screenshots/board-collapsed-mode.png b/doc/fr/screenshots/board-collapsed-mode.png deleted file mode 100644 index a496faff..00000000 Binary files a/doc/fr/screenshots/board-collapsed-mode.png and /dev/null differ diff --git a/doc/fr/screenshots/board-compact-mode.png b/doc/fr/screenshots/board-compact-mode.png deleted file mode 100644 index 872ceae5..00000000 Binary files a/doc/fr/screenshots/board-compact-mode.png and /dev/null differ diff --git a/doc/fr/screenshots/board-expanded-mode.png b/doc/fr/screenshots/board-expanded-mode.png deleted file mode 100644 index 19f61451..00000000 Binary files a/doc/fr/screenshots/board-expanded-mode.png and /dev/null differ diff --git a/doc/fr/screenshots/board-task-limit.png b/doc/fr/screenshots/board-task-limit.png deleted file mode 100644 index 8353f33c..00000000 Binary files a/doc/fr/screenshots/board-task-limit.png and /dev/null differ diff --git a/doc/fr/screenshots/board-view.png b/doc/fr/screenshots/board-view.png deleted file mode 100644 index 0d1e18ea..00000000 Binary files a/doc/fr/screenshots/board-view.png and /dev/null differ diff --git a/doc/fr/screenshots/calendar-view.png b/doc/fr/screenshots/calendar-view.png deleted file mode 100644 index 1226162b..00000000 Binary files a/doc/fr/screenshots/calendar-view.png and /dev/null differ diff --git a/doc/fr/screenshots/gantt-view.png b/doc/fr/screenshots/gantt-view.png deleted file mode 100644 index 3caafa98..00000000 Binary files a/doc/fr/screenshots/gantt-view.png and /dev/null differ diff --git a/doc/fr/screenshots/hide-column.png b/doc/fr/screenshots/hide-column.png deleted file mode 100644 index 61015f9a..00000000 Binary files a/doc/fr/screenshots/hide-column.png and /dev/null differ diff --git a/doc/fr/screenshots/list-view.png b/doc/fr/screenshots/list-view.png deleted file mode 100644 index c40e807a..00000000 Binary files a/doc/fr/screenshots/list-view.png and /dev/null differ diff --git a/doc/fr/screenshots/new-project.png b/doc/fr/screenshots/new-project.png deleted file mode 100644 index 42e5f196..00000000 Binary files a/doc/fr/screenshots/new-project.png and /dev/null differ diff --git a/doc/fr/screenshots/new-user.png b/doc/fr/screenshots/new-user.png deleted file mode 100644 index 116e9074..00000000 Binary files a/doc/fr/screenshots/new-user.png and /dev/null differ diff --git a/doc/fr/screenshots/project-disable-sharing.png b/doc/fr/screenshots/project-disable-sharing.png deleted file mode 100644 index 58832045..00000000 Binary files a/doc/fr/screenshots/project-disable-sharing.png and /dev/null differ diff --git a/doc/fr/screenshots/project-edition.png b/doc/fr/screenshots/project-edition.png deleted file mode 100644 index ce8594fe..00000000 Binary files a/doc/fr/screenshots/project-edition.png and /dev/null differ diff --git a/doc/fr/screenshots/project-enable-sharing.png b/doc/fr/screenshots/project-enable-sharing.png deleted file mode 100644 index 147ccc53..00000000 Binary files a/doc/fr/screenshots/project-enable-sharing.png and /dev/null differ diff --git a/doc/fr/screenshots/project-permissions.png b/doc/fr/screenshots/project-permissions.png deleted file mode 100644 index 54f38690..00000000 Binary files a/doc/fr/screenshots/project-permissions.png and /dev/null differ diff --git a/doc/fr/screenshots/project-view.png b/doc/fr/screenshots/project-view.png deleted file mode 100644 index ff9a7f76..00000000 Binary files a/doc/fr/screenshots/project-view.png and /dev/null differ diff --git a/doc/fr/screenshots/show-column.png b/doc/fr/screenshots/show-column.png deleted file mode 100644 index 51f78ac8..00000000 Binary files a/doc/fr/screenshots/show-column.png and /dev/null differ diff --git a/doc/fr/screenshots/swimlane-configuration.png b/doc/fr/screenshots/swimlane-configuration.png deleted file mode 100644 index d0b25e9c..00000000 Binary files a/doc/fr/screenshots/swimlane-configuration.png and /dev/null differ diff --git a/doc/fr/screenshots/swimlanes.png b/doc/fr/screenshots/swimlanes.png deleted file mode 100644 index e24a5b85..00000000 Binary files a/doc/fr/screenshots/swimlanes.png and /dev/null differ diff --git a/doc/fr/sharing-projects.markdown b/doc/fr/sharing-projects.markdown deleted file mode 100644 index f3db3c68..00000000 --- a/doc/fr/sharing-projects.markdown +++ /dev/null @@ -1,35 +0,0 @@ -Partager des tableaux et des tâches -=================================== - -Par défaut, les tableaux sont privés, mais il est possible de rendre un tableau public. - -Un tableau public ne **peut pas être modifié, il est en lecture seule**. -Son accès est protégé par un jeton aléatoire, seules les personnes qui ont la bonne URL peuvent voir le tableau. - -Les tableaux publics sont automatiquement réactualisés toutes les minutes. -Les détails des tâches sont disponibles en lecture seule. - -Exemples d'utilisation : - -- Partager son tableau avec quelqu'un qui ne fait pas partie de votre organisation / entreprise / groupe -- Afficher le tableau sur un grand écran dans votre bureau - -Activer l'accès public ----------------------- - -Choisissez votre projet, puis cliquez sur « Accès public » et enfin sur le bouton « Activer l'accès public ». - -![Activer l'accès public](screenshots/project-enable-sharing.png) - -Lorsque l'accès public est activé, plusieurs liens sont créés : - -- Affichage du tableau public -- Lien de souscription au fil RSS -- Lien d'abonnement à iCalendar - -![Désactiver l'accès public](screenshots/project-disable-sharing.png) - -Vous pouvez désactiver l'accès public à tout moment. - -À chaque fois que vous activez ou désactivez l'accès public, un nouveau jeton aléatoire est créé. -**Les liens précédents ne fonctionneront pas**. diff --git a/doc/fr/subtasks.markdown b/doc/fr/subtasks.markdown deleted file mode 100644 index 02345c2a..00000000 --- a/doc/fr/subtasks.markdown +++ /dev/null @@ -1,43 +0,0 @@ -Sous-tâches -======== - -Les sous-tâches sont utiles pour se partager le travail que représente une tâche. - -Chaque sous-tâche : - -- peut être assignée à un membre du projet -- a trois différents statuts : **À faire**, **En cours**, **Terminé** -- dispose d'informations sur le temps de travail : **temps passé** et **temps estimé** -- est classée en fonction de sa position - -Créer des sous-tâches ------------------ - -Depuis la vue par tâche, cliquez sur **Ajouter une sous-tâche** dans le panneau latéral. - -![Ajouter une sous-tâche](https://kanboard.net/screenshots/documentation/add-subtask.png) - -Vous pouvez aussi ajouter rapidement une sous-tâche en saisissant seulement son titre : - -![Add a subtask from the task view](https://kanboard.net/screenshots/documentation/add-subtask-shortcut.png) - -Modifier le statut d'une sous-tâche ---------------------- - -Quand vous cliquez sur le titre d'une sous-tâche son statut change : - -![Sous-tâche en cours](https://kanboard.net/screenshots/documentation/subtask-status-inprogress.png) - -L'icône devant le titre est mise à jour en fonction du statut. - -![Sous-tâche effectuée](https://kanboard.net/screenshots/documentation/subtask-status-done.png) - -Remarque : quand la tâche est fermée, toutes les sous-tâches voient leur statut passer à **Terminé**. - -Chrono des sous-tâches -------------- - -- À chaque fois qu'une sous-tâche est en cours de réalisation, le chronomètre est également démarré. Il peut être lancé et interrompu à tout moment. -- Le chronomètre enregistre automatiquement le temps passé sur la sous-tâche. Vous pouvez aussi modifier manuellement la valeur du temps passé dans le champ adéquat quand vous modifiez une sous-tâche. -- Le temps passé est arrondi au quart d'heure le plus proche. Cette information est enregistrée dans un tableau distinct. -- Le temps passé à la tâche ainsi que le temps estimé sont automatiquement mis à jour en fonction de la somme de toutes les sous-tâches. diff --git a/doc/fr/swimlanes.markdown b/doc/fr/swimlanes.markdown deleted file mode 100644 index 92b4a9fa..00000000 --- a/doc/fr/swimlanes.markdown +++ /dev/null @@ -1,29 +0,0 @@ -Swimlanes -========= - -Les *swimlanes* sont des séparations horizontales de votre tableau (pensez à des « couloirs » ou « lignes d'eau » dans une piscine). - -Par exemple, cela peut servir à séparer les sorties des différentes versions d'un logiciel, à diviser vos tâches selon différents produits, équipes ou tout autre critère de votre choix. - -Tableau avec des swimlanes --------------------------- - -![Swimlanes](screenshots/swimlanes.png) - -Gestion des swimlanes ------------------- - -- Tous les projets ont une swimlane par défaut. -- S'il existe plus d'une swimlane, le tableau les affichera toutes. -- Vous pouvez glisser-déposer les tâches d'une swimlane à l'autre. - -Pour configurer les swimlanes allez sur la page de **Configuration du projet** et choisissez la section **Swimlanes**. - -![Swimlanes Configuration](screenshots/swimlane-configuration.png) - -À partir de cet endroit, vous pouvez ajouter une nouvelle swimlane ou renommer celle qui existe par défaut. -Vous pouvez aussi désactiver et modifier la position des diverses swimlanes. - -- La swimlane par défaut est toujours en haut de tableau mais vous pouvez la cacher. -- Les swimlanes inactives ne sont pas affichées dans le tableau. -- **Supprimer une swimlane ne supprime pas les tâches qui lui sont assignées**, ces tâches seront transférées à la swimlane par défaut. diff --git a/doc/fr/task-links.markdown b/doc/fr/task-links.markdown deleted file mode 100644 index f2756ac7..00000000 --- a/doc/fr/task-links.markdown +++ /dev/null @@ -1,22 +0,0 @@ -Liens entre les tâches -========== - -Les tâches peuvent être liées ensemble avec des relations prédéfinies. - -![Task Links](https://kanboard.net/screenshots/documentation/task-links.png) - -Les relations établies par défaut sont les suivantes : - -- **fait référence à** -- **bloque** | est bloqué par -- **est bloqué par** | bloque -- **duplique** | est dupliqué par -- **est dupliqué par** | duplique -- **est un enfant de** | est un parent de -- **est un parent de** | est un enfant de -- **vise les étapes importantes** | est une étape importante de -- **est une étape importante de** | vise les étapes importantes -- **correctifs** | est réglé par -- **est réglé par** | correctifs - -Ces étiquettes peuvent être modifiées dans les paramètres de l'application. diff --git a/doc/fr/time-tracking.markdown b/doc/fr/time-tracking.markdown deleted file mode 100644 index 625bc26f..00000000 --- a/doc/fr/time-tracking.markdown +++ /dev/null @@ -1,44 +0,0 @@ -Suivi du temps -============= - -Les informations de la feuille de suivi du temps peuvent être définies au niveau des tâches ou des sous-tâches - -Suivi de temps des tâches ------------------- - -![Suivi de temps des tâches ](https://kanboard.net/screenshots/documentation/task-time-tracking.png) - -Les tâches ont deux champs: - -- Temps estimé -- Temps passé - -Ces valeurs représentent des heures de travail et doivent être entrées manuellement. - - -Suivi de temps des sous-tâches ---------------------- - -![Suivi de temps des sous-tâches](https://kanboard.net/screenshots/documentation/subtask-time-tracking.png) - -Les sous-tâches ont aussi les champs "temps passé" et "temps estimé" - -Lorsque vous changez la valeur de ces champs, **le suivi des tâches est mis à jour automatiquement et devient la somme des sous-tâches**. - -Kanboard enregistre le temps entre chaque changement de statut des sous-tâches dans une table séparée - -- Changer le statut de la sous-tâche de **à faire** à **en cours** marque le temps de début -- Changer le statut de la sous-tâche de **en cours** à **à faire** marque le temps de fin mais aussi met à jour le temps passé sur la sous-tâche et la tâche - -La répartition de tous les enregistrements est visible sur la page de la tâche - -![Feuille de suivi du temps pour les tâches](https://kanboard.net/screenshots/documentation/task-timesheet.png) - -Pour chaque sous-tâche, le chrono peut être à tout moment arrêté/démarré - -![Chrono des sous-tâches](https://kanboard.net/screenshots/documentation/subtask-timer.png) - -- Le chrono ne dépend pas du statut de la sous-tâche -- Chaque fois que vous démarrez le chrono, un nouvel enregistrement est créé dans la table de suivi des temps -- Chaque fois que vous arrêtez l'horloge, la date de fin est enregistrée dans la table de suivi des temps -- Le temps passé est arrondi au quart d’heure le plus proche diff --git a/doc/fr/transitions.markdown b/doc/fr/transitions.markdown deleted file mode 100644 index 94a14bbc..00000000 --- a/doc/fr/transitions.markdown +++ /dev/null @@ -1,20 +0,0 @@ -Transitions entre les tâches -================ - -Les transitions enregistrent tous les mouvements des tâches entre les colonnes - -![Transitions](https://kanboard.net/screenshots/documentation/transitions.png) - -Depuis la page des tâches, vous pouvez accéder à ces informations: - -- Date de l'action -- Colonne d'origine -- Colonne de destination -- Exécutant (Pour l'utilisateur qui a déplacé la tâche) -- Temps passé sur la colonne d’origine - -Les données de transition entre les tâches peuvent aussi être exportées depuis la page des paramètres du projet - -![Transitions Export](https://kanboard.net/screenshots/documentation/transitions-export.png) - -Pour la période spécifiée, vous allez générer un fichier CSV que vous pouvez utiliser avec n’importe quel tableur diff --git a/doc/fr/usage-examples.markdown b/doc/fr/usage-examples.markdown deleted file mode 100644 index b91fa613..00000000 --- a/doc/fr/usage-examples.markdown +++ /dev/null @@ -1,69 +0,0 @@ -Exemples d'utilisation -============== -Il est possible de personnaliser ses tableaux selon l'activité de votre entreprise : - -Développement logiciel --------------------- - -- Prévu -- Prêt -- En cours -- À valider -- Validé -- En production - -Suivi de bogues ------------- - -- Rapporté -- Confirmé -- En cours -- Testé -- Résolu - -Ventes ------ - -- Objectifs -- Réunions -- Propositions -- Achats - -Gestion au plus juste ------------------------- - -- Idées -- Expression de la demande -- Étude de marché -- Analyses -- Fait - - -Procédure de recrutement ------------------- - -- Offres d'emploi -- Candidats -- Appels téléphoniques -- Entretiens -- Embauches - -Boutiques en ligne ------------- - -- Commande -- Empaquetage -- Prêt à envoyer -- Envoyé - -Artisanat ------------ - -- Commande -- Assemblage -- Tests -- Empaquetage -- Prêt à envoyer -- Envoyé - - diff --git a/doc/fr/user-management.markdown b/doc/fr/user-management.markdown deleted file mode 100644 index bb9b0731..00000000 --- a/doc/fr/user-management.markdown +++ /dev/null @@ -1,35 +0,0 @@ -Gestion des utilisateurs -======================== - -Ajouter un nouvel utilisateur ------------------------------ - -Pour ajouter un nouvel utilisateur, vous devez être administrateur. - -1. Depuis le menu déroulant situé en haut à droite, cliquez sur **Gestion des utilisateurs** -2. Dans la partie haute vous avez un lien **Créer un utilisateur local** ou **Créer un utilisateur distant** -3. Informez les champs de saisie et enregistrez - -![Nouvel utilisateur](screenshots/new-user.png) - -Quand vous créez un **utilisateur local**, vous devez préciser au moins deux valeurs : - -- **nom d'utilisateur** : c'est l'identifiant unique de votre utilisateur (login) -- **mot de passe** : le mot de passe de votre utilisateur doit comporter au moins 6 caractères - -Pour les **utilisateurs distants**, seul le nom d'utilisateur est obligatoire. - -Modifier des utilisateurs -------------------------- - -Quand vous allez au menu **utilisateurs**, vous disposez d'une liste d'utilisateurs. Pour modifier un utilisateur cliquez sur le lien **Modifier**. - -- si vous êtes un utilisateur ordinaire, vous ne pouvez modifier que votre propre profil -- vous devez être administrateur pour pouvoir modifier n'importe quel utilisateur - -Supprimer des utilisateurs --------------------------- - -Depuis le menu **utilisateurs**, cliquez sur le lien **supprimer**. Ce lien n'est visible que si vous êtes administrateur. - -Si vous supprimez un utilisateur particulier, **les tâches assignées à cette personne ne lui seront plus assignées** après cette opération. diff --git a/doc/fr/what-is-kanban.markdown b/doc/fr/what-is-kanban.markdown deleted file mode 100644 index f479927c..00000000 --- a/doc/fr/what-is-kanban.markdown +++ /dev/null @@ -1,34 +0,0 @@ -Qu'est-ce que Kanban? -=============== - - -Kanban est une méthodologie développée à l'origine par l'entreprise Toyota pour gagner en efficacité. - -Kanban n'impose que deux contraintes : - -- Visualiser votre flux d'activité -- Limiter votre travail en cours - -Visualiser votre flux d'activité ------------------------ - -- Votre activité est affichée sur un tableau, vous disposez ainsi d'une vue très nette sur l'ensemble de votre projet -- Chaque colonne représente une étape de votre flux d'activité - -Se concentrer sur une seule tâche à la fois sans disperser son activité ----------------------------------- - -- Chaque phase peut avoir sa date d'échéance -- Les limites fixées sont très utiles pour identifier les goulots d'étranglement -- Les limites évitent de travailler à un trop grand nombre de tâches à la fois - -Mesure des performances et des progrès ------------------------------------ - -Kanban utilise lead time et cycle times pour mesurer les performances : - -- **Lead time** : le *lead time* est la durée entre la création de la tâche et son achèvement. -- **Cycle time** : le *cycle time* est la durée entre la date de début et l'achèvement. - -Par exemple, vous pouvez avoir un *lead time* de 100 jours et n'avoir à travailler qu'une heure pour achever la tâche. - diff --git a/doc/fr_FR/2fa.markdown b/doc/fr_FR/2fa.markdown new file mode 100644 index 00000000..2ecaa10b --- /dev/null +++ b/doc/fr_FR/2fa.markdown @@ -0,0 +1,33 @@ +Authentification à deux facteurs +========================= + +Chaque utilisateur peut activer [l'authentification à deux facteurs](http://en.wikipedia.org/wiki/Two_factor_authentication). +Après s’être connecté, un code à usage unique (6 caractères) est demandé à l'utilisateur pour lui autoriser l’accès à Kanboard. + +Ce code doit être fourni par un logiciel compatible, généralement installé sur votre smartphone. + +Kanboard utilise le [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) défini dans la [RFC 6238](http://tools.ietf.org/html/rfc6238). + +Il existe de nombreux logiciels compatibles avec le standard TOTP system. +Par exemple, vous pouvez utilisez ces applications libres et open source : + +- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry) +- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS) +- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (utilitaire en ligne de commande sur Unix/Linux) + +Ce système peut fonctionner hors ligne et vous n'avez pas l'obligation d'avoir un téléphone portable. + +Paramétrage +----- + +1. Allez dans le profil utilisateur. +2. Sur la gauche, cliquez sur **Authentification à deux facteurs** et cochez la case. +3. Une clef secrète est générée pour vous. + +![2FA](https://kanboard.net/screenshots/documentation/2fa.png) + +- Vous devez sauvegarder votre clef dans votre logiciel TOTP. Si vous utilisez un smartphone, la solution la plus simple est de scanner le QR code avec FreeOTP ou Google Authenticator +- À chaque ouverture de session, un nouveau code sera demandé +- N'oubliez pas de tester votre appareil avant de quitter votre session + +Une nouvelle clef est générée à chaque fois que vous activez/désactivez cette fonction diff --git a/doc/fr_FR/analytics-tasks.markdown b/doc/fr_FR/analytics-tasks.markdown new file mode 100644 index 00000000..0eb89e34 --- /dev/null +++ b/doc/fr_FR/analytics-tasks.markdown @@ -0,0 +1,24 @@ +Analytique des tâches +=================== + +Chaque tâche possède une section analytique accessible à partir du menu à gauche dans la page des tâches + +Lead et cycle time +------------------- + +![Lead and cycle time](https://kanboard.net/screenshots/documentation/task-lead-cycle-time.png) + +- Le lead time est la durée entre la création de la tâche et son achèvement (tâche fermée). +- Le cycle time est la durée entre la date de début et l'achèvement. +- Si la tâche n’est pas fermée, l’heure courante est utilisée à la place de la date d'achèvement. +- Si la date de départ n'est pas spécifiée, le cycle time n'est pas calculé. + +Remarque : vous pouvez configurer une action pour définir automatiquement que la date de départ sera le moment où vous déplacez une tâche vers une colonne de votre choix + +Temps passé dans chaque colonne +--------------------------- + +![Temps passé dans chaque colonne](https://kanboard.net/screenshots/documentation/time-into-each-column.png) + +- Ce graphique montre le temps total passé dans chaque colonne pour la tâche +- Le temps passé est calculé jusqu’à ce que la tâche soit fermée diff --git a/doc/fr_FR/analytics.markdown b/doc/fr_FR/analytics.markdown new file mode 100644 index 00000000..0b94f272 --- /dev/null +++ b/doc/fr_FR/analytics.markdown @@ -0,0 +1,70 @@ +Analytique +========= + +Chaque projet dispose d'une section analytique. En fonction de la façon dont vous utilisez Kanboard, vous pourrez voir les rapports suivants : + +Répartition des utilisateurs +---------------- + +![Répartition des utilisateurs](https://kanboard.net/screenshots/documentation/user-repartition.png) + +Ce graphique circulaire affiche le nombre de tâches assignées par utilisateur. + +Distribution des tâches +----------------- + +![Distribution des tâches](https://kanboard.net/screenshots/documentation/task-distribution.png) + +Ce graphique circulaire donne une vue d'ensemble du nombre de tâches ouvertes par colonne. + +Diagramme de flux cumulé +----------------------- + +![Diagramme de flux cumulé](https://kanboard.net/screenshots/documentation/cfd.png) + +- Ce graphique affiche le nombre de tâches de façon cumulée pour chaque colonne en fonction du temps passé. +- Chaque jour, le nombre total de tâches est enregistré pour chaque colonne. +- Si vous souhaitez exclure les tâches terminées, modifiez les [paramètres du projet global](project-configuration.markdown). + +Remarque : il faut au moins deux jours de données pour que le graphique apparaisse. + +Graphique d'avancement +-------------- + +![Graphique d'avancement](https://kanboard.net/screenshots/documentation/burndown-chart.png) + +Un [graphique d'avancement](http://en.wikipedia.org/wiki/Burn_down_chart) est disponible pour chaque projet. + +- Il s'agit de la représentation graphique du travail qui reste à faire en fonction du temps restant. +- Kanboard utilise la complexité des estimations d'achèvement pour créer le graphique. +- Chaque jour, la somme des estimations pour chaque colonne est calculée. + +Temps moyen passé pour chaque colonne +----------------------------------- + +![Temps moyen passé pour chaque colonne](https://kanboard.net/screenshots/documentation/average-time-spent-into-each-column.png) + +Ce graphique affiche le temps moyen passé pour chaque colonne pour les 1000 dernière tâches. + +- Kanboard utilise les transitions entre tâches pour calculer les données. +- Le temps passé est calculé jusqu'à la fin de la tâche. + +Temps moyen de Lead et Cycle +--------------------------- + +![Temps moyen passé pour chaque colonne](https://kanboard.net/screenshots/documentation/average-lead-cycle-time.png) + +Ce graphique affiche le temps moyen de lead et cycle pour les 1000 dernières tâches au cours du temps. + +- Le *lead time* est le temps passé entre la création de la tâche et sa date d'achèvement. +- Le *cycle time* est le temps passé entre la date de début spécifiée et la date d'achèvement de la tâche. +- Si la tâche n'est pas close, la date courante est utilisée à la place de la date d'achèvement. + +Ces métriques sont calculées et enregistrées chaque jour pour l'ensemble du projet. + +N'oubliez pas de lancer chaque jour le calcul statistique +------------------------------------------------------- + +Pour générer des données analytique précises, vous devriez lancer chaque jour le cronjob **statistiques quotidiennes du projet**. + +[Consultez la documentation sur la ligne de commande avec Kanboard](cli.markdown) diff --git a/doc/fr_FR/application-configuration.markdown b/doc/fr_FR/application-configuration.markdown new file mode 100644 index 00000000..12768f03 --- /dev/null +++ b/doc/fr_FR/application-configuration.markdown @@ -0,0 +1,41 @@ +Paramètres de l'application +==================== + +Certains paramètres de l'application peuvent être modifiés sur la page des paramètres. +Seuls les administrateurs peuvent modifier ces paramètres. + +Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche. + +![Paramètres de l'application](https://kanboard.net/screenshots/documentation/application-settings.png) + +### URL de l'application + +Ce paramètre est utilisé pour les notifications par mail. +Le pied de page du mail contiendra un lien vers la tâche du Kanboard. + +### Langue + +La langue de l'application peut être modifiée à tout moment. +Elle sera définie pour tous les utilisateurs. + +### Fuseau horaire + +Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire. +La liste contient tous les fuseaux horaires pris en charge par votre serveur web. + +### Format de date + +Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches. + +Kanboard propose 4 différents formats: + +- JJ/MM/AAAA +- MM/JJ/AAAA (par défaut) +- AAAA/MM/JJ +- MM.JJ.AAAA + +Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ). + +### Feuille de style personnalisée + +Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard. diff --git a/doc/fr_FR/application-configuration.markup b/doc/fr_FR/application-configuration.markup new file mode 100644 index 00000000..12768f03 --- /dev/null +++ b/doc/fr_FR/application-configuration.markup @@ -0,0 +1,41 @@ +Paramètres de l'application +==================== + +Certains paramètres de l'application peuvent être modifiés sur la page des paramètres. +Seuls les administrateurs peuvent modifier ces paramètres. + +Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche. + +![Paramètres de l'application](https://kanboard.net/screenshots/documentation/application-settings.png) + +### URL de l'application + +Ce paramètre est utilisé pour les notifications par mail. +Le pied de page du mail contiendra un lien vers la tâche du Kanboard. + +### Langue + +La langue de l'application peut être modifiée à tout moment. +Elle sera définie pour tous les utilisateurs. + +### Fuseau horaire + +Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire. +La liste contient tous les fuseaux horaires pris en charge par votre serveur web. + +### Format de date + +Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches. + +Kanboard propose 4 différents formats: + +- JJ/MM/AAAA +- MM/JJ/AAAA (par défaut) +- AAAA/MM/JJ +- MM.JJ.AAAA + +Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ). + +### Feuille de style personnalisée + +Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard. diff --git a/doc/fr_FR/automatic-actions.markdown b/doc/fr_FR/automatic-actions.markdown new file mode 100644 index 00000000..f136b98c --- /dev/null +++ b/doc/fr_FR/automatic-actions.markdown @@ -0,0 +1,133 @@ +Actions automatiques +==================== + +Pour réduire au minimum l'interaction avec les utilisateurs, Kanboard dispose d'actions automatiques. + +Chaque action automatique est définie ainsi : + +- Un événement à suivre +- Une action associée à cet évènement +- Éventuellement quelques paramètres à définir + +Chaque projet a une série d'actions automatisées qui lui sont propres, le panneau de configuration est situé sur la page qui liste les projets, il vous suffit de cliquer sur le lien **Actions automatiques**. + +Ajouter une nouvelle action +--------------------------- + +Cliquez sur le lien **Ajouter une nouvelle action**. + +![Action automatique](screenshots/automatic-action-creation.png) + +- Commencez par choisir une action +- Ensuite, sélectionnez un évènement +- Et pour finir, les paramètres de l'action + +Liste des évènements disponibles +-------------------------------- + +- Déplacement d'une tâche vers une autre colonne +- Déplacement d'une tâche à un autre emplacement de la même colonne +- Modification d'une tâche +- Création d'une tâche +- Réouverture d'une tâche +- Fermeture d'une tâche +- Création ou modification d'une tâche +- Changement d'assigné à une tâche +- Création ou mise à jour du lien vers une tâche +- Réception d'un *commit* de Github +- Ouverture d'une *issue* de Github +- Fermeture d'une *issue* de Github +- Réouverture d'une *issue* de Github +- Modification de l'assigné à une *issue* de Github +- Modification de l'étiquette d'une *issue* de Github +- Création d'un commentaire d'une *issue* de Github +- Ouverture d'une *issue* de Gitlab +- Fermeture d'une *issue* de Gitlab +- Réception d'un *commit* de Gitlab +- Réception d'un *commit* de Bitbucket +- Ouverture d'une *issue* de Bitbucket +- Fermeture d'une *issue* de Bitbucket +- Réouverture d'une *issue* de Bitbucket +- Modification de l'assigné à une *issue* de Bitbucket issue assignee change +- Création d'un commentaire d'une *issue* de Bitbucket + +Liste des actions disponibles +----------------------------- + +- Fermer une tâche +- Ouvrir une tâche +- Assigner la tâche à un utilisateur particulier +- Assigner la tâche à la personne qui fait l'action +- Cloner la tâche depuis un autre projet +- Déplacer la tâche vers un autre projet +- Déplacer la tâche vers une autre colonne quand elle est assignée à un utilisateur +- Déplacer la tâche vers une autre colonne quand quand l'assigné est supprimé +- Assigner une couleur quand la tâche est déplacée vers une colonne particulière +- Assigner une couleur à un utilisateur particulier +- Assigner automatiquement une couleur selon la catégorie +- Assigner automatiquement une catégorie en fonction d'une couleur +- Créer un commentaire depuis un fournisseur externe +- Créer une tâche depuis un fournisseur externe +- Ajouter un journal de commentaires quand on change une tâche de colonne +- Modifier l'assigné en fonction d'un nom d'utilisateur externe +- Modifier la catégorie en fonction d'une étiquette externe +- Mettre à jour automatiquement la date de début +- Déplacer la tâche vers une autre colonne quand la catégorie a changé +- Envoyer une tâche par mail à quelqu'un +- Modifier la couleur de la tâche quand on utilise un lien particulier pour cette tâche + +Exemples +-------- +Voici quelques exemples d'utilisation dans la vraie vie : + +### Quand je déplace une tâche vers la colonne "Terminer", fermer automatiquement cette tâche + +- Choisir l'action : **Fermer la tâche** +- Choisir l'évènement : **Déplacement d'une tâche vers une autre colonne** +- Définir le paramètre de l'action : **Colonne = Terminé** (c'est la colonne de destination) + +### Quand je déplace une tâche vers la colonne "À valider", assigner cette tâche à un utilisateur particulier + +- Choisir l'action : **Assigner la tâche à un utilisateur particulier** +- Choisir l'évènement : **Déplacer une tâche vers une nouvelle colonne** +- Définir les paramètres de l'action :**Colonne = À valider** et **Utilisateur = Adrien** (Adrien est par exemple un testeur) + +### Quand je déplace une tâche vers la colonne "Travail en cours", assigner cette tâche à l'utilisateur courant + +- Choisir l'action : **Assigner la tâche à la personne qui fait cette action** +- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** +- Définir le paramètre de l'action : **Colonne = Travail en cours** + +### Quand une tâche est terminée, dupliquer cette tâche vers un autre projet + +Supposons que nous ayons deux projets : "Commande du client" et "Production". Une fois validée la commande, la basculer vers le projet "Production". + +- Choisir l'action : **Dupliquer la tâche vers un autre projet** +- Choisir l'évènement : **Fermer une tâche** +- Définir les paramètres de l'action : **Colonne = Validé** et **Projet = Production** + +### Quand une tâche est déplacée vers la toute dernière colonne, déplacer la même tâche exactement vers un autre projet + +Supposons que nous ayons deux projets : "Idées" et "Développement". Une fois validée l'idée, la basculer vers le projet "Développement". + +- Choisir l'action : **Déplacer la tâche vers un autre projet** +- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** +- Définir les paramètres de l'action : **Colonne = Validé** et **Projet = Développement** + +### Je veux assigner automatiquement une couleur à l'utilisateur Adrien + +- Choisir l'action : **Assigner une couleur à un utilisateur particulier** +- Choisir l'évènement : **Modification de l'assigné à une tâche** +- Définir les paramètres de l'action :**Couleur = Vert** et **Assigné = Adrien** + +### Je veux assigner automatiquement une couleur à la catégorie "Demande de fonctionnalité" + +- Choisir l'action : **Assigner automatiquement une couleur à une catégorie particulière** +- Choisir l'évènement : **Création ou modification d'une tâche** +- Définir les paramètres de l'action : **Couleur = Bleu** et **Catégorie = Demande de fonctionnalité** + +### Je veux régler automatiquement la date de début quand la tâche est déplacée dans la colonne "Travail en cours" + +- Choisir l'action : **Mettre à jour automatiquement la date de début** +- Choisir l'évènement : **Déplacer une tâche vers une autre colonne** +- Définir les paramètres de l'action : **Colonne= Travail en cours** diff --git a/doc/fr_FR/board-collapsed-expanded.markdown b/doc/fr_FR/board-collapsed-expanded.markdown new file mode 100644 index 00000000..29396772 --- /dev/null +++ b/doc/fr_FR/board-collapsed-expanded.markdown @@ -0,0 +1,18 @@ +Mode replié et déplié +===================== + +Les tâches peuvent être affichées sur le tableau en mode replié ou déplié. +Basculer d'un mode à l'autre peut être fait à l'aide du raccourci clavier **« s »** ou en utilisant le menu déroulant sur la gauche. + +Mode replié +----------- + +![Tâches repliées](screenshots/board-collapsed-mode.png) + +- Si la tâche est affectée à quelqu'un, les initiales de la personne sont affichées à côté du numéro de la tâche. +- Si le titre de la tâche est trop long, mettez le curseur de la souris au-dessus de la tâche pour voir une boite flottante avec le titre entier. + +Mode déplié +----------- + +![Tâches dépliées](screenshots/board-expanded-mode.png) diff --git a/doc/fr_FR/board-configuration.markdown b/doc/fr_FR/board-configuration.markdown new file mode 100644 index 00000000..f7f8be33 --- /dev/null +++ b/doc/fr_FR/board-configuration.markdown @@ -0,0 +1,24 @@ +Paramètres du tableau +============== + +Allez dans le menu **Paramètres** puis choisissez *Paramètres du tableau** sur la gauche + +![Paramètres du tableau](https://kanboard.net/screenshots/documentation/board-settings.png) + +### Mise en avant d'une tâche + +Cette fonctionnalité affiche une ombre autour de la tâche lorsqu'une tâche à été déplacée récemment. + +Initialisez la fonctionnalité à 0 pour la désactiver, par défaut 2 jours (172800 secondes). + +Toutes les tâches qui ont été déplacées depuis 2 jours seront entourées d'une ombre. + +### Intervalle pour rafraîchir un tableau public + + Lorsque vous partagez un tableau, la page sera, par défaut, automatiquement rafraîchie toutes les 60 secondes. + +### Intervalle pour rafraîchir un tableau privé + + Lorsque votre navigateur web est ouvert sur un tableau, Kanboard vérifie toutes les 10 secondes si quelque chose à été modifié par un autre utilisateur. + + Techniquement, ce processus est fait par Ajax polling. diff --git a/doc/fr_FR/board-horizontal-scrolling-and-compact-view.markdown b/doc/fr_FR/board-horizontal-scrolling-and-compact-view.markdown new file mode 100644 index 00000000..7ad9c23c --- /dev/null +++ b/doc/fr_FR/board-horizontal-scrolling-and-compact-view.markdown @@ -0,0 +1,11 @@ +Défilement horizontal et mode compact +===================================== + +Lorsque le tableau ne loge pas dans votre écran, une barre de défilement horizontal appaîtra en bas de l'écran. + +Cependant, il est possible de basculer vers la vue compacte pour afficher toutes les colonnes dans votre écran. + +![Tableau en mode compact](screenshots/board-compact-mode.png) + +Basculer entre le défilement horizontal et la vue compacte s'effectue avec le raccourci clavier **« c »** ou en utilisant le menu déroulant sur la gauche. + diff --git a/doc/fr_FR/board-show-hide-columns.markdown b/doc/fr_FR/board-show-hide-columns.markdown new file mode 100644 index 00000000..8eac0b2c --- /dev/null +++ b/doc/fr_FR/board-show-hide-columns.markdown @@ -0,0 +1,12 @@ +Afficher ou cacher des colonnes dans le tableau +=============================================== + +Vous pouvez très facilement cacher ou afficher des colonnes dans le tableau : + +![Cacher une colonne](screenshots/hide-column.png) + +Pour cacher une colonne, ouvrez le menu déroulant de la colonne. + +![Afficher une colonne](screenshots/show-column.png) + +Pour afficher de nouveau la colonne, cliquez sur l'icône avec le « plus ». diff --git a/doc/fr_FR/calendar-configuration.markdown b/doc/fr_FR/calendar-configuration.markdown new file mode 100644 index 00000000..6494568a --- /dev/null +++ b/doc/fr_FR/calendar-configuration.markdown @@ -0,0 +1,43 @@ +Paramètres du calendrier +================= + +Allez au menu **Paramètres**, puis choisissez **Paramètres du calendrier** sur la gauche. + +![Paramètres du calendrier](https://kanboard.net/screenshots/documentation/calendar-settings.png) + +il existe deux calendriers distincts dans Kanboard : + +- le calendrier du projet +- le calendrier de l'utilisateur, disponible dans le tableau de bord + +Le calendrier du projet +---------------- + +Ce calendrier affiche les tâches avec les dates d'échéance et les tâches selon leur date de création ou de début. + +### Afficher les tâches selon leur date de création + +- La date de début d'un évènement du calendrier est la date de création de la tâche. +- la date de fin de l'évènement est la date d'achèvement de la tâche. + +### Afficher les tâches selon leur date de début + +- La date de début d'un évènement du calendrier est la date du démarrage effectif de la tâche. +- Cette date ne peut pas être définie manuellement. +- La date de fin de l'évènement est la date de l'achèvement de la tâche. +- S'il n'existe pas de date de début la tâche ne figurera pas sur le calendrier . + +Calendrier de l'utilisateur +------------- + +Ce calendrier n'affiche que les tâches assignées à l'utilisateur et de façon facultative des informations sur les sous-tâches. + +### Afficher les sous-tâches selon le suivi du temps passé + +- Affiche les sous-tâches dans le calendrier d'après les informations recueillies dans l afeuille de suivi du temps. +- Le croisement des données avec l'emploi du temps de l'utilisateur est également calculé. + +### Afficher les estimations des sous-tâches (anticipation sur le travail à venir) + +- Affiche l'estimation du travail à venir pour les sous-tâches qui ont le statut « à faire » et avec une valeur définie à « estimé ». + diff --git a/doc/fr_FR/calendar.markdown b/doc/fr_FR/calendar.markdown new file mode 100644 index 00000000..2ceeeaa4 --- /dev/null +++ b/doc/fr_FR/calendar.markdown @@ -0,0 +1,20 @@ +Calendriers +======== + +il existe deux visualisations différentes des calendriers : + +- La vue du projet avec des filtres (disponibles depuis le tableau) +- La vue utilisateur (disponible depuis le tableau de bord de l'utilisateur) + +Pour l'instant le calendrier permet d'afficher les informations suivantes : + +- Les tâches avec une date d'échéance, affichée en haut. **La date d'échéance peut être modifiée en déplaçant la tâche vers un autre jour**. +- les tâches basées sur la date de création ou la date de début. **Ces évènements ne peuvent pas être modifiés avec le calendrier**. +- Le suivi dans le temps de sous-tâches, tous les segments temporels sont affichés dans le calendrier. +- Les estimations pour les sous-tâches, les prévisions et le travail restant + +![Calendrier](https://kanboard.net/screenshots/documentation/calendar.png) + +La configuration du calendrier peut être modifiée dans la page des paramètres. + +Remarque : la date d'échéance n'inclut pas d'information temporelle. diff --git a/doc/fr_FR/closing-tasks.markdown b/doc/fr_FR/closing-tasks.markdown new file mode 100644 index 00000000..022a1dfd --- /dev/null +++ b/doc/fr_FR/closing-tasks.markdown @@ -0,0 +1,16 @@ +Fermer des tâches +============= + +Quand une tâche est fermée, elle n'est plus visible sur le tableau. + +Toutefois, vous pouvez toujours accéder à la liste des tâches closes en utilisant la requête **status:closed** dans un formulaire de recherche, ou bien choisissez simplement **Tâches fermées** dans le menu déroulant des filtres. + +Il existe deux façons différentes de fermer une tâche, depuis le menu déroulant des tâches sur le tableau : + +![Fermer une tâche par le menu déroulant](https://kanboard.net/screenshots/documentation/menu-close-task.png) + +…ou bien depuis la barre latérale dans la vue détaillée des tâches + +![Fermer une tâche](https://kanboard.net/screenshots/documentation/closing-tasks.png) + +Remarque : quand vous fermez une tâche, toutes les sous-tâches qui ne sont pas achevées verront leur statut passer à "Terminé". diff --git a/doc/fr_FR/create-tasks-by-email.markdown b/doc/fr_FR/create-tasks-by-email.markdown new file mode 100644 index 00000000..dd06a1c4 --- /dev/null +++ b/doc/fr_FR/create-tasks-by-email.markdown @@ -0,0 +1,45 @@ +Créer des tâches par email +===================== + +Vous pouvez créer des tâches directement en envoyant un message. + +Pour le moment, Kanboard fonctionne avec 3 services externes : + +- [Mailgun](https://kanboard.net/documentation/mailgun) +- [Sendgrid](https://kanboard.net/documentation/sendgrid) +- [Postmark](https://kanboard.net/documentation/postmark) + +Ces services gèrent le courrier entrant sans qu'on ait à configurer un serveur SMTP. + +À la réception d'un email par l'un de ces services, le message qu'il contenait est transmis et traité automatiquement par Kanboard. +Toutes les opérations complexes sont prises en charge par ces services. + +Processus de réception du courrier entrant +------------------------ + +1. Vous envoyez un mail à une adresse spécifique, par exemple **quelquechose+monprojet@inbound.mondomaine.tld** +2. Votre mail est envoyé sur les serveurs tiers SMTP +3. Le fournisseur de SMTP appelle Kanboard via un webhook avec le mail en JSON ou aux formats multipart/form-data +4. Kanboard analyse le mail reçu et crée la tâche dans le bon projet + +Remarque : les nouvelles tâches sont automatiquement créées dans la première colonne. + +Format du mail +------------ + +- La partie locale de l'adresse mail doit utiliser le signe + comme séparateur, par exemple **kanboard+projet123** +- La chaîne de caractères définie après le signe + doit correspondre à l'identifiant d'un projet, par exemple **projet123** est l'identifiant du projet **Projet 123** +- le sujet de l'email devient le titre de la tâche +- Le corps du message devient la description de la tâche (au format Markdown) + +Les courriers entrants peuvent être écrits aux formats .txt ou .HTML. +**Kanboard peut convertir en Markdown les messages écrits en simple HTML**. + +Sécurité et prérequis +------------------------- + +- Le webhook de Kanboard est protégé par un jeton aléatoire +- L'adresse de l'expéditeur doit correspondre à celle d'un utilisateur de Kanboard +- L'utilisateur de Kanboard doit être un membre du projet +- Le projet Kanboard doit avoir un identifiant unique, par exemple **MONPROJET** + diff --git a/doc/fr_FR/creating-projects.markdown b/doc/fr_FR/creating-projects.markdown new file mode 100644 index 00000000..e5da7cc6 --- /dev/null +++ b/doc/fr_FR/creating-projects.markdown @@ -0,0 +1,39 @@ +Créer des projets +================= + +Kanboard peut gérer de multiples projets. Voici deux sortes de projets : + +- Les projets multi-utilisateurs (pour le travail collaboratif, en équipe) +- Les projets privés, réservés à un seul utilisateur + +Créer des projets multi-utilisateurs +------------------------------------ + +- Seuls les administrateurs et les gestionnaires de projets peuvent créer ce type de projets +- La gestion des utilisateurs est disponible + +Depuis le tableau de bord, cliquez sur le lien **Nouveau projet** : + +![Formulaire de création de projet](screenshots/new-project.png) + +C'est vraiment très simple, il vous suffit de trouver un nom pour votre projet ! + +Créer un projet privé +--------------------- + +- Tout le monde peut créer un projet privé (sauf si désactivé par l'administrateur) +- Il n'y a **pas** de gestion des utilisateurs +- Seuls le propriétaire et les administrateurs peuvent accéder au projet + +Depuis le tableau principal, cliquez sur le lien **Nouveau projet privé**. + +Créer un projet depuis un autre projet +-------------------------------------- + +Lorsque vous créez un nouveau projet, vous pouvez choisir de dupliquer les propriétés d'un projet existant : + +- Permissions +- Catégories +- Actions +- Swimlanes +- Tâches diff --git a/doc/fr_FR/creating-tasks.markdown b/doc/fr_FR/creating-tasks.markdown new file mode 100644 index 00000000..9b7fa274 --- /dev/null +++ b/doc/fr_FR/creating-tasks.markdown @@ -0,0 +1,27 @@ +Créer des tâches +============== + +Depuis le tableau, cliquez sur le signe plus + à côté du nom de la colonne : + +![Création de tâche à partir du tableau](https://kanboard.net/screenshots/documentation/task-creation-board.png) + +Le formulaire de création de tâche apparaît : + +![Formulaire de création de tâche](https://kanboard.net/screenshots/documentation/task-creation-form.png) + +Le seul champ obligatoire est le titre. + +Description des champs : + +- **Titre** : le titre de votre tâche, tel qu'il sera affiché sur le tableau. +- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](https://kanboard.net/documentation/syntax-guide). +- **Créer une autre tâche** : cochez cette case si vous souhaitez créer une tâche similaire (les champs seront pré-remplis). +- **Assigné** : la personne qui va travailler sur la tâche. +- **Catégorie** : une seule catégorie peut être assignée à une tâche. +- **Colonne** : la colonne dans laquelle la tâche sera créée. La tâche sera positionnée en bas de cette colonne. +- **Couleur** : Choisissez la couleur de la carte. +- **Complexité** : utilisée dans la gestion de projet agile (Scrum), la complexité des points d'étape est un nombre qui montre à l'équipe le degré de difficulté de l'avancement du projet. Les utilisateurs se servent souvent des suites de Fibonacci. +- **Estimation originale** : estimation du nombre d'heures nécessaire pour terminer les tâches. +- **Date d'échéance** : les tâches dont la date d'échéance est dépassée auront une date d'échéance en rouge et les dates suivantes seront en noir dans le tableau. Plusieurs formats de date sont acceptés, outre le sélecteur de date. + +Avec le lien d'aperçu (« Prévisualiser »), vous pouvez voir la description de la tâche convertie depuis la syntaxe Markdown. diff --git a/doc/fr_FR/currency-rate.markdown b/doc/fr_FR/currency-rate.markdown new file mode 100644 index 00000000..e84acd31 --- /dev/null +++ b/doc/fr_FR/currency-rate.markdown @@ -0,0 +1,11 @@ +Taux de change des devises +============== + +Chaque utilisateur peut avoir un taux horaire prédéfini dans différentes devises. +Si vous avez à manipuler plusieurs devises, vous pouvez définir ici le taux en fonction de la devise de référence. + +Cette fonctionnalité est utilisée pour calculer le budget du projet. + +![Currency Rate](https://kanboard.net/screenshots/documentation/currency-rate.png) + +Les paramètres pour le taux de change des devises sont situés dans **Paramètres > Taux de change** diff --git a/doc/fr_FR/duplicate-move-tasks.markdown b/doc/fr_FR/duplicate-move-tasks.markdown new file mode 100644 index 00000000..07c863d0 --- /dev/null +++ b/doc/fr_FR/duplicate-move-tasks.markdown @@ -0,0 +1,58 @@ +Dupliquer et déplacer des tâches +======================== + +Dupliquer une tâche dans le même projet +-------------------------------------- + +Allez à la vue par tâche et choisissez **Dupliquer** sur la gauche. + +![Duplication de tâche](https://kanboard.net/screenshots/documentation/task-duplication.png) + +Une nouvelle tâche sera créée avec les mêmes propriétés que celles de la tâche originale. + +Dupliquer une tâche vers un autre projet +----------------------------------- + +Allez à la vue par tâches et choisissez **Dupliquer dans un autre projet**. + +![Duplication d'une tâche dans un autre projet](https://kanboard.net/screenshots/documentation/task-duplication-another-project.png) + +Seuls les projets dont vous êtes membre apparaîtront dans le menu déroulant. + +Avant de copier les tâches, Kanboard vous demandera les propriétés de la destination qui ne sont pas communes entre les projets source et destination. + +Vous devez essentiellement définir : + +- La swimlane de destination +- La colonne +- La catégorie +- L'assigné + +Déplacer une tâche vers un autre projet +------------------------------ + +Allez à la vue par tâches et choisissez **Déplacer vers un autre projet**. + +Déplacer vers un autre projet est semblable à l'opération de duplication, vous devez choisir les nouvelles propriétés de la tâche. + +Liste des champs dupliqués +------------------------- +Voici la liste des champs dupliqués : + +- title +- description +- date_due +- color_id +- project_id +- column_id +- owner_id +- score +- category_id +- time_estimated +- swimlane_id +- recurrence_status +- recurrence_trigger +- recurrence_factor +- recurrence_timeframe +- recurrence_basedate + diff --git a/doc/fr_FR/editing-projects.markdown b/doc/fr_FR/editing-projects.markdown new file mode 100644 index 00000000..2186a1b9 --- /dev/null +++ b/doc/fr_FR/editing-projects.markdown @@ -0,0 +1,15 @@ +Modifier des projets +==================== + +Les projets peuvent être renommés et désactivés à tout moment. + +Pour renommer un projet, il suffit de cliquer sur le lien « Modifier un projet » sur la gauche. + +![Modification de projet](screenshots/project-edition.png) + +- Les dates de début et de fin sont utilisées pour créer le diagramme de Gantt du projet +- La description est visible en infobulle sur le tableau et sur la page qui liste les projets +- Les administrateurs et administrateurs de projets peuvent convertir un projet privé en projet multi-utilisateur en décochant la case « Projet privé ». +- Vous pouvez également convertir un projet multi-utilisateur en projet privé. + +Remarque : quand vous rendez un projet privé, tous les utilisateurs existants auront accès au projet. Ajustez la liste des utilisateurs selon vos besoins. diff --git a/doc/fr_FR/gantt-chart-projects.markdown b/doc/fr_FR/gantt-chart-projects.markdown new file mode 100644 index 00000000..3801dc88 --- /dev/null +++ b/doc/fr_FR/gantt-chart-projects.markdown @@ -0,0 +1,17 @@ +Diagramme de Gantt pour tous les projets +============================ + +Le but de ce diagramme de Gantt est d'afficher une vue d'ensemble de tous les projets basée sur les dates de début et de fin. + +- Ce diagramme de Gantt est disponible dans la section de gestion du projet +- Seuls les administrateurs et administrateurs de projet peuvent accéder à cette section +- Les administrateurs de projet ne verront que les projets dans lesquels il y a des membres +- Les objets privés ne sont pas affichés dans ce graphique + +![Diagramme de Gantt pour tous les projets](https://kanboard.net/screenshots/documentation/gantt-chart-all-projects.png) + +- La **date de début** et la **date de fin** des projets est utilisée pour construire le graphique +- Les barres horizontales peuvent être redimensionnées et déplacées latéralement avec votre souris +- Il n'y a pas de glisser-déposer vertical +- Les barres de projet sont affichées en noir quand il n'y a ni date de début ni date de fin définies +- L'infobulle affiche la liste des gestionnaires de projets et les membres ordinaires diff --git a/doc/fr_FR/gantt-chart-tasks.markdown b/doc/fr_FR/gantt-chart-tasks.markdown new file mode 100644 index 00000000..fbd1b587 --- /dev/null +++ b/doc/fr_FR/gantt-chart-tasks.markdown @@ -0,0 +1,20 @@ +Diagramme de Gantt pour les tâches +====================== + +Le but de ce diagramme de Gantt est de montrer une vue d'ensemble du temps utilisé en fonction de l'ensemble des tâches d'un projet donné. + +- Le diagramme de Gantt est disponible depuis le « sélecteur de vue » +- Seuls les gestionnaires de projet peuvent accéder à cette section + +![Gantt Chart](https://kanboard.net/screenshots/documentation/gantt-chart-project.png) + +- La **date de début** et la **date de fin** des tâches sont utilisées pour créer le graphique +- Les tâches peuvent être redimensionnées et déplacées horizontalement avec votre souris +- Il n'y a pas de glisser-déposer vertical +- La barre est de la même couleur que la tâche +- Chaque barre affiche un niveau de progression en pourcentage, qui est calculé en utilisant la position de la colonne dans le tableau +- Pour correspondre au modèle du Kanban, les tâches peuvent être ordonnées suivant leur position dans le tableau ou suivant les dates de début +- Les nouvelles tâches crées avec cette vue seront affichées sur le tableau en position 1 de la première colonne +- Les tâches sont affichées en noir quand il n'existe ni date de début ni date d'échéance définies + +![Tâche non définie](https://kanboard.net/screenshots/documentation/gantt-chart-not-defined.png) diff --git a/doc/fr_FR/index.markdown b/doc/fr_FR/index.markdown new file mode 100644 index 00000000..f74c3fce --- /dev/null +++ b/doc/fr_FR/index.markdown @@ -0,0 +1,63 @@ +Documentation +============= + +Utiliser Kanboard +----------------- + +### Introduction + +- [Qu'est-ce que Kanban ?](what-is-kanban.markdown) +- [Comparons Kanban aux Todo listes et à Scrum](kanban-vs-todo-and-scrum.markdown) +- [Exemples d'utilisation](usage-examples.markdown) + +### Utiliser un tableau + +- [Vues Tableau, Agenda et Liste](project-views.markdown) +- [Mode Replié et Déplié](board-collapsed-expanded.markdown) +- [Défilement horizontal et mode compact](board-horizontal-scrolling-and-compact-view.markdown) +- [Afficher ou cacher des colonnes dans le tableau](board-show-hide-columns.markdown) + +### Travailler avec les projets + +- [Types de projets](project-types.markdown) +- [Créer des projets](creating-projects.markdown) +- [Modifier des projets](editing-projects.markdown) +- [Partager des tableaux et des tâches](sharing-projects.markdown) +- [Actions automatiques](automatic-actions.markdown) +- [Permissions des projets](project-permissions.markdown) +- [Swimlanes](swimlanes.markdown) +- [Calendriers](calendar.markdown) +- [Analytique](analytics.markdown) +- [Diagramme de Gantt pour les tâches](gantt-chart-tasks.markdown) +- [Diagramme de Gantt pour tous les projets](gantt-chart-projects.markdown) + +### Travailler avec les tâches + +- [Créer des tâches](creating-tasks.markdown) +- [Fermer des tâches](closing-tasks.markdown) +- [Dupliquer et déplacer des tâches](duplicate-move-tasks.markdown) +- [Ajouter des captures d'écran](screenshots.markdown) +- [Liens entre les tâches](task-links.markdown) +- [Transitions](transitions.markdown) +- [Suivi du temps](time-tracking.markdown) +- [Tâches récurrentes](recurring-tasks.markdown) +- [Créer des tâches par email](create-tasks-by-email.markdown) +- [Sous-tâches](subtasks.markdown) +- [Analytique des tâches](analytics-tasks.markdown) + +### Travailler avec les utilisateurs + +- [Rôles](roles.markdown) +- [Gestion des utilisateurs](user-management.markdown) +- [Notifications](notifications.markdown) +- [Authentification à deux facteurs](2fa.markdown) + +### Paramètres + +- [Raccourcis clavier](keyboard-shortcuts.markdown) +- [Paramètres de l'application](application-configuration.markdown) +- [Paramètres du projet](project-configuration.markdown) +- [Paramètres du tableau](board-configuration.markdown) +- [Paramètres du calendrier](calendar-configuration.markdown) +- [Paramètres du lien](link-labels.markdown) +- [Taux de change](currency-rate.markdown) diff --git a/doc/fr_FR/kanban-vs-todo-and-scrum.markdown b/doc/fr_FR/kanban-vs-todo-and-scrum.markdown new file mode 100644 index 00000000..b6f5bc1f --- /dev/null +++ b/doc/fr_FR/kanban-vs-todo-and-scrum.markdown @@ -0,0 +1,36 @@ +Comparons Kanban aux Todo listes et à Scrum +============================== + +Kanban et les Todo listes +-------------------- + +### Todo listes : + +- Une seule phase (une simple liste d'éléments) +- La possibilité de multitâche (moins efficace) + +### Kanban: + +- Multiples phases, chaque colonne représente une étape +- Permet de se concentrer sans se disperser sur de multiples tâches, puisque l'on peut poser une limite au travail en cours par colonne + +Kanban et Scrum +--------------- +### Scrum : + +- Limite les Sprints dans le temps, généralement à 2 ou 4 semaines +- N'accepte pas de modifications pendant l'itération +- Nécessite une estimation +- Utilise la vélocité comme métrique par défaut +- Le tableau Scrum est remis à zéro entre chaque Sprint +- Scrum a des rôles prédéfinis comme Scrum Master, Product Owner et l'équipe +- Beaucoup de réunions : planification, consolidation du backlog, quotidienne, rétrospective + +### Kanban : +- Flux continu +- Des modifications peuvent arriver à n'importe quel moment +- L'estimation est facultative +- Utilise le temps *lead* et *cycle* pour mesurer l'efficacité +- Le tableau Kanban est permanent +- Kanban n'impose aucune contrainte stricte ni de réunion, le processus est plus flexible + diff --git a/doc/fr_FR/keyboard-shortcuts.markdown b/doc/fr_FR/keyboard-shortcuts.markdown new file mode 100644 index 00000000..28a131d8 --- /dev/null +++ b/doc/fr_FR/keyboard-shortcuts.markdown @@ -0,0 +1,37 @@ +Raccourcis clavier +================== + +La disponibilité des raccourcis clavier dépend de la page sur laquelle vous êtes couramment. + +Vues par projets (Tableau, Agenda, Liste, Gantt) +-------------------------------------------- + +- Passer à la vue tableau = **v b** (appuyer sur **v** puis **b**) +- Passer à la vue agenda = **v c** +- Passer à la vue liste = **v l** +- Passer à la vue Gantt = **v g** + +Vue tableau +---------- + +- Nouvelle tâche = **n** +- Étendre / replier une tâche = **s** +- Vue compacte / vue étendue = **c** + +Vue détaillée d'une tâche +------------------------- + +- Modifier une tâche = **e** +- Nouvelle sous-tâche = **s** +- Nouveau commentaire = **c** +- Nouveau lien interne = **l** + +Application +----------- + +- Afficher la liste des raccourcis clavier = **?** +- Ouvrir le changement de tableau = **b** +- Aller au moteur de recherche = **f** +- Restaurer la boîte de recherche = **r** +- Fermer la fenêtre de dialogue = **ESC** +- Soumettre un formulaire = **CTRL+ENTER** ou **⌘+ENTER** diff --git a/doc/fr_FR/link-labels.markdown b/doc/fr_FR/link-labels.markdown new file mode 100644 index 00000000..9c266b5a --- /dev/null +++ b/doc/fr_FR/link-labels.markdown @@ -0,0 +1,13 @@ +Paramètres des liens +============= + +Les relations entre les tâches peuvent être modifiées depuis les paramètres de l'application (**Paramètres > Paramètres des liens**) + +![Libellé des liens](https://kanboard.net/screenshots/documentation/link-labels.png) + +Chaque nom du libellé peut avoir un nom du libellé opposé. + +Si il n'y a pas d'opposé, le nom du libellé sera considéré comme étant bidirectionnel. + +![Création d'un libellé de lien](https://kanboard.net/screenshots/documentation/link-label-creation.png) + diff --git a/doc/fr_FR/notifications.markdown b/doc/fr_FR/notifications.markdown new file mode 100644 index 00000000..43f34a8e --- /dev/null +++ b/doc/fr_FR/notifications.markdown @@ -0,0 +1,45 @@ +Notifications +============= + +Kanboard est capable d'envoyer des notifications via différents canaux : + +- Email +- Web (Liste de message non lus) + +Vous pouvez ajouter d'autres canaux en ajoutant des extensions comme par exemple Hipchat, Slack ou encore Jabber. + +Configuration +-------------- + +Chaque utilisateur doit autoriser les notifications dans son profil : **Profil Utilisateur > Notifications**. Cette option est désactivée par défaut. + +Vous devez, bien sûr, avoir renseigné une adresse email valide dans votre profil et l'application doit être configurée pour envoyer des emails. + +![Notifications](https://kanboard.net/screenshots/documentation/notifications.png) + +Vous pouvez choisir votre méthode favorite de notification : + +- Email +- Web + +Pour chaque projet dont vous êtes membre, vous pouvez choisir de recevoir des notifications pour : + +- Toutes les tâches +- Seulement les tâches qui vous sont assignées +- Seulement les tâches que vous avez créées +- Seulement les tâches que vous avez créées et celles qui vous sont assignées + +Vous pouvez aussi sélectionner certain projets, par défaut tous les projets dont vous êtes membre sont sélectionnés. + +Notifications web +----------------- + +Les notifications web sont accessibles depuis le tableau de bord ou depuis l'icône en haut de la page : + +![Icône des notifications web](https://kanboard.net/screenshots/documentation/web-notifications-icon.png) + +Les notifications sont affichées sous forme de liste. Vous pouvez marquer comme lu chacune d'entre-elle ou toutes en même temps. + +![Notifications web](https://kanboard.net/screenshots/documentation/web-notifications.png) + +Avec cette méthode vous pouvez quand même rester avertis de ce que se passe sans pour autant être inondé d'emails. diff --git a/doc/fr_FR/project-configuration.markdown b/doc/fr_FR/project-configuration.markdown new file mode 100644 index 00000000..22db5bf1 --- /dev/null +++ b/doc/fr_FR/project-configuration.markdown @@ -0,0 +1,42 @@ + +Paramètres du projet +================ + +Aller dans le menu **Préférences**; puis choisissez **Paramètres du projet** sur la gauche + +![Paramètres du projet](https://kanboard.net/screenshots/documentation/project-settings.png) + +###Colonnes par défaut pour les nouveaux projets + +Vous pouvez changer le nom des colonnes par défaut. +C'est utile si vous créez toujours des projets comprenant les même colonnes + +Chaque nom de colonne doit être séparé par une virgule. + +Par défaut, Kanboard utilise les noms de colonne suivants : en attente, prêt, en cours, terminé. + +###Catégories par défaut pour les nouveaux projets + +Les catégories ne sont pas globales à l'application mais rattachées à un projet. +Chaque projet peut avoir plusieurs catégories. + +De plus, si vous créez toujours la même catégorie pour tous vos projets, vous pouvez définir ici la liste des catégories à créer automatiquement + +### Autoriser une seule sous-tâche en cours à la fois pour un utilisateur + +Lorsque cette option est sélectionnée, un utilisateur ne peut travailler que sur une seule sous-tâche à la fois + +Si une autre sous-tâche possède le statut « en cours », l'utilisateur verra cette boite de dialogue : + +![Limite des sous-tâches pour l'utilisateur](https://kanboard.net/screenshots/documentation/subtask-user-restriction.png) + +### Déclencher automatiquement le suivi du temps pour les sous-tâches + +- Si activé, lorsque le statut d'une sous-tâche devient « en cours », le chrono va démarrer automatiquement +- Désactivez cette option si vous n'utilisez pas le suivi du temps. + +### Inclure les tâches fermées dans le diagramme de flux cumulé + +- Si l'option est activée, les tâches fermées seront incluses dans le diagramme de flux cumulé +- Si l'option est désactivée, seules les tâches ouvertes seront incluses dans le diagramme de flux cumulé +- Cette option affecte la colonne "total" de la table "project_daily_column_stats" diff --git a/doc/fr_FR/project-permissions.markdown b/doc/fr_FR/project-permissions.markdown new file mode 100644 index 00000000..c4ef4df4 --- /dev/null +++ b/doc/fr_FR/project-permissions.markdown @@ -0,0 +1,22 @@ +Permissions des projets +======================= + +Chaque projet est isolé des autres. +Les accès au projet doivent être autorisés par le chef de projet. + +Chaque utilisateur et chaque groupe peut avoir un rôle différent. +Il y a 3 types de [rôles pour les projets](roles.markdown) : + +- Chef de projet +- Membre du projet +- Visualiseur + +L'assignation des rôles est disponible depuis **Paramètres du projet > Permissions**: + +![Permissions du projet](screenshots/project-permissions.png) + +Si vous choisissez d'autoriser tout le monde, tous les utilisateurs de Kanboard seront considérés comme **Membre du projet**. +Ce qui signifie qu'il n'y a plus des gestion de rôles. +Les permissions par utilisateur ou par groupe ne peuvent plus être appliquées. + +Les projets privés ne peuvent pas définir de permissions. diff --git a/doc/fr_FR/project-types.markdown b/doc/fr_FR/project-types.markdown new file mode 100644 index 00000000..70434ec8 --- /dev/null +++ b/doc/fr_FR/project-types.markdown @@ -0,0 +1,14 @@ +Types de projets +================ + +Il y a deux types de projets : + +| Type | Description | +|-------------------|-------------------------------------------------------------------------------------| +| Projet d'équipe | La gestion des utilisateurs est activée | +| Projet privé | Projet qui appartient à une seule personne, il n'y a pas de gestion d'utilisateurs | + +- Seulement les administrateurs et les gestionnaires peuvent créer des projets d'équipe. +- Les projets privés peuvent être créé par tout le monde. + +[Lire la documentation à propos des rôles dans Kanboard](roles.markdown) diff --git a/doc/fr_FR/project-views.markdown b/doc/fr_FR/project-views.markdown new file mode 100644 index 00000000..603108f6 --- /dev/null +++ b/doc/fr_FR/project-views.markdown @@ -0,0 +1,61 @@ +Vues Tableau, Agenda et Liste +============================= + +Pour chaque projet, les tâches peuvent être visualisées dans différentes vues : **Tableau, Agenda, Liste ou Gantt**. +Chaque vue affiche le résultat filtré par le champ de recherche en haut de page. +Le moteur de recherche utilise la [syntaxe avancée](search.markdown). + +Vue Tableau +----------- + +![Vue Tableau](screenshots/board-view.png) + +- Dans cette vue, il est possible de glisser-déposer facilement des tâches d'une colonne à l'autre. +- Il est également possible d'utiliser le raccourci clavier **« v b »** pour afficher la vue Tableau. +- Les tâches avec une ombre ont été modifiées récemment. + +![Tableau Limite de tâches](screenshots/board-task-limit.png) + +Lorsque la limite de tâches est atteinte pour une colonne, l'arrière-plan devient rouge. +Ce qui signifie qu'il y a trop de tâches en cours en même temps. + +[En apprendre plus sur la configuration du Tableau](board-configuration.markdown) + +Vue Agenda +---------- + +![Vue Agenda](screenshots/calendar-view.png) + +- Dans cette vue, il est possible de voir les tâches avec des dates d'échéance. +- Selon les paramètres, il est également possible de voir les tâches en cours. +- Il est également possible d'utiliser le raccourci clavier **« v c »** pour afficher la vue Agenda. +- [En apprendre plus sur la configuration de l'Agenda](calendar-configuration.markdown) + +Vue Liste +--------- + +![Vue liste](screenshots/list-view.png) + +- Dans cette vue, tous les résultats de votre recherche sont affichés dans un tableau. +- Il est également possible d'utiliser le raccourci clavier **« v l »** pour afficher la vue Liste. + +Vue Gantt +--------- + +![Vue Gantt](screenshots/gantt-view.png) + +- La vue Gantt affiche les tâches dans une fresque horizontale +- Le diagramme utilise la date de début et la date d'échéance pour afficher les tâches +- Il est également possible d'utiliser le raccourci clavier **« v g »** pour afficher la vue Gantt. + +Aperçu du projet +---------------- + +![Aperçu du projet](screenshots/project-view.png) + +Ce mode permet d'afficher une vue d'ensemble du projet : + +- Vous pouvez voir la description du projet +- Attacher et visualiser des pièces-jointes au projet +- Visualiser la liste des membres +- Voir les dernières activités du projet diff --git a/doc/fr_FR/recurring-tasks.markdown b/doc/fr_FR/recurring-tasks.markdown new file mode 100644 index 00000000..95f24c40 --- /dev/null +++ b/doc/fr_FR/recurring-tasks.markdown @@ -0,0 +1,24 @@ +Tâches récurrentes +=============== + +Pour convenir à ma méthodologie de Kanban, les tâches récurrentes ne sont pas basées sur une date mais sur les évènements du tableau. + +- Les tâches récurrentes sont dupliquées dans la première colonne du tableau quand les évènements sélectionnés se produisent +- La date d'échéance peut être automatiquement recalculée +- Chaque tâche enregistre l'identifiant de tâche de la tâche parente qui l'a créée et la tâche enfant qui a été créée. + +Configuration +------------- + +Allez à la page de vue par tâches ou utilisez le menu déroulant du tableau, puis choisissez **Modifier la récurrence**. + +![Tâche récurrente](https://kanboard.net/screenshots/documentation/recurring-tasks.png) + +il existe trois façons de déclencher la création d'une nouvelle tâche récurrente : + +- Déplacer une tâche depuis la première colonne +- Déplacer une tâche vers la dernière colonne +- Fermer la tâche + +Les dates d'échéance, si elles concernent la tâche courante, peuvent être recalculées en fonction d'un nombre donné de jours, mois ou années. +La date de base pour le calcul de la nouvelle date d'échéance peut être soit la date d'échéance existante, soit la date de l'action. diff --git a/doc/fr_FR/roles.markdown b/doc/fr_FR/roles.markdown new file mode 100644 index 00000000..e55a3969 --- /dev/null +++ b/doc/fr_FR/roles.markdown @@ -0,0 +1,24 @@ +Rôles des utilisateurs +====================== + +Rôles au niveau de l'application +-------------------------------- + +Chaque utilisateur possède un de ces rôles : + +| Rôle | Description | +|----------------|----------------------------------------------------------------------------------------| +| Administrateur | Accès à tout | +| Gestionnaire | Peut créer des projets d'équipe mais ne peut pas changer les réglages de l'application | +| Utilisateur | Peut créer des projets privés | + +Rôles au niveau des projets +--------------------------- + +Chaque membre d'un projet peut avoir un rôle différent : + +| Rôle | Description | +|------------------------|----------------------------------------------------------------------| +| Chef de projet | Peut changer les paramètres du projet, accéder aux rapports | +| Membre du projet | Peut créer des tâches et utiliser le tableau Kanban | +| Visualiseur de projet | Accès en lecture seule au projet | diff --git a/doc/fr_FR/screenshots.markdown b/doc/fr_FR/screenshots.markdown new file mode 100644 index 00000000..e634bd1b --- /dev/null +++ b/doc/fr_FR/screenshots.markdown @@ -0,0 +1,26 @@ +Ajouter des captures d'écran +================== + +Vous pouvez copier-coller des images directement dans Kanboard pour gagner du temps. +Ces images sont mises en ligne en tant que pièces jointes à une tâche. + +Ceci est particulièrement utile pour prendre des captures d'écran, quand il faut par exemple décrire un problème. + +Vous pouvez ajouter directement des captures depuis le tableau en cliquant sur le menu déroulant ou sur la page de visualisation des tâches. + +![La capture d'écran dans le menu déroulant](https://kanboard.net/screenshots/documentation/dropdown-screenshot.png) + +Pour ajouter une nouvelle image, prenez votre capture et collez-la avec CTRL+V ou Command+V: + +![Page de capture](https://kanboard.net/screenshots/documentation/task-screenshot.png) + +Avec Mac OS X, vous pouvez utiliser les raccourcis suivants pour prendre des captures d'écran : + +- Command-Control-Maj-3 : prend une capture de l'écran entier et l'enregistre dans le presse-papiers +- Command-Control-Maj-4, puis choix d'une zone : prend une capture d'une zone définie et l'enregistre dans le presse-papiers +- Command-Control-Maj-4, puis touche espace, puis clic sur une fenêtre : prend une capture d'une fenêtre et l'enregistre dans le presse-papiers + +Il existe plusieurs applications tierces qui peuvent être utilisées pour prendre des captures d'écran avec des annotations et un choix de formes. + +**Remarque : cette fonctionnalité n'est pas disponible sur tous les navigateurs.** Elle n'existe pas pour Safari en raison de ce bug : https://bugs.webkit.org/show_bug.cgi?id=49141 + diff --git a/doc/fr_FR/screenshots/automatic-action-creation.png b/doc/fr_FR/screenshots/automatic-action-creation.png new file mode 100644 index 00000000..ad90590d Binary files /dev/null and b/doc/fr_FR/screenshots/automatic-action-creation.png differ diff --git a/doc/fr_FR/screenshots/board-collapsed-mode.png b/doc/fr_FR/screenshots/board-collapsed-mode.png new file mode 100644 index 00000000..a496faff Binary files /dev/null and b/doc/fr_FR/screenshots/board-collapsed-mode.png differ diff --git a/doc/fr_FR/screenshots/board-compact-mode.png b/doc/fr_FR/screenshots/board-compact-mode.png new file mode 100644 index 00000000..872ceae5 Binary files /dev/null and b/doc/fr_FR/screenshots/board-compact-mode.png differ diff --git a/doc/fr_FR/screenshots/board-expanded-mode.png b/doc/fr_FR/screenshots/board-expanded-mode.png new file mode 100644 index 00000000..19f61451 Binary files /dev/null and b/doc/fr_FR/screenshots/board-expanded-mode.png differ diff --git a/doc/fr_FR/screenshots/board-task-limit.png b/doc/fr_FR/screenshots/board-task-limit.png new file mode 100644 index 00000000..8353f33c Binary files /dev/null and b/doc/fr_FR/screenshots/board-task-limit.png differ diff --git a/doc/fr_FR/screenshots/board-view.png b/doc/fr_FR/screenshots/board-view.png new file mode 100644 index 00000000..0d1e18ea Binary files /dev/null and b/doc/fr_FR/screenshots/board-view.png differ diff --git a/doc/fr_FR/screenshots/calendar-view.png b/doc/fr_FR/screenshots/calendar-view.png new file mode 100644 index 00000000..1226162b Binary files /dev/null and b/doc/fr_FR/screenshots/calendar-view.png differ diff --git a/doc/fr_FR/screenshots/gantt-view.png b/doc/fr_FR/screenshots/gantt-view.png new file mode 100644 index 00000000..3caafa98 Binary files /dev/null and b/doc/fr_FR/screenshots/gantt-view.png differ diff --git a/doc/fr_FR/screenshots/hide-column.png b/doc/fr_FR/screenshots/hide-column.png new file mode 100644 index 00000000..61015f9a Binary files /dev/null and b/doc/fr_FR/screenshots/hide-column.png differ diff --git a/doc/fr_FR/screenshots/list-view.png b/doc/fr_FR/screenshots/list-view.png new file mode 100644 index 00000000..c40e807a Binary files /dev/null and b/doc/fr_FR/screenshots/list-view.png differ diff --git a/doc/fr_FR/screenshots/new-project.png b/doc/fr_FR/screenshots/new-project.png new file mode 100644 index 00000000..42e5f196 Binary files /dev/null and b/doc/fr_FR/screenshots/new-project.png differ diff --git a/doc/fr_FR/screenshots/new-user.png b/doc/fr_FR/screenshots/new-user.png new file mode 100644 index 00000000..116e9074 Binary files /dev/null and b/doc/fr_FR/screenshots/new-user.png differ diff --git a/doc/fr_FR/screenshots/project-disable-sharing.png b/doc/fr_FR/screenshots/project-disable-sharing.png new file mode 100644 index 00000000..58832045 Binary files /dev/null and b/doc/fr_FR/screenshots/project-disable-sharing.png differ diff --git a/doc/fr_FR/screenshots/project-edition.png b/doc/fr_FR/screenshots/project-edition.png new file mode 100644 index 00000000..ce8594fe Binary files /dev/null and b/doc/fr_FR/screenshots/project-edition.png differ diff --git a/doc/fr_FR/screenshots/project-enable-sharing.png b/doc/fr_FR/screenshots/project-enable-sharing.png new file mode 100644 index 00000000..147ccc53 Binary files /dev/null and b/doc/fr_FR/screenshots/project-enable-sharing.png differ diff --git a/doc/fr_FR/screenshots/project-permissions.png b/doc/fr_FR/screenshots/project-permissions.png new file mode 100644 index 00000000..54f38690 Binary files /dev/null and b/doc/fr_FR/screenshots/project-permissions.png differ diff --git a/doc/fr_FR/screenshots/project-view.png b/doc/fr_FR/screenshots/project-view.png new file mode 100644 index 00000000..ff9a7f76 Binary files /dev/null and b/doc/fr_FR/screenshots/project-view.png differ diff --git a/doc/fr_FR/screenshots/show-column.png b/doc/fr_FR/screenshots/show-column.png new file mode 100644 index 00000000..51f78ac8 Binary files /dev/null and b/doc/fr_FR/screenshots/show-column.png differ diff --git a/doc/fr_FR/screenshots/swimlane-configuration.png b/doc/fr_FR/screenshots/swimlane-configuration.png new file mode 100644 index 00000000..d0b25e9c Binary files /dev/null and b/doc/fr_FR/screenshots/swimlane-configuration.png differ diff --git a/doc/fr_FR/screenshots/swimlanes.png b/doc/fr_FR/screenshots/swimlanes.png new file mode 100644 index 00000000..e24a5b85 Binary files /dev/null and b/doc/fr_FR/screenshots/swimlanes.png differ diff --git a/doc/fr_FR/sharing-projects.markdown b/doc/fr_FR/sharing-projects.markdown new file mode 100644 index 00000000..f3db3c68 --- /dev/null +++ b/doc/fr_FR/sharing-projects.markdown @@ -0,0 +1,35 @@ +Partager des tableaux et des tâches +=================================== + +Par défaut, les tableaux sont privés, mais il est possible de rendre un tableau public. + +Un tableau public ne **peut pas être modifié, il est en lecture seule**. +Son accès est protégé par un jeton aléatoire, seules les personnes qui ont la bonne URL peuvent voir le tableau. + +Les tableaux publics sont automatiquement réactualisés toutes les minutes. +Les détails des tâches sont disponibles en lecture seule. + +Exemples d'utilisation : + +- Partager son tableau avec quelqu'un qui ne fait pas partie de votre organisation / entreprise / groupe +- Afficher le tableau sur un grand écran dans votre bureau + +Activer l'accès public +---------------------- + +Choisissez votre projet, puis cliquez sur « Accès public » et enfin sur le bouton « Activer l'accès public ». + +![Activer l'accès public](screenshots/project-enable-sharing.png) + +Lorsque l'accès public est activé, plusieurs liens sont créés : + +- Affichage du tableau public +- Lien de souscription au fil RSS +- Lien d'abonnement à iCalendar + +![Désactiver l'accès public](screenshots/project-disable-sharing.png) + +Vous pouvez désactiver l'accès public à tout moment. + +À chaque fois que vous activez ou désactivez l'accès public, un nouveau jeton aléatoire est créé. +**Les liens précédents ne fonctionneront pas**. diff --git a/doc/fr_FR/subtasks.markdown b/doc/fr_FR/subtasks.markdown new file mode 100644 index 00000000..02345c2a --- /dev/null +++ b/doc/fr_FR/subtasks.markdown @@ -0,0 +1,43 @@ +Sous-tâches +======== + +Les sous-tâches sont utiles pour se partager le travail que représente une tâche. + +Chaque sous-tâche : + +- peut être assignée à un membre du projet +- a trois différents statuts : **À faire**, **En cours**, **Terminé** +- dispose d'informations sur le temps de travail : **temps passé** et **temps estimé** +- est classée en fonction de sa position + +Créer des sous-tâches +----------------- + +Depuis la vue par tâche, cliquez sur **Ajouter une sous-tâche** dans le panneau latéral. + +![Ajouter une sous-tâche](https://kanboard.net/screenshots/documentation/add-subtask.png) + +Vous pouvez aussi ajouter rapidement une sous-tâche en saisissant seulement son titre : + +![Add a subtask from the task view](https://kanboard.net/screenshots/documentation/add-subtask-shortcut.png) + +Modifier le statut d'une sous-tâche +--------------------- + +Quand vous cliquez sur le titre d'une sous-tâche son statut change : + +![Sous-tâche en cours](https://kanboard.net/screenshots/documentation/subtask-status-inprogress.png) + +L'icône devant le titre est mise à jour en fonction du statut. + +![Sous-tâche effectuée](https://kanboard.net/screenshots/documentation/subtask-status-done.png) + +Remarque : quand la tâche est fermée, toutes les sous-tâches voient leur statut passer à **Terminé**. + +Chrono des sous-tâches +------------- + +- À chaque fois qu'une sous-tâche est en cours de réalisation, le chronomètre est également démarré. Il peut être lancé et interrompu à tout moment. +- Le chronomètre enregistre automatiquement le temps passé sur la sous-tâche. Vous pouvez aussi modifier manuellement la valeur du temps passé dans le champ adéquat quand vous modifiez une sous-tâche. +- Le temps passé est arrondi au quart d'heure le plus proche. Cette information est enregistrée dans un tableau distinct. +- Le temps passé à la tâche ainsi que le temps estimé sont automatiquement mis à jour en fonction de la somme de toutes les sous-tâches. diff --git a/doc/fr_FR/swimlanes.markdown b/doc/fr_FR/swimlanes.markdown new file mode 100644 index 00000000..92b4a9fa --- /dev/null +++ b/doc/fr_FR/swimlanes.markdown @@ -0,0 +1,29 @@ +Swimlanes +========= + +Les *swimlanes* sont des séparations horizontales de votre tableau (pensez à des « couloirs » ou « lignes d'eau » dans une piscine). + +Par exemple, cela peut servir à séparer les sorties des différentes versions d'un logiciel, à diviser vos tâches selon différents produits, équipes ou tout autre critère de votre choix. + +Tableau avec des swimlanes +-------------------------- + +![Swimlanes](screenshots/swimlanes.png) + +Gestion des swimlanes +------------------ + +- Tous les projets ont une swimlane par défaut. +- S'il existe plus d'une swimlane, le tableau les affichera toutes. +- Vous pouvez glisser-déposer les tâches d'une swimlane à l'autre. + +Pour configurer les swimlanes allez sur la page de **Configuration du projet** et choisissez la section **Swimlanes**. + +![Swimlanes Configuration](screenshots/swimlane-configuration.png) + +À partir de cet endroit, vous pouvez ajouter une nouvelle swimlane ou renommer celle qui existe par défaut. +Vous pouvez aussi désactiver et modifier la position des diverses swimlanes. + +- La swimlane par défaut est toujours en haut de tableau mais vous pouvez la cacher. +- Les swimlanes inactives ne sont pas affichées dans le tableau. +- **Supprimer une swimlane ne supprime pas les tâches qui lui sont assignées**, ces tâches seront transférées à la swimlane par défaut. diff --git a/doc/fr_FR/task-links.markdown b/doc/fr_FR/task-links.markdown new file mode 100644 index 00000000..f2756ac7 --- /dev/null +++ b/doc/fr_FR/task-links.markdown @@ -0,0 +1,22 @@ +Liens entre les tâches +========== + +Les tâches peuvent être liées ensemble avec des relations prédéfinies. + +![Task Links](https://kanboard.net/screenshots/documentation/task-links.png) + +Les relations établies par défaut sont les suivantes : + +- **fait référence à** +- **bloque** | est bloqué par +- **est bloqué par** | bloque +- **duplique** | est dupliqué par +- **est dupliqué par** | duplique +- **est un enfant de** | est un parent de +- **est un parent de** | est un enfant de +- **vise les étapes importantes** | est une étape importante de +- **est une étape importante de** | vise les étapes importantes +- **correctifs** | est réglé par +- **est réglé par** | correctifs + +Ces étiquettes peuvent être modifiées dans les paramètres de l'application. diff --git a/doc/fr_FR/time-tracking.markdown b/doc/fr_FR/time-tracking.markdown new file mode 100644 index 00000000..625bc26f --- /dev/null +++ b/doc/fr_FR/time-tracking.markdown @@ -0,0 +1,44 @@ +Suivi du temps +============= + +Les informations de la feuille de suivi du temps peuvent être définies au niveau des tâches ou des sous-tâches + +Suivi de temps des tâches +------------------ + +![Suivi de temps des tâches ](https://kanboard.net/screenshots/documentation/task-time-tracking.png) + +Les tâches ont deux champs: + +- Temps estimé +- Temps passé + +Ces valeurs représentent des heures de travail et doivent être entrées manuellement. + + +Suivi de temps des sous-tâches +--------------------- + +![Suivi de temps des sous-tâches](https://kanboard.net/screenshots/documentation/subtask-time-tracking.png) + +Les sous-tâches ont aussi les champs "temps passé" et "temps estimé" + +Lorsque vous changez la valeur de ces champs, **le suivi des tâches est mis à jour automatiquement et devient la somme des sous-tâches**. + +Kanboard enregistre le temps entre chaque changement de statut des sous-tâches dans une table séparée + +- Changer le statut de la sous-tâche de **à faire** à **en cours** marque le temps de début +- Changer le statut de la sous-tâche de **en cours** à **à faire** marque le temps de fin mais aussi met à jour le temps passé sur la sous-tâche et la tâche + +La répartition de tous les enregistrements est visible sur la page de la tâche + +![Feuille de suivi du temps pour les tâches](https://kanboard.net/screenshots/documentation/task-timesheet.png) + +Pour chaque sous-tâche, le chrono peut être à tout moment arrêté/démarré + +![Chrono des sous-tâches](https://kanboard.net/screenshots/documentation/subtask-timer.png) + +- Le chrono ne dépend pas du statut de la sous-tâche +- Chaque fois que vous démarrez le chrono, un nouvel enregistrement est créé dans la table de suivi des temps +- Chaque fois que vous arrêtez l'horloge, la date de fin est enregistrée dans la table de suivi des temps +- Le temps passé est arrondi au quart d’heure le plus proche diff --git a/doc/fr_FR/transitions.markdown b/doc/fr_FR/transitions.markdown new file mode 100644 index 00000000..94a14bbc --- /dev/null +++ b/doc/fr_FR/transitions.markdown @@ -0,0 +1,20 @@ +Transitions entre les tâches +================ + +Les transitions enregistrent tous les mouvements des tâches entre les colonnes + +![Transitions](https://kanboard.net/screenshots/documentation/transitions.png) + +Depuis la page des tâches, vous pouvez accéder à ces informations: + +- Date de l'action +- Colonne d'origine +- Colonne de destination +- Exécutant (Pour l'utilisateur qui a déplacé la tâche) +- Temps passé sur la colonne d’origine + +Les données de transition entre les tâches peuvent aussi être exportées depuis la page des paramètres du projet + +![Transitions Export](https://kanboard.net/screenshots/documentation/transitions-export.png) + +Pour la période spécifiée, vous allez générer un fichier CSV que vous pouvez utiliser avec n’importe quel tableur diff --git a/doc/fr_FR/usage-examples.markdown b/doc/fr_FR/usage-examples.markdown new file mode 100644 index 00000000..b91fa613 --- /dev/null +++ b/doc/fr_FR/usage-examples.markdown @@ -0,0 +1,69 @@ +Exemples d'utilisation +============== +Il est possible de personnaliser ses tableaux selon l'activité de votre entreprise : + +Développement logiciel +-------------------- + +- Prévu +- Prêt +- En cours +- À valider +- Validé +- En production + +Suivi de bogues +------------ + +- Rapporté +- Confirmé +- En cours +- Testé +- Résolu + +Ventes +----- + +- Objectifs +- Réunions +- Propositions +- Achats + +Gestion au plus juste +------------------------ + +- Idées +- Expression de la demande +- Étude de marché +- Analyses +- Fait + + +Procédure de recrutement +------------------ + +- Offres d'emploi +- Candidats +- Appels téléphoniques +- Entretiens +- Embauches + +Boutiques en ligne +------------ + +- Commande +- Empaquetage +- Prêt à envoyer +- Envoyé + +Artisanat +----------- + +- Commande +- Assemblage +- Tests +- Empaquetage +- Prêt à envoyer +- Envoyé + + diff --git a/doc/fr_FR/user-management.markdown b/doc/fr_FR/user-management.markdown new file mode 100644 index 00000000..bb9b0731 --- /dev/null +++ b/doc/fr_FR/user-management.markdown @@ -0,0 +1,35 @@ +Gestion des utilisateurs +======================== + +Ajouter un nouvel utilisateur +----------------------------- + +Pour ajouter un nouvel utilisateur, vous devez être administrateur. + +1. Depuis le menu déroulant situé en haut à droite, cliquez sur **Gestion des utilisateurs** +2. Dans la partie haute vous avez un lien **Créer un utilisateur local** ou **Créer un utilisateur distant** +3. Informez les champs de saisie et enregistrez + +![Nouvel utilisateur](screenshots/new-user.png) + +Quand vous créez un **utilisateur local**, vous devez préciser au moins deux valeurs : + +- **nom d'utilisateur** : c'est l'identifiant unique de votre utilisateur (login) +- **mot de passe** : le mot de passe de votre utilisateur doit comporter au moins 6 caractères + +Pour les **utilisateurs distants**, seul le nom d'utilisateur est obligatoire. + +Modifier des utilisateurs +------------------------- + +Quand vous allez au menu **utilisateurs**, vous disposez d'une liste d'utilisateurs. Pour modifier un utilisateur cliquez sur le lien **Modifier**. + +- si vous êtes un utilisateur ordinaire, vous ne pouvez modifier que votre propre profil +- vous devez être administrateur pour pouvoir modifier n'importe quel utilisateur + +Supprimer des utilisateurs +-------------------------- + +Depuis le menu **utilisateurs**, cliquez sur le lien **supprimer**. Ce lien n'est visible que si vous êtes administrateur. + +Si vous supprimez un utilisateur particulier, **les tâches assignées à cette personne ne lui seront plus assignées** après cette opération. diff --git a/doc/fr_FR/what-is-kanban.markdown b/doc/fr_FR/what-is-kanban.markdown new file mode 100644 index 00000000..f479927c --- /dev/null +++ b/doc/fr_FR/what-is-kanban.markdown @@ -0,0 +1,34 @@ +Qu'est-ce que Kanban? +=============== + + +Kanban est une méthodologie développée à l'origine par l'entreprise Toyota pour gagner en efficacité. + +Kanban n'impose que deux contraintes : + +- Visualiser votre flux d'activité +- Limiter votre travail en cours + +Visualiser votre flux d'activité +----------------------- + +- Votre activité est affichée sur un tableau, vous disposez ainsi d'une vue très nette sur l'ensemble de votre projet +- Chaque colonne représente une étape de votre flux d'activité + +Se concentrer sur une seule tâche à la fois sans disperser son activité +---------------------------------- + +- Chaque phase peut avoir sa date d'échéance +- Les limites fixées sont très utiles pour identifier les goulots d'étranglement +- Les limites évitent de travailler à un trop grand nombre de tâches à la fois + +Mesure des performances et des progrès +----------------------------------- + +Kanban utilise lead time et cycle times pour mesurer les performances : + +- **Lead time** : le *lead time* est la durée entre la création de la tâche et son achèvement. +- **Cycle time** : le *cycle time* est la durée entre la date de début et l'achèvement. + +Par exemple, vous pouvez avoir un *lead time* de 100 jours et n'avoir à travailler qu'une heure pour achever la tâche. + -- cgit v1.2.3 From b2e92480c29acb15586bc8ea305c8416927a667c Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 24 Jun 2016 11:40:58 -0400 Subject: Added filter class for tags --- app/Filter/TaskTagFilter.php | 74 +++++++++++++++++++ app/Model/TaskTagModel.php | 20 ++--- app/ServiceProvider/FilterProvider.php | 4 + doc/search.markdown | 6 ++ tests/units/Base.php | 4 +- tests/units/Core/Filter/LexerTest.php | 12 +++ tests/units/Filter/TaskTagFilterTest.php | 121 +++++++++++++++++++++++++++++++ tests/units/Model/TaskTagModelTest.php | 14 ++++ 8 files changed, 243 insertions(+), 12 deletions(-) create mode 100644 app/Filter/TaskTagFilter.php create mode 100644 tests/units/Filter/TaskTagFilterTest.php (limited to 'doc') diff --git a/app/Filter/TaskTagFilter.php b/app/Filter/TaskTagFilter.php new file mode 100644 index 00000000..01b6f625 --- /dev/null +++ b/app/Filter/TaskTagFilter.php @@ -0,0 +1,74 @@ +db = $db; + return $this; + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $task_ids = $this->db + ->table(TagModel::TABLE) + ->ilike(TagModel::TABLE.'.name', $this->value) + ->asc(TagModel::TABLE.'.project_id') + ->join(TaskTagModel::TABLE, 'tag_id', 'id') + ->findAllByColumn(TaskTagModel::TABLE.'.task_id'); + + if (empty($task_ids)) { + $task_ids = array(-1); + } + + $this->query->in(TaskModel::TABLE.'.id', $task_ids); + + return $this; + } +} diff --git a/app/Model/TaskTagModel.php b/app/Model/TaskTagModel.php index 3dd1dd88..91dfd224 100644 --- a/app/Model/TaskTagModel.php +++ b/app/Model/TaskTagModel.php @@ -74,9 +74,9 @@ class TaskTagModel extends Base * Add or update a list of tags to a task * * @access public - * @param integer $project_id - * @param integer $task_id - * @param string[] $tags + * @param integer $project_id + * @param integer $task_id + * @param string[] $tags * @return boolean */ public function save($project_id, $task_id, array $tags) @@ -123,10 +123,10 @@ class TaskTagModel extends Base * Associate missing tags * * @access protected - * @param integer $project_id - * @param integer $task_id - * @param array $task_tags - * @param array $tags + * @param integer $project_id + * @param integer $task_id + * @param array $task_tags + * @param string[] $tags * @return bool */ protected function associateTags($project_id, $task_id, $task_tags, $tags) @@ -146,9 +146,9 @@ class TaskTagModel extends Base * Dissociate removed tags * * @access protected - * @param integer $task_id - * @param array $task_tags - * @param array $tags + * @param integer $task_id + * @param array $task_tags + * @param string[] $tags * @return bool */ protected function dissociateTags($task_id, $task_tags, $tags) diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index cdef9ed8..20281a09 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -26,6 +26,7 @@ use Kanboard\Filter\TaskReferenceFilter; use Kanboard\Filter\TaskStatusFilter; use Kanboard\Filter\TaskSubtaskAssigneeFilter; use Kanboard\Filter\TaskSwimlaneFilter; +use Kanboard\Filter\TaskTagFilter; use Kanboard\Filter\TaskTitleFilter; use Kanboard\Model\ProjectModel; use Kanboard\Model\ProjectGroupRoleModel; @@ -163,6 +164,9 @@ class FilterProvider implements ServiceProviderInterface ->setDatabase($c['db']) ) ->withFilter(new TaskSwimlaneFilter()) + ->withFilter(TaskTagFilter::getInstance() + ->setDatabase($c['db']) + ) ->withFilter(new TaskTitleFilter(), true) ; diff --git a/doc/search.markdown b/doc/search.markdown index 37bb8625..ab8e0b5a 100644 --- a/doc/search.markdown +++ b/doc/search.markdown @@ -152,6 +152,12 @@ Attribute: **comment** - Find comments that contains this title: `comment:"My comment message"` +### Search by tags + +Attribute: **tag** + +- Example: `tag:"My tag"` + Activity stream search ---------------------- diff --git a/tests/units/Base.php b/tests/units/Base.php index f7bee241..9dbfb280 100644 --- a/tests/units/Base.php +++ b/tests/units/Base.php @@ -48,8 +48,8 @@ abstract class Base extends PHPUnit_Framework_TestCase new Stopwatch ); - $this->container['db']->logQueries = true; - $this->container['logger'] = new Logger; + $this->container['db']->getStatementHandler()->withLogging(); + $this->container['logger'] = new Logger(); $this->container['httpClient'] = $this ->getMockBuilder('\Kanboard\Core\Http\Client') diff --git a/tests/units/Core/Filter/LexerTest.php b/tests/units/Core/Filter/LexerTest.php index c72231c4..b777531d 100644 --- a/tests/units/Core/Filter/LexerTest.php +++ b/tests/units/Core/Filter/LexerTest.php @@ -202,4 +202,16 @@ class LexerTest extends Base $this->assertSame($expected, $lexer->tokenize('६Δↈ五一')); } + + public function testTokenizeWithMultipleValues() + { + $lexer = new Lexer(); + $lexer->addToken("/^(tag:)/", 'T_TAG'); + + $expected = array( + 'T_TAG' => array('tag 1', 'tag2'), + ); + + $this->assertSame($expected, $lexer->tokenize('tag:"tag 1" tag:tag2')); + } } diff --git a/tests/units/Filter/TaskTagFilterTest.php b/tests/units/Filter/TaskTagFilterTest.php new file mode 100644 index 00000000..a36d3475 --- /dev/null +++ b/tests/units/Filter/TaskTagFilterTest.php @@ -0,0 +1,121 @@ +container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + $query = $taskFinderModel->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test2'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test3'))); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); + $this->assertTrue($taskTagModel->save(1, 2, array('My tag 3'))); + + $filter = new TaskTagFilter(); + $filter->setDatabase($this->container['db']); + $filter->withQuery($query); + $filter->withValue('my tag 3'); + $filter->apply(); + + $tasks = $query->findAll(); + $this->assertCount(2, $tasks); + $this->assertEquals('test1', $tasks[0]['title']); + $this->assertEquals('test2', $tasks[1]['title']); + } + + public function testWithSingleMatch() + { + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + $query = $taskFinderModel->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test2'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test3'))); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); + $this->assertTrue($taskTagModel->save(1, 2, array('My tag 3'))); + + $filter = new TaskTagFilter(); + $filter->setDatabase($this->container['db']); + $filter->withQuery($query); + $filter->withValue('my tag 2'); + $filter->apply(); + + $tasks = $query->findAll(); + $this->assertCount(1, $tasks); + $this->assertEquals('test1', $tasks[0]['title']); + } + + public function testWithNoMatch() + { + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + $query = $taskFinderModel->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test2'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test3'))); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); + $this->assertTrue($taskTagModel->save(1, 2, array('My tag 3'))); + + $filter = new TaskTagFilter(); + $filter->setDatabase($this->container['db']); + $filter->withQuery($query); + $filter->withValue('my tag 42'); + $filter->apply(); + + $tasks = $query->findAll(); + $this->assertCount(0, $tasks); + } + + public function testWithSameTagInMultipleProjects() + { + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + $query = $taskFinderModel->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 2, 'title' => 'test2'))); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag'))); + $this->assertTrue($taskTagModel->save(2, 2, array('My tag'))); + + $filter = new TaskTagFilter(); + $filter->setDatabase($this->container['db']); + $filter->withQuery($query); + $filter->withValue('my tag'); + $filter->apply(); + + $tasks = $query->findAll(); + $this->assertCount(2, $tasks); + $this->assertEquals('test1', $tasks[0]['title']); + $this->assertEquals('test2', $tasks[1]['title']); + } +} diff --git a/tests/units/Model/TaskTagModelTest.php b/tests/units/Model/TaskTagModelTest.php index 819f55b8..73bbeac1 100644 --- a/tests/units/Model/TaskTagModelTest.php +++ b/tests/units/Model/TaskTagModelTest.php @@ -110,4 +110,18 @@ class TaskTagModelTest extends Base $this->assertEquals($expected, $tags); } + + public function testGetTagsForTasksWithEmptyList() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1'))); + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); + + $tags = $taskTagModel->getTagsByTasks(array()); + $this->assertEquals(array(), $tags); + } } -- cgit v1.2.3 From 3e1a48113cc9a48fd737eece23e3beb3c6c84c20 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 24 Jun 2016 18:15:31 -0400 Subject: Update list of hooks --- doc/plugin-hooks.markdown | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'doc') diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index a21157e5..1f90bdbc 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -182,8 +182,9 @@ List of template hooks: | `template:task:dropdown` | Task dropdown menu in listing pages | | `template:task:sidebar:actions` | Sidebar on task page (section actions) | | `template:task:sidebar:information` | Sidebar on task page (section information) | -| `template:task:form:left-column` | Left column in task form | -| `template:task:form:right-column` | Right column in task form | +| `template:task:form:first-column` | 1st column in task form | +| `template:task:form:second-column` | 2nd column in task form | +| `template:task:form:third-column` | 3nd column in task form | | `template:task:show:top ` | Show task page: top | | `template:task:show:bottom` | Show task page: bottom | | `template:task:show:before-description` | Show task page: before description | -- cgit v1.2.3 From 922e0fb6de06a98774418612e0b0f75af72b6dbb Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 25 Jun 2016 14:34:46 -0400 Subject: Rewrite integration tests to run with Docker containers --- .docker/crontab/cronjob.alpine | 1 + .docker/crontab/cronjob.debian | 1 + .docker/crontab/kanboard | 1 - .dockerignore | 6 + ChangeLog | 1 + Dockerfile | 3 +- Makefile | 23 +- app/Api/BaseApi.php | 11 + app/Api/FileApi.php | 91 --- app/Api/ProjectApi.php | 8 +- app/Api/ProjectPermissionApi.php | 18 - app/Api/TaskApi.php | 10 +- app/Api/TaskFileApi.php | 59 ++ app/Api/UserApi.php | 13 +- app/ServiceProvider/ApiProvider.php | 4 +- doc/api-project-procedures.markdown | 4 +- doc/tests.markdown | 49 +- tests/configs/config.mysql.php | 12 + tests/configs/config.postgres.php | 12 + tests/configs/config.sqlite.php | 8 + tests/docker/Dockerfile.xenial | 24 + tests/docker/compose.integration.mysql.yaml | 27 + tests/docker/compose.integration.postgres.yaml | 26 + tests/docker/compose.integration.sqlite.yaml | 16 + tests/docker/entrypoint.sh | 33 + tests/docker/supervisord.conf | 6 + tests/integration.mysql.xml | 14 +- tests/integration.postgres.xml | 13 +- tests/integration.sqlite.xml | 9 +- tests/integration/ActionTest.php | 66 ++ tests/integration/ApiTest.php | 928 ------------------------- tests/integration/AppTest.php | 24 +- tests/integration/Base.php | 62 -- tests/integration/BaseIntegrationTest.php | 122 ++++ tests/integration/BoardTest.php | 16 +- tests/integration/CategoryTest.php | 76 ++ tests/integration/ColumnTest.php | 78 ++- tests/integration/CommentTest.php | 63 ++ tests/integration/GroupMemberTest.php | 56 +- tests/integration/GroupTest.php | 56 +- tests/integration/LinkTest.php | 70 ++ tests/integration/MeTest.php | 228 +----- tests/integration/OverdueTaskTest.php | 43 ++ tests/integration/ProjectPermissionTest.php | 107 +-- tests/integration/ProjectTest.php | 89 +++ tests/integration/SubtaskTest.php | 64 ++ tests/integration/SwimlaneTest.php | 86 +-- tests/integration/TaskFileTest.php | 67 ++ tests/integration/TaskLinkTest.php | 68 ++ tests/integration/TaskTest.php | 139 +--- tests/integration/UserTest.php | 63 +- 51 files changed, 1387 insertions(+), 1687 deletions(-) create mode 100644 .docker/crontab/cronjob.alpine create mode 100644 .docker/crontab/cronjob.debian delete mode 100644 .docker/crontab/kanboard create mode 100644 .dockerignore delete mode 100644 app/Api/FileApi.php create mode 100644 app/Api/TaskFileApi.php create mode 100644 tests/configs/config.mysql.php create mode 100644 tests/configs/config.postgres.php create mode 100644 tests/configs/config.sqlite.php create mode 100644 tests/docker/Dockerfile.xenial create mode 100644 tests/docker/compose.integration.mysql.yaml create mode 100644 tests/docker/compose.integration.postgres.yaml create mode 100644 tests/docker/compose.integration.sqlite.yaml create mode 100755 tests/docker/entrypoint.sh create mode 100644 tests/docker/supervisord.conf create mode 100644 tests/integration/ActionTest.php delete mode 100644 tests/integration/ApiTest.php delete mode 100644 tests/integration/Base.php create mode 100644 tests/integration/BaseIntegrationTest.php create mode 100644 tests/integration/CategoryTest.php create mode 100644 tests/integration/CommentTest.php create mode 100644 tests/integration/LinkTest.php create mode 100644 tests/integration/OverdueTaskTest.php create mode 100644 tests/integration/ProjectTest.php create mode 100644 tests/integration/SubtaskTest.php create mode 100644 tests/integration/TaskFileTest.php create mode 100644 tests/integration/TaskLinkTest.php (limited to 'doc') diff --git a/.docker/crontab/cronjob.alpine b/.docker/crontab/cronjob.alpine new file mode 100644 index 00000000..91ad044e --- /dev/null +++ b/.docker/crontab/cronjob.alpine @@ -0,0 +1 @@ +1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.docker/crontab/cronjob.debian b/.docker/crontab/cronjob.debian new file mode 100644 index 00000000..40310d4f --- /dev/null +++ b/.docker/crontab/cronjob.debian @@ -0,0 +1 @@ +@daily www-data cd /var/www/html/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.docker/crontab/kanboard b/.docker/crontab/kanboard deleted file mode 100644 index 91ad044e..00000000 --- a/.docker/crontab/kanboard +++ /dev/null @@ -1 +0,0 @@ -1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..569d36ca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.git* +data/* +Makefile +.*.yml +*.json \ No newline at end of file diff --git a/ChangeLog b/ChangeLog index 7a6f4edf..b86fea57 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ Version 1.0.31 (unreleased) Improvements: +* Rewrite integration tests to run with Docker containers * Use the same task form layout everywhere * Remove some tasks dropdown menus that are now available with task edit form * Make embedded documentation available in multiple languages diff --git a/Dockerfile b/Dockerfile index 6f523373..aa9eb9cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,8 +24,7 @@ COPY .docker/php/conf.d/local.ini /etc/php5/conf.d/ COPY .docker/php/php-fpm.conf /etc/php5/ COPY .docker/nginx/nginx.conf /etc/nginx/ COPY .docker/kanboard/config.php /var/www/kanboard/ -COPY .docker/kanboard/config.php /var/www/kanboard/ -COPY .docker/crontab/kanboard /var/spool/cron/crontabs/nginx +COPY .docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx EXPOSE 80 diff --git a/Makefile b/Makefile index 6ffba241..47e9d389 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,28 @@ test-postgres: unittest: test-sqlite test-mysql test-postgres test-browser: - @ phpunit -c tests/acceptance.xml + @ phpunit -c tests/acceptance.xml + +integration-test-mysql: + @ composer install + @ docker-compose -f tests/docker/compose.integration.mysql.yaml build + @ docker-compose -f tests/docker/compose.integration.mysql.yaml up -d mysql app + @ docker-compose -f tests/docker/compose.integration.mysql.yaml up tests + @ docker-compose -f tests/docker/compose.integration.mysql.yaml down + +integration-test-postgres: + @ composer install + @ docker-compose -f tests/docker/compose.integration.postgres.yaml build + @ docker-compose -f tests/docker/compose.integration.postgres.yaml up -d postgres app + @ docker-compose -f tests/docker/compose.integration.postgres.yaml up tests + @ docker-compose -f tests/docker/compose.integration.postgres.yaml down + +integration-test-sqlite: + @ composer install + @ docker-compose -f tests/docker/compose.integration.sqlite.yaml build + @ docker-compose -f tests/docker/compose.integration.sqlite.yaml up -d app + @ docker-compose -f tests/docker/compose.integration.sqlite.yaml up tests + @ docker-compose -f tests/docker/compose.integration.sqlite.yaml down sql: @ pg_dump --schema-only --no-owner --no-privileges --quote-all-identifiers -n public --file app/Schema/Sql/postgres.sql kanboard diff --git a/app/Api/BaseApi.php b/app/Api/BaseApi.php index 9f69aa65..8f18802c 100644 --- a/app/Api/BaseApi.php +++ b/app/Api/BaseApi.php @@ -71,4 +71,15 @@ abstract class BaseApi extends Base return $projects; } + + protected function filterValues(array $values) + { + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $values; + } } diff --git a/app/Api/FileApi.php b/app/Api/FileApi.php deleted file mode 100644 index 1ed3aeb9..00000000 --- a/app/Api/FileApi.php +++ /dev/null @@ -1,91 +0,0 @@ -taskFileModel->getById($file_id); - } - - public function getAllTaskFiles($task_id) - { - return $this->taskFileModel->getAll($task_id); - } - - public function downloadTaskFile($file_id) - { - try { - $file = $this->taskFileModel->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 createTaskFile($project_id, $task_id, $filename, $blob) - { - try { - return $this->taskFileModel->uploadContent($task_id, $filename, $blob); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return false; - } - } - - public function removeTaskFile($file_id) - { - return $this->taskFileModel->remove($file_id); - } - - public function removeAllTaskFiles($task_id) - { - return $this->taskFileModel->removeAll($task_id); - } - - // Deprecated procedures - - public function getFile($file_id) - { - return $this->getTaskFile($file_id); - } - - public function getAllFiles($task_id) - { - return $this->getAllTaskFiles($task_id); - } - - public function downloadFile($file_id) - { - return $this->downloadTaskFile($file_id); - } - - public function createFile($project_id, $task_id, $filename, $blob) - { - return $this->createTaskFile($project_id, $task_id, $filename, $blob); - } - - public function removeFile($file_id) - { - return $this->removeTaskFile($file_id); - } - - public function removeAllFiles($task_id) - { - return $this->removeAllTaskFiles($task_id); - } -} diff --git a/app/Api/ProjectApi.php b/app/Api/ProjectApi.php index 29a9cd79..a726d4eb 100644 --- a/app/Api/ProjectApi.php +++ b/app/Api/ProjectApi.php @@ -73,13 +73,13 @@ class ProjectApi extends BaseApi return $valid ? $this->projectModel->create($values) : false; } - public function updateProject($id, $name, $description = null) + public function updateProject($project_id, $name, $description = null) { - $values = array( - 'id' => $id, + $values = $this->filterValues(array( + 'id' => $project_id, 'name' => $name, 'description' => $description - ); + )); list($valid, ) = $this->projectValidator->validateModification($values); return $valid && $this->projectModel->update($values); diff --git a/app/Api/ProjectPermissionApi.php b/app/Api/ProjectPermissionApi.php index 703cd0f3..37c5e13c 100644 --- a/app/Api/ProjectPermissionApi.php +++ b/app/Api/ProjectPermissionApi.php @@ -52,22 +52,4 @@ class ProjectPermissionApi extends Base { return $this->projectGroupRoleModel->changeGroupRole($project_id, $group_id, $role); } - - // Deprecated - public function getMembers($project_id) - { - return $this->getProjectUsers($project_id); - } - - // Deprecated - public function revokeUser($project_id, $user_id) - { - return $this->removeProjectUser($project_id, $user_id); - } - - // Deprecated - public function allowUser($project_id, $user_id) - { - return $this->addProjectUser($project_id, $user_id); - } } diff --git a/app/Api/TaskApi.php b/app/Api/TaskApi.php index ddb3ac54..523bfaa0 100644 --- a/app/Api/TaskApi.php +++ b/app/Api/TaskApi.php @@ -139,7 +139,7 @@ class TaskApi extends BaseApi return false; } - $values = array( + $values = $this->filterValues(array( 'id' => $id, 'title' => $title, 'color_id' => $color_id, @@ -155,13 +155,7 @@ class TaskApi extends BaseApi 'recurrence_basedate' => $recurrence_basedate, 'reference' => $reference, 'priority' => $priority, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } + )); list($valid) = $this->taskValidator->validateApiModification($values); return $valid && $this->taskModificationModel->update($values); diff --git a/app/Api/TaskFileApi.php b/app/Api/TaskFileApi.php new file mode 100644 index 00000000..7b27477c --- /dev/null +++ b/app/Api/TaskFileApi.php @@ -0,0 +1,59 @@ +taskFileModel->getById($file_id); + } + + public function getAllTaskFiles($task_id) + { + return $this->taskFileModel->getAll($task_id); + } + + public function downloadTaskFile($file_id) + { + try { + $file = $this->taskFileModel->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 createTaskFile($project_id, $task_id, $filename, $blob) + { + try { + return $this->taskFileModel->uploadContent($task_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + public function removeTaskFile($file_id) + { + return $this->taskFileModel->remove($file_id); + } + + public function removeAllTaskFiles($task_id) + { + return $this->taskFileModel->removeAll($task_id); + } +} diff --git a/app/Api/UserApi.php b/app/Api/UserApi.php index 88d75527..6cb9df1c 100644 --- a/app/Api/UserApi.php +++ b/app/Api/UserApi.php @@ -2,7 +2,6 @@ namespace Kanboard\Api; -use Kanboard\Core\Base; use LogicException; use Kanboard\Core\Security\Role; use Kanboard\Core\Ldap\Client as LdapClient; @@ -15,7 +14,7 @@ use Kanboard\Core\Ldap\User as LdapUser; * @package Kanboard\Api * @author Frederic Guillot */ -class UserApi extends Base +class UserApi extends BaseApi { public function getUser($user_id) { @@ -118,19 +117,13 @@ class UserApi extends Base public function updateUser($id, $username = null, $name = null, $email = null, $role = null) { - $values = array( + $values = $this->filterValues(array( 'id' => $id, 'username' => $username, 'name' => $name, 'email' => $email, 'role' => $role, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } + )); list($valid, ) = $this->userValidator->validateApiModification($values); return $valid && $this->userModel->update($values); diff --git a/app/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php index 93b3c7f5..e0312056 100644 --- a/app/ServiceProvider/ApiProvider.php +++ b/app/ServiceProvider/ApiProvider.php @@ -9,7 +9,7 @@ use Kanboard\Api\BoardApi; use Kanboard\Api\CategoryApi; use Kanboard\Api\ColumnApi; use Kanboard\Api\CommentApi; -use Kanboard\Api\FileApi; +use Kanboard\Api\TaskFileApi; use Kanboard\Api\GroupApi; use Kanboard\Api\GroupMemberApi; use Kanboard\Api\LinkApi; @@ -56,7 +56,7 @@ class ApiProvider implements ServiceProviderInterface ->withObject(new ColumnApi($container)) ->withObject(new CategoryApi($container)) ->withObject(new CommentApi($container)) - ->withObject(new FileApi($container)) + ->withObject(new TaskFileApi($container)) ->withObject(new LinkApi($container)) ->withObject(new ProjectApi($container)) ->withObject(new ProjectPermissionApi($container)) diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown index 6cc1b15b..f8e2cc5e 100644 --- a/doc/api-project-procedures.markdown +++ b/doc/api-project-procedures.markdown @@ -183,7 +183,7 @@ Response example: - Purpose: **Update a project** - Parameters: - - **id** (integer, required) + - **project_id** (integer, required) - **name** (string, required) - **description** (string, optional) - Result on success: **true** @@ -197,7 +197,7 @@ Request example: "method": "updateProject", "id": 1853996288, "params": { - "id": 1, + "project_id": 1, "name": "PHP client update" } } diff --git a/doc/tests.markdown b/doc/tests.markdown index 5e3d71d2..59177f87 100644 --- a/doc/tests.markdown +++ b/doc/tests.markdown @@ -9,7 +9,7 @@ Requirements ------------ - Linux/Unix machine -- PHP cli +- PHP - PHPUnit installed - Mysql and Postgresql (optional) - Selenium (optional) @@ -85,46 +85,37 @@ From your Kanboard directory, run the command `phpunit -c tests/units.postgres.x Integration Tests ----------------- -Acceptance tests (also known as end-to-end tests and sometimes functional tests) allow us to test the actual functionality of the browser using Selenium and PHPUnit. +Integration tests are mainly used to test the API. +The test suites are making real HTTP calls to the application that run inside a container. -The PHPUnit config file is `tests/acceptance.xml`. -From your Kanboard directory, run the command `phpunit -c tests/units.sqlite.xml`. - -Actually only the API calls are tested. - -Real HTTP calls are made with those tests. -So a local instance of Kanboard is necessary and must listen on `http://localhost:8000/`. - -All data will be removed/altered by the test suite. -Moreover the script will reset and set a new API key. +### Requirements -1. Start a local instance of Kanboard `php -S 127.0.0.1:8000` -2. Run the test suite from another terminal +- PHP +- Composer +- Unix operating system (Mac OS or Linux) +- Docker +- Docker Compose -The same method as above is used to run tests across different databases: +### Running integration tests -- Sqlite: `phpunit -c tests/integration.sqlite.xml` -- Mysql: `phpunit -c tests/integration.mysql.xml` -- Postgresql: `phpunit -c tests/integration.postgres.xml` +Integration tests are using Docker containers. +There are 3 different environment available to run tests against each supported database. -Example: +You can use these commands to run each test suite: ```bash -phpunit -c tests/integration.sqlite.xml - -PHPUnit 5.0.0 by Sebastian Bergmann and contributors. +# Run tests with Sqlite +make integration-test-sqlite -............................................................... 63 / 135 ( 46%) -............................................................... 126 / 135 ( 93%) -......... 135 / 135 (100%) +# Run tests with Mysql +make integration-test-mysql -Time: 1.18 minutes, Memory: 14.75Mb - -OK (135 tests, 526 assertions) +# Run tests with Postgres +make integration-test-postgres ``` Acceptance Tests ------------------ +---------------- Acceptance tests (also sometimes known as end-to-end tests, and functional tests) test the actual functionality of the UI in a browser using Selenium. diff --git a/tests/configs/config.mysql.php b/tests/configs/config.mysql.php new file mode 100644 index 00000000..27e32744 --- /dev/null +++ b/tests/configs/config.mysql.php @@ -0,0 +1,12 @@ +> /etc/apache2/apache2.conf && \ + sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf && \ + a2enmod rewrite && \ + curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer + +COPY . /var/www/html + +RUN chown -R www-data:www-data /var/www/html/data /var/www/html/plugins + +COPY tests/docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY tests/configs /configs/ + +EXPOSE 80 + +ENTRYPOINT ["/var/www/html/tests/docker/entrypoint.sh"] diff --git a/tests/docker/compose.integration.mysql.yaml b/tests/docker/compose.integration.mysql.yaml new file mode 100644 index 00000000..6eda5eec --- /dev/null +++ b/tests/docker/compose.integration.mysql.yaml @@ -0,0 +1,27 @@ +version: '2' +services: + mysql: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: "kanboard" + MYSQL_DATABASE: "kanboard" + MYSQL_USER: "kanboard" + MYSQL_PASSWORD: "kanboard" + ports: + - "3306:3306" + app: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + ports: + - "8000:80" + depends_on: + - mysql + command: config-mysql + tests: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + depends_on: + - app + command: integration-test-mysql diff --git a/tests/docker/compose.integration.postgres.yaml b/tests/docker/compose.integration.postgres.yaml new file mode 100644 index 00000000..ed095248 --- /dev/null +++ b/tests/docker/compose.integration.postgres.yaml @@ -0,0 +1,26 @@ +version: '2' +services: + postgres: + image: postgres:9.5 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: kanboard + ports: + - "5432:5432" + app: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + ports: + - "8000:80" + depends_on: + - postgres + command: config-postgres + tests: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + depends_on: + - app + command: integration-test-postgres diff --git a/tests/docker/compose.integration.sqlite.yaml b/tests/docker/compose.integration.sqlite.yaml new file mode 100644 index 00000000..6431484e --- /dev/null +++ b/tests/docker/compose.integration.sqlite.yaml @@ -0,0 +1,16 @@ +version: '2' +services: + app: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + ports: + - "8000:80" + command: config-sqlite + tests: + build: + context: ../.. + dockerfile: tests/docker/Dockerfile.xenial + depends_on: + - app + command: integration-test-sqlite diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh new file mode 100755 index 00000000..a88c7ed8 --- /dev/null +++ b/tests/docker/entrypoint.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +function wait_schema_creation() { + curl -s http://app/login > /dev/null + sleep $1 +} + +case "$1" in +"config-sqlite") + cp /configs/config.sqlite.php /var/www/html/config.php + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf + ;; +"config-postgres") + cp /configs/config.postgres.php /var/www/html/config.php + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf + ;; +"config-mysql") + cp /configs/config.mysql.php /var/www/html/config.php + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf + ;; +"integration-test-sqlite") + wait_schema_creation 1 + /var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.sqlite.xml + ;; +"integration-test-postgres") + wait_schema_creation 5 + /var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.postgres.xml + ;; +"integration-test-mysql") + wait_schema_creation 15 + /var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.mysql.xml + ;; +esac diff --git a/tests/docker/supervisord.conf b/tests/docker/supervisord.conf new file mode 100644 index 00000000..4d5ee621 --- /dev/null +++ b/tests/docker/supervisord.conf @@ -0,0 +1,6 @@ +[supervisord] +nodaemon=true + +[program:apache2] +command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND" +autorestart=true diff --git a/tests/integration.mysql.xml b/tests/integration.mysql.xml index 9d87f77e..33813187 100644 --- a/tests/integration.mysql.xml +++ b/tests/integration.mysql.xml @@ -5,16 +5,8 @@ - - - - - - - - - - - + + + diff --git a/tests/integration.postgres.xml b/tests/integration.postgres.xml index ed8a3de3..33813187 100644 --- a/tests/integration.postgres.xml +++ b/tests/integration.postgres.xml @@ -5,13 +5,8 @@ - - - - - - - - + + + - \ No newline at end of file + diff --git a/tests/integration.sqlite.xml b/tests/integration.sqlite.xml index 1964f822..33813187 100644 --- a/tests/integration.sqlite.xml +++ b/tests/integration.sqlite.xml @@ -5,9 +5,8 @@ - - - - + + + - \ No newline at end of file + diff --git a/tests/integration/ActionTest.php b/tests/integration/ActionTest.php new file mode 100644 index 00000000..7a5adc4a --- /dev/null +++ b/tests/integration/ActionTest.php @@ -0,0 +1,66 @@ +app->getAvailableActions(); + $this->assertNotEmpty($actions); + $this->assertInternalType('array', $actions); + $this->assertArrayHasKey('\Kanboard\Action\TaskCloseColumn', $actions); + } + + public function testGetAvailableActionEvents() + { + $events = $this->app->getAvailableActionEvents(); + $this->assertNotEmpty($events); + $this->assertInternalType('array', $events); + $this->assertArrayHasKey('task.move.column', $events); + } + + public function testGetCompatibleActionEvents() + { + $events = $this->app->getCompatibleActionEvents('\Kanboard\Action\TaskCloseColumn'); + $this->assertNotEmpty($events); + $this->assertInternalType('array', $events); + $this->assertArrayHasKey('task.move.column', $events); + } + + public function testCRUD() + { + $this->assertCreateTeamProject(); + $this->assertCreateAction(); + $this->assertGetActions(); + $this->assertRemoveAction(); + } + + public function assertCreateAction() + { + $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertNotFalse($actionId); + $this->assertTrue($actionId > 0); + } + + public function assertGetActions() + { + $actions = $this->app->getActions($this->projectId); + $this->assertNotEmpty($actions); + $this->assertInternalType('array', $actions); + $this->assertArrayHasKey('id', $actions[0]); + $this->assertArrayHasKey('project_id', $actions[0]); + $this->assertArrayHasKey('event_name', $actions[0]); + $this->assertArrayHasKey('action_name', $actions[0]); + $this->assertArrayHasKey('params', $actions[0]); + $this->assertArrayHasKey('column_id', $actions[0]['params']); + } + + public function assertRemoveAction() + { + $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertTrue($this->app->removeAction($actionId)); + } +} diff --git a/tests/integration/ApiTest.php b/tests/integration/ApiTest.php deleted file mode 100644 index f552bea9..00000000 --- a/tests/integration/ApiTest.php +++ /dev/null @@ -1,928 +0,0 @@ -exec('DROP DATABASE '.DB_NAME); - $pdo->exec('CREATE DATABASE '.DB_NAME); - $pdo = null; - } elseif (DB_DRIVER === 'postgres') { - $pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); - $pdo->exec('DROP DATABASE '.DB_NAME); - $pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); - $pdo = null; - } - - $service = new Kanboard\ServiceProvider\DatabaseProvider; - - $db = $service->getInstance(); - $db->table('settings')->eq('option', 'api_token')->update(array('value' => API_KEY)); - $db->table('settings')->eq('option', 'application_timezone')->update(array('value' => 'Europe/Paris')); - $db->closeConnection(); - } - - public function setUp() - { - $this->client = new JsonRPC\Client(API_URL); - $this->client->authentication('jsonrpc', API_KEY); - // $this->client->debug = true; - } - - private function getTaskId() - { - $tasks = $this->client->getAllTasks(1, 1); - $this->assertNotEmpty($tasks); - - return $tasks[0]['id']; - } - - public function testRemoveAll() - { - $projects = $this->client->getAllProjects(); - - if ($projects) { - foreach ($projects as $project) { - $this->assertEquals('http://127.0.0.1:8000/?controller=BoardViewController&action=show&project_id='.$project['id'], $project['url']['board']); - $this->assertEquals('http://127.0.0.1:8000/?controller=CalendarController&action=show&project_id='.$project['id'], $project['url']['calendar']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskListController&action=show&project_id='.$project['id'], $project['url']['list']); - $this->assertTrue($this->client->removeProject($project['id'])); - } - } - } - - public function testCreateProject() - { - $project_id = $this->client->createProject('API test'); - $this->assertNotFalse($project_id); - $this->assertInternalType('int', $project_id); - } - - public function testGetProjectById() - { - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertEquals(1, $project['id']); - $this->assertEquals('http://127.0.0.1:8000/?controller=BoardViewController&action=show&project_id='.$project['id'], $project['url']['board']); - $this->assertEquals('http://127.0.0.1:8000/?controller=CalendarController&action=show&project_id='.$project['id'], $project['url']['calendar']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskListController&action=show&project_id='.$project['id'], $project['url']['list']); - } - - public function testGetProjectByName() - { - $project = $this->client->getProjectByName('API test'); - $this->assertNotEmpty($project); - $this->assertEquals(1, $project['id']); - $this->assertEquals('http://127.0.0.1:8000/?controller=BoardViewController&action=show&project_id='.$project['id'], $project['url']['board']); - $this->assertEquals('http://127.0.0.1:8000/?controller=CalendarController&action=show&project_id='.$project['id'], $project['url']['calendar']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskListController&action=show&project_id='.$project['id'], $project['url']['list']); - - $project = $this->client->getProjectByName(array('name' => 'API test')); - $this->assertNotEmpty($project); - $this->assertEquals(1, $project['id']); - - $project = $this->client->getProjectByName('None'); - $this->assertEmpty($project); - $this->assertNull($project); - } - - public function testGetAllProjects() - { - $projects = $this->client->getAllProjects(); - $this->assertNotEmpty($projects); - - foreach ($projects as $project) { - $this->assertEquals('http://127.0.0.1:8000/?controller=BoardViewController&action=show&project_id='.$project['id'], $project['url']['board']); - $this->assertEquals('http://127.0.0.1:8000/?controller=CalendarController&action=show&project_id='.$project['id'], $project['url']['calendar']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskListController&action=show&project_id='.$project['id'], $project['url']['list']); - } - } - - public function testUpdateProject() - { - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertTrue($this->client->execute('updateProject', array('id' => 1, 'name' => 'API test 2'))); - - $project = $this->client->getProjectById(1); - $this->assertEquals('API test 2', $project['name']); - - $this->assertTrue($this->client->execute('updateProject', array('id' => 1, 'name' => 'API test', 'description' => 'test'))); - - $project = $this->client->getProjectById(1); - $this->assertEquals('API test', $project['name']); - $this->assertEquals('test', $project['description']); - } - - public function testDisableProject() - { - $this->assertTrue($this->client->disableProject(1)); - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertEquals(0, $project['is_active']); - } - - public function testEnableProject() - { - $this->assertTrue($this->client->enableProject(1)); - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertEquals(1, $project['is_active']); - } - - public function testEnableProjectPublicAccess() - { - $this->assertTrue($this->client->enableProjectPublicAccess(1)); - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertEquals(1, $project['is_public']); - $this->assertNotEmpty($project['token']); - } - - public function testDisableProjectPublicAccess() - { - $this->assertTrue($this->client->disableProjectPublicAccess(1)); - $project = $this->client->getProjectById(1); - $this->assertNotEmpty($project); - $this->assertEquals(0, $project['is_public']); - $this->assertEmpty($project['token']); - } - - public function testgetProjectActivities() - { - $activities = $this->client->getProjectActivities(array('project_ids' => array(1))); - $this->assertInternalType('array', $activities); - $this->assertCount(0, $activities); - } - - public function testgetProjectActivity() - { - $activities = $this->client->getProjectActivity(1); - $this->assertInternalType('array', $activities); - $this->assertCount(0, $activities); - } - - public function testCreateTaskWithWrongMember() - { - $task = array( - 'title' => 'Task #1', - 'color_id' => 'blue', - 'owner_id' => 1, - 'project_id' => 1, - 'column_id' => 2, - ); - - $task_id = $this->client->createTask($task); - - $this->assertFalse($task_id); - } - - public function testGetAllowedUsers() - { - $users = $this->client->getMembers(1); - $this->assertNotFalse($users); - $this->assertEquals(array(), $users); - } - - public function testAddMember() - { - $this->assertTrue($this->client->allowUser(1, 1)); - } - - public function testCreateTask() - { - $task = array( - 'title' => 'Task #1', - 'color_id' => 'blue', - 'owner_id' => 1, - 'project_id' => 1, - 'column_id' => 2, - ); - - $task_id = $this->client->createTask($task); - - $this->assertNotFalse($task_id); - $this->assertInternalType('int', $task_id); - $this->assertTrue($task_id > 0); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testCreateTaskWithBadParams() - { - $task = array( - 'title' => 'Task #1', - 'color_id' => 'blue', - 'owner_id' => 1, - ); - - $this->client->createTask($task); - } - - public function testGetTask() - { - $task = $this->client->getTask(1); - - $this->assertNotFalse($task); - $this->assertTrue(is_array($task)); - $this->assertEquals('Task #1', $task['title']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskViewController&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'], $task['url']); - } - - public function testGetAllTasks() - { - $tasks = $this->client->getAllTasks(1, 1); - - $this->assertNotFalse($tasks); - $this->assertTrue(is_array($tasks)); - $this->assertEquals('Task #1', $tasks[0]['title']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskViewController&action=show&task_id='.$tasks[0]['id'].'&project_id='.$tasks[0]['project_id'], $tasks[0]['url']); - - $tasks = $this->client->getAllTasks(2, 0); - - $this->assertNotFalse($tasks); - $this->assertTrue(is_array($tasks)); - $this->assertEmpty($tasks); - } - - public function testMoveTaskSwimlane() - { - $task_id = $this->getTaskId(); - - $task = $this->client->getTask($task_id); - $this->assertNotFalse($task); - $this->assertTrue(is_array($task)); - $this->assertEquals(1, $task['position']); - $this->assertEquals(2, $task['column_id']); - $this->assertEquals(0, $task['swimlane_id']); - - $moved_timestamp = $task['date_moved']; - sleep(1); - $this->assertTrue($this->client->moveTaskPosition(1, $task_id, 4, 1, 2)); - - $task = $this->client->getTask($task_id); - $this->assertNotFalse($task); - $this->assertTrue(is_array($task)); - $this->assertEquals(1, $task['position']); - $this->assertEquals(4, $task['column_id']); - $this->assertEquals(2, $task['swimlane_id']); - $this->assertNotEquals($moved_timestamp, $task['date_moved']); - } - - public function testUpdateTask() - { - $task = $this->client->getTask(1); - - $values = array(); - $values['id'] = $task['id']; - $values['color_id'] = 'green'; - $values['description'] = 'test'; - $values['date_due'] = ''; - - $this->assertTrue($this->client->execute('updateTask', $values)); - } - - public function testRemoveTask() - { - $this->assertTrue($this->client->removeTask(1)); - } - - public function testRemoveUsers() - { - $users = $this->client->getAllUsers(); - $this->assertNotFalse($users); - $this->assertNotEmpty($users); - - foreach ($users as $user) { - if ($user['id'] > 1) { - $this->assertTrue($this->client->removeUser($user['id'])); - } - } - } - - public function testCreateUser() - { - $user = array( - 'username' => 'toto', - 'name' => 'Toto', - 'password' => '123456', - ); - - $user_id = $this->client->execute('createUser', $user); - $this->assertNotFalse($user_id); - $this->assertInternalType('int', $user_id); - $this->assertTrue($user_id > 0); - } - - public function testCreateManagerUser() - { - $user = array( - 'username' => 'manager', - 'name' => 'Manager', - 'password' => '123456', - 'role' => 'app-manager' - ); - - $user_id = $this->client->execute('createUser', $user); - $this->assertNotFalse($user_id); - $this->assertInternalType('int', $user_id); - $this->assertTrue($user_id > 0); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testCreateUserWithBadParams() - { - $user = array( - 'name' => 'Titi', - 'password' => '123456', - ); - - $this->assertNull($this->client->execute('createUser', $user)); - } - - public function testGetUser() - { - $user = $this->client->getUser(2); - $this->assertNotFalse($user); - $this->assertTrue(is_array($user)); - $this->assertEquals('toto', $user['username']); - - $user = $this->client->getUser(3); - $this->assertNotEmpty($user); - $this->assertEquals('app-manager', $user['role']); - - $this->assertNull($this->client->getUser(2222)); - } - - public function testGetUserByName() - { - $user = $this->client->getUserByName('toto'); - $this->assertNotFalse($user); - $this->assertTrue(is_array($user)); - $this->assertEquals(2, $user['id']); - - $user = $this->client->getUserByName('manager'); - $this->assertNotEmpty($user); - $this->assertEquals('app-manager', $user['role']); - - $this->assertNull($this->client->getUserByName('nonexistantusername')); - } - - public function testUpdateUser() - { - $user = array(); - $user['id'] = 2; - $user['username'] = 'titi'; - $user['name'] = 'Titi'; - - $this->assertTrue($this->client->execute('updateUser', $user)); - - $user = $this->client->getUser(2); - $this->assertNotFalse($user); - $this->assertTrue(is_array($user)); - $this->assertEquals('titi', $user['username']); - $this->assertEquals('Titi', $user['name']); - - $user = array(); - $user['id'] = 2; - $user['email'] = 'titi@localhost'; - - $this->assertTrue($this->client->execute('updateUser', $user)); - - $user = $this->client->getUser(2); - $this->assertNotFalse($user); - $this->assertTrue(is_array($user)); - $this->assertEquals('titi@localhost', $user['email']); - } - - public function testAllowedUser() - { - $this->assertTrue($this->client->allowUser(1, 2)); - - $users = $this->client->getMembers(1); - $this->assertNotFalse($users); - $this->assertEquals(array(1 => 'admin', 2 => 'Titi'), $users); - } - - public function testRevokeUser() - { - $this->assertTrue($this->client->revokeUser(1, 2)); - - $users = $this->client->getMembers(1); - $this->assertNotFalse($users); - $this->assertEquals(array(1 => 'admin'), $users); - } - - public function testCreateComment() - { - $task = array( - 'title' => 'Task with comment', - 'color_id' => 'red', - 'owner_id' => 1, - 'project_id' => 1, - 'column_id' => 1, - ); - - $this->assertNotFalse($this->client->execute('createTask', $task)); - - $tasks = $this->client->getAllTasks(1, 1); - $this->assertNotEmpty($tasks); - $this->assertEquals(1, count($tasks)); - - $comment = array( - 'task_id' => $tasks[0]['id'], - 'user_id' => 2, - 'content' => 'boo', - ); - - $comment_id = $this->client->execute('createComment', $comment); - - $this->assertNotFalse($comment_id); - $this->assertInternalType('int', $comment_id); - $this->assertTrue($comment_id > 0); - } - - public function testGetComment() - { - $comment = $this->client->getComment(1); - $this->assertNotFalse($comment); - $this->assertNotEmpty($comment); - $this->assertEquals(2, $comment['user_id']); - $this->assertEquals('boo', $comment['comment']); - } - - public function testUpdateComment() - { - $comment = array(); - $comment['id'] = 1; - $comment['content'] = 'test'; - - $this->assertTrue($this->client->execute('updateComment', $comment)); - - $comment = $this->client->getComment(1); - $this->assertEquals('test', $comment['comment']); - } - - public function testGetAllComments() - { - $task_id = $this->getTaskId(); - - $comment = array( - 'task_id' => $task_id, - 'user_id' => 1, - 'content' => 'blabla', - ); - - $comment_id = $this->client->createComment($comment); - - $this->assertNotFalse($comment_id); - $this->assertInternalType('int', $comment_id); - $this->assertTrue($comment_id > 0); - - $comments = $this->client->getAllComments($task_id); - $this->assertNotFalse($comments); - $this->assertNotEmpty($comments); - $this->assertTrue(is_array($comments)); - $this->assertEquals(2, count($comments)); - } - - public function testRemoveComment() - { - $task_id = $this->getTaskId(); - - $comments = $this->client->getAllComments($task_id); - $this->assertNotFalse($comments); - $this->assertNotEmpty($comments); - $this->assertTrue(is_array($comments)); - - foreach ($comments as $comment) { - $this->assertTrue($this->client->removeComment($comment['id'])); - } - - $comments = $this->client->getAllComments($task_id); - $this->assertNotFalse($comments); - $this->assertEmpty($comments); - $this->assertTrue(is_array($comments)); - } - - public function testCreateSubtask() - { - $subtask = array( - 'task_id' => $this->getTaskId(), - 'title' => 'subtask #1', - ); - - $subtask_id = $this->client->createSubtask($subtask); - - $this->assertNotFalse($subtask_id); - $this->assertInternalType('int', $subtask_id); - $this->assertTrue($subtask_id > 0); - } - - public function testGetSubtask() - { - $subtask = $this->client->getSubtask(1); - $this->assertNotFalse($subtask); - $this->assertNotEmpty($subtask); - $this->assertEquals($this->getTaskId(), $subtask['task_id']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals('subtask #1', $subtask['title']); - } - - public function testUpdateSubtask() - { - $subtask = array(); - $subtask['id'] = 1; - $subtask['task_id'] = $this->getTaskId(); - $subtask['title'] = 'test'; - - $this->assertTrue($this->client->execute('updateSubtask', $subtask)); - - $subtask = $this->client->getSubtask(1); - $this->assertEquals('test', $subtask['title']); - } - - public function testGetAllSubtasks() - { - $subtask = array( - 'task_id' => $this->getTaskId(), - 'user_id' => 2, - 'title' => 'Subtask #2', - ); - - $this->assertNotFalse($this->client->execute('createSubtask', $subtask)); - - $subtasks = $this->client->getAllSubtasks($this->getTaskId()); - $this->assertNotFalse($subtasks); - $this->assertNotEmpty($subtasks); - $this->assertTrue(is_array($subtasks)); - $this->assertEquals(2, count($subtasks)); - } - - public function testRemoveSubtask() - { - $this->assertTrue($this->client->removeSubtask(1)); - - $subtasks = $this->client->getAllSubtasks($this->getTaskId()); - $this->assertNotFalse($subtasks); - $this->assertNotEmpty($subtasks); - $this->assertTrue(is_array($subtasks)); - $this->assertEquals(1, count($subtasks)); - } - - public function testMoveTaskPosition() - { - $task_id = $this->getTaskId(); - $this->assertTrue($this->client->moveTaskPosition(1, $task_id, 3, 1)); - - $task = $this->client->getTask($task_id); - $this->assertNotFalse($task); - $this->assertTrue(is_array($task)); - $this->assertEquals(1, $task['position']); - $this->assertEquals(3, $task['column_id']); - } - - public function testCategoryCreation() - { - $category = array( - 'name' => 'Category', - 'project_id' => 1, - ); - - $cat_id = $this->client->execute('createCategory', $category); - $this->assertNotFalse($cat_id); - $this->assertInternalType('int', $cat_id); - $this->assertTrue($cat_id > 0); - - // Duplicate - - $category = array( - 'name' => 'Category', - 'project_id' => 1, - ); - - $this->assertFalse($this->client->execute('createCategory', $category)); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testCategoryCreationWithBadParams() - { - // Missing project id - $category = array( - 'name' => 'Category', - ); - - $this->assertNull($this->client->execute('createCategory', $category)); - } - - public function testCategoryRead() - { - $category = $this->client->getCategory(1); - - $this->assertTrue(is_array($category)); - $this->assertNotEmpty($category); - $this->assertEquals(1, $category['id']); - $this->assertEquals('Category', $category['name']); - $this->assertEquals(1, $category['project_id']); - } - - public function testGetAllCategories() - { - $categories = $this->client->getAllCategories(1); - - $this->assertNotEmpty($categories); - $this->assertNotFalse($categories); - $this->assertTrue(is_array($categories)); - $this->assertEquals(1, count($categories)); - $this->assertEquals(1, $categories[0]['id']); - $this->assertEquals('Category', $categories[0]['name']); - $this->assertEquals(1, $categories[0]['project_id']); - } - - public function testCategoryUpdate() - { - $category = array( - 'id' => 1, - 'name' => 'Renamed category', - ); - - $this->assertTrue($this->client->execute('updateCategory', $category)); - - $category = $this->client->getCategory(1); - $this->assertTrue(is_array($category)); - $this->assertNotEmpty($category); - $this->assertEquals(1, $category['id']); - $this->assertEquals('Renamed category', $category['name']); - $this->assertEquals(1, $category['project_id']); - } - - public function testCategoryRemove() - { - $this->assertTrue($this->client->removeCategory(1)); - $this->assertFalse($this->client->removeCategory(1)); - $this->assertFalse($this->client->removeCategory(1111)); - } - - public function testGetAvailableActions() - { - $actions = $this->client->getAvailableActions(); - $this->assertNotEmpty($actions); - $this->assertInternalType('array', $actions); - $this->assertArrayHasKey('\Kanboard\Action\TaskCloseColumn', $actions); - } - - public function testGetAvailableActionEvents() - { - $events = $this->client->getAvailableActionEvents(); - $this->assertNotEmpty($events); - $this->assertInternalType('array', $events); - $this->assertArrayHasKey('task.move.column', $events); - } - - public function testGetCompatibleActionEvents() - { - $events = $this->client->getCompatibleActionEvents('\Kanboard\Action\TaskCloseColumn'); - $this->assertNotEmpty($events); - $this->assertInternalType('array', $events); - $this->assertArrayHasKey('task.move.column', $events); - } - - public function testCreateAction() - { - $action_id = $this->client->createAction(1, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); - $this->assertNotFalse($action_id); - $this->assertEquals(1, $action_id); - } - - public function testGetActions() - { - $actions = $this->client->getActions(1); - $this->assertNotEmpty($actions); - $this->assertInternalType('array', $actions); - $this->assertCount(1, $actions); - $this->assertArrayHasKey('id', $actions[0]); - $this->assertArrayHasKey('project_id', $actions[0]); - $this->assertArrayHasKey('event_name', $actions[0]); - $this->assertArrayHasKey('action_name', $actions[0]); - $this->assertArrayHasKey('params', $actions[0]); - $this->assertArrayHasKey('column_id', $actions[0]['params']); - } - - public function testRemoveAction() - { - $this->assertTrue($this->client->removeAction(1)); - - $actions = $this->client->getActions(1); - $this->assertEmpty($actions); - $this->assertCount(0, $actions); - } - - public function testGetAllLinks() - { - $links = $this->client->getAllLinks(); - $this->assertNotEmpty($links); - $this->assertArrayHasKey('id', $links[0]); - $this->assertArrayHasKey('label', $links[0]); - $this->assertArrayHasKey('opposite_id', $links[0]); - } - - public function testGetOppositeLink() - { - $link = $this->client->getOppositeLinkId(1); - $this->assertEquals(1, $link); - - $link = $this->client->getOppositeLinkId(2); - $this->assertEquals(3, $link); - } - - public function testGetLinkByLabel() - { - $link = $this->client->getLinkByLabel('blocks'); - $this->assertNotEmpty($link); - $this->assertEquals(2, $link['id']); - $this->assertEquals(3, $link['opposite_id']); - } - - public function testGetLinkById() - { - $link = $this->client->getLinkById(4); - $this->assertNotEmpty($link); - $this->assertEquals(4, $link['id']); - $this->assertEquals(5, $link['opposite_id']); - $this->assertEquals('duplicates', $link['label']); - } - - public function testCreateLink() - { - $link_id = $this->client->createLink(array('label' => 'test')); - $this->assertNotFalse($link_id); - $this->assertInternalType('int', $link_id); - - $link_id = $this->client->createLink(array('label' => 'foo', 'opposite_label' => 'bar')); - $this->assertNotFalse($link_id); - $this->assertInternalType('int', $link_id); - } - - public function testUpdateLink() - { - $link1 = $this->client->getLinkByLabel('bar'); - $this->assertNotEmpty($link1); - - $link2 = $this->client->getLinkByLabel('test'); - $this->assertNotEmpty($link2); - - $this->assertNotFalse($this->client->updateLink($link1['id'], $link2['id'], 'boo')); - - $link = $this->client->getLinkById($link1['id']); - $this->assertNotEmpty($link); - $this->assertEquals($link2['id'], $link['opposite_id']); - $this->assertEquals('boo', $link['label']); - - $this->assertTrue($this->client->removeLink($link1['id'])); - } - - public function testCreateTaskLink() - { - $task_id1 = $this->client->createTask(array('project_id' => 1, 'title' => 'A')); - $this->assertNotFalse($task_id1); - - $task_id2 = $this->client->createTask(array('project_id' => 1, 'title' => 'B')); - $this->assertNotFalse($task_id2); - - $task_id3 = $this->client->createTask(array('project_id' => 1, 'title' => 'C')); - $this->assertNotFalse($task_id3); - - $task_link_id = $this->client->createTaskLink($task_id1, $task_id2, 1); - $this->assertNotFalse($task_link_id); - - $task_link = $this->client->getTaskLinkById($task_link_id); - $this->assertNotEmpty($task_link); - $this->assertEquals($task_id1, $task_link['task_id']); - $this->assertEquals($task_id2, $task_link['opposite_task_id']); - $this->assertEquals(1, $task_link['link_id']); - - $task_links = $this->client->getAllTaskLinks($task_id1); - $this->assertNotEmpty($task_links); - $this->assertCount(1, $task_links); - - $this->assertTrue($this->client->updateTaskLink($task_link_id, $task_id1, $task_id3, 2)); - - $task_link = $this->client->getTaskLinkById($task_link_id); - $this->assertNotEmpty($task_link); - $this->assertEquals($task_id1, $task_link['task_id']); - $this->assertEquals($task_id3, $task_link['opposite_task_id']); - $this->assertEquals(2, $task_link['link_id']); - - $this->assertTrue($this->client->removeTaskLink($task_link_id)); - $this->assertEmpty($this->client->getAllTaskLinks($task_id1)); - } - - public function testCreateFile() - { - $this->assertNotFalse($this->client->createFile(1, $this->getTaskId(), 'My file', base64_encode('plain text file'))); - } - - public function testGetAllFiles() - { - $files = $this->client->getAllFiles(array('task_id' => $this->getTaskId())); - - $this->assertNotEmpty($files); - $this->assertCount(1, $files); - $this->assertEquals('My file', $files[0]['name']); - - $file = $this->client->getFile($files[0]['id']); - $this->assertNotEmpty($file); - $this->assertEquals('My file', $file['name']); - - $content = $this->client->downloadFile($file['id']); - $this->assertNotEmpty($content); - $this->assertEquals('plain text file', base64_decode($content)); - - $content = $this->client->downloadFile(1234567); - $this->assertEmpty($content); - - $this->assertTrue($this->client->removeFile($file['id'])); - $this->assertEmpty($this->client->getAllFiles(1)); - } - - public function testRemoveAllFiles() - { - $this->assertNotFalse($this->client->createFile(1, $this->getTaskId(), 'My file 1', base64_encode('plain text file'))); - $this->assertNotFalse($this->client->createFile(1, $this->getTaskId(), 'My file 2', base64_encode('plain text file'))); - - $files = $this->client->getAllFiles(array('task_id' => $this->getTaskId())); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - - $this->assertTrue($this->client->removeAllFiles(array('task_id' => $this->getTaskId()))); - - $files = $this->client->getAllFiles(array('task_id' => $this->getTaskId())); - $this->assertEmpty($files); - } - - public function testCreateTaskWithReference() - { - $task = array( - 'title' => 'Task with external ticket number', - 'reference' => 'TICKET-1234', - 'project_id' => 1, - 'description' => '[Link to my ticket](http://my-ticketing-system/1234)', - ); - - $task_id = $this->client->createTask($task); - - $this->assertNotFalse($task_id); - $this->assertInternalType('int', $task_id); - $this->assertTrue($task_id > 0); - } - - public function testGetTaskByReference() - { - $task = $this->client->getTaskByReference(array('project_id' => 1, 'reference' => 'TICKET-1234')); - - $this->assertNotEmpty($task); - $this->assertEquals('Task with external ticket number', $task['title']); - $this->assertEquals('TICKET-1234', $task['reference']); - $this->assertEquals('http://127.0.0.1:8000/?controller=TaskViewController&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'], $task['url']); - } - - public function testCreateOverdueTask() - { - $this->assertNotFalse($this->client->createTask(array( - 'title' => 'overdue task', - 'project_id' => 1, - 'date_due' => date('Y-m-d', strtotime('-2days')), - ))); - } - - public function testGetOverdueTasksByProject() - { - $tasks = $this->client->getOverdueTasksByProject(1); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals('API test', $tasks[0]['project_name']); - } - - public function testGetOverdueTasks() - { - $tasks = $this->client->getOverdueTasks(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals('API test', $tasks[0]['project_name']); - } -} diff --git a/tests/integration/AppTest.php b/tests/integration/AppTest.php index 6575fbb8..287e6299 100644 --- a/tests/integration/AppTest.php +++ b/tests/integration/AppTest.php @@ -1,8 +1,8 @@ assertEquals('Project Member', $roles['project-member']); $this->assertEquals('Project Viewer', $roles['project-viewer']); } + + public function testGetDefaultColor() + { + $this->assertEquals('yellow', $this->user->getDefaultTaskColor()); + } + + public function testGetDefaultColors() + { + $colors = $this->user->getDefaultTaskColors(); + $this->assertNotEmpty($colors); + $this->assertArrayHasKey('red', $colors); + } + + public function testGetColorList() + { + $colors = $this->user->getColorList(); + $this->assertNotEmpty($colors); + $this->assertArrayHasKey('red', $colors); + $this->assertEquals('Red', $colors['red']); + } } diff --git a/tests/integration/Base.php b/tests/integration/Base.php deleted file mode 100644 index 6f3ae076..00000000 --- a/tests/integration/Base.php +++ /dev/null @@ -1,62 +0,0 @@ -exec('DROP DATABASE '.DB_NAME); - $pdo->exec('CREATE DATABASE '.DB_NAME); - $pdo = null; - } elseif (DB_DRIVER === 'postgres') { - $pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); - $pdo->exec('DROP DATABASE '.DB_NAME); - $pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); - $pdo = null; - } - - $service = new Kanboard\ServiceProvider\DatabaseProvider; - - $db = $service->getInstance(); - $db->table('settings')->eq('option', 'api_token')->update(array('value' => API_KEY)); - $db->closeConnection(); - } - - public function setUp() - { - $this->app = new JsonRPC\Client(API_URL); - $this->app->authentication('jsonrpc', API_KEY); - $this->app->getHttpClient()->withDebug(); - - $this->admin = new JsonRPC\Client(API_URL); - $this->admin->authentication('admin', 'admin'); - $this->admin->getHttpClient()->withDebug(); - - $this->user = new JsonRPC\Client(API_URL); - $this->user->authentication('user', 'password'); - $this->user->getHttpClient()->withDebug(); - } - - protected function getProjectId() - { - $projects = $this->app->getAllProjects(); - $this->assertNotEmpty($projects); - return $projects[0]['id']; - } - - protected function getGroupId() - { - $groups = $this->app->getAllGroups(); - $this->assertNotEmpty($groups); - return $groups[0]['id']; - } -} diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php new file mode 100644 index 00000000..cd837173 --- /dev/null +++ b/tests/integration/BaseIntegrationTest.php @@ -0,0 +1,122 @@ +setUpAppClient(); + $this->setUpAdminUser(); + $this->setUpManagerUser(); + $this->setUpStandardUser(); + } + + public function setUpAppClient() + { + $this->app = new JsonRPC\Client(API_URL); + $this->app->authentication('jsonrpc', API_KEY); + $this->app->getHttpClient()->withDebug()->withTimeout(10); + } + + public function setUpAdminUser() + { + $this->adminUserId = $this->getUserId('superuser'); + + if (! $this->adminUserId) { + $this->adminUserId = $this->app->createUser('superuser', 'password', 'Admin User', 'user@localhost', 'app-admin'); + $this->assertNotFalse($this->adminUserId); + } + + $this->admin = new JsonRPC\Client(API_URL); + $this->admin->authentication('superuser', 'password'); + $this->admin->getHttpClient()->withDebug(); + } + + public function setUpManagerUser() + { + $this->managerUserId = $this->getUserId('manager'); + + if (! $this->managerUserId) { + $this->managerUserId = $this->app->createUser('manager', 'password', 'Manager User', 'user@localhost', 'app-manager'); + $this->assertNotFalse($this->managerUserId); + } + + $this->manager = new JsonRPC\Client(API_URL); + $this->manager->authentication('manager', 'password'); + $this->manager->getHttpClient()->withDebug(); + } + + public function setUpStandardUser() + { + $this->userUserId = $this->getUserId('user'); + + if (! $this->userUserId) { + $this->userUserId = $this->app->createUser('user', 'password', 'Standard User', 'user@localhost', 'app-user'); + $this->assertNotFalse($this->userUserId); + } + + $this->user = new JsonRPC\Client(API_URL); + $this->user->authentication('user', 'password'); + $this->user->getHttpClient()->withDebug(); + } + + public function getUserId($username) + { + $user = $this->app->getUserByName($username); + + if (! empty($user)) { + return $user['id']; + } + + return 0; + } + + public function assertCreateTeamProject() + { + $this->projectId = $this->app->createProject($this->projectName, 'Description'); + $this->assertNotFalse($this->projectId); + } + + public function assertCreateUser() + { + $this->userId = $this->app->createUser($this->username, 'password'); + $this->assertNotFalse($this->userId); + } + + public function assertCreateGroups() + { + $this->groupId1 = $this->app->createGroup($this->groupName1); + $this->groupId2 = $this->app->createGroup($this->groupName2, 'External ID'); + $this->assertNotFalse($this->groupId1); + $this->assertNotFalse($this->groupId2); + } + + public function assertCreateTask() + { + $this->taskId = $this->app->createTask(array('title' => $this->taskTitle, 'project_id' => $this->projectId)); + $this->assertNotFalse($this->taskId); + } +} diff --git a/tests/integration/BoardTest.php b/tests/integration/BoardTest.php index bf8d50b9..aa0f61ff 100644 --- a/tests/integration/BoardTest.php +++ b/tests/integration/BoardTest.php @@ -1,17 +1,21 @@ assertEquals(1, $this->app->createProject('A project')); + $this->assertCreateTeamProject(); + $this->assertGetBoard(); } - public function testGetBoard() + public function assertGetBoard() { - $board = $this->app->getBoard(1); + $board = $this->app->getBoard($this->projectId); + $this->assertNotNull($board); $this->assertCount(1, $board); $this->assertEquals('Default swimlane', $board[0]['name']); diff --git a/tests/integration/CategoryTest.php b/tests/integration/CategoryTest.php new file mode 100644 index 00000000..c1aec0bc --- /dev/null +++ b/tests/integration/CategoryTest.php @@ -0,0 +1,76 @@ +assertCreateTeamProject(); + $this->assertCreateCategory(); + $this->assertThatCategoriesAreUnique(); + $this->assertGetCategory(); + $this->assertGetAllCategories(); + $this->assertCategoryUpdate(); + $this->assertRemoveCategory(); + } + + public function assertCreateCategory() + { + $this->categoryId = $this->app->createCategory(array( + 'name' => 'Category', + 'project_id' => $this->projectId, + )); + + $this->assertNotFalse($this->categoryId); + } + + public function assertThatCategoriesAreUnique() + { + $this->assertFalse($this->app->execute('createCategory', array( + 'name' => 'Category', + 'project_id' => $this->projectId, + ))); + } + + public function assertGetCategory() + { + $category = $this->app->getCategory($this->categoryId); + + $this->assertInternalType('array', $category); + $this->assertEquals($this->categoryId, $category['id']); + $this->assertEquals('Category', $category['name']); + $this->assertEquals($this->projectId, $category['project_id']); + } + + public function assertGetAllCategories() + { + $categories = $this->app->getAllCategories($this->projectId); + + $this->assertCount(1, $categories); + $this->assertEquals($this->categoryId, $categories[0]['id']); + $this->assertEquals('Category', $categories[0]['name']); + $this->assertEquals($this->projectId, $categories[0]['project_id']); + } + + public function assertCategoryUpdate() + { + $this->assertTrue($this->app->execute('updateCategory', array( + 'id' => $this->categoryId, + 'name' => 'Renamed category', + ))); + + $category = $this->app->getCategory($this->categoryId); + $this->assertEquals('Renamed category', $category['name']); + } + + public function assertRemoveCategory() + { + $this->assertTrue($this->app->removeCategory($this->categoryId)); + $this->assertFalse($this->app->removeCategory($this->categoryId)); + $this->assertFalse($this->app->removeCategory(1111)); + } +} diff --git a/tests/integration/ColumnTest.php b/tests/integration/ColumnTest.php index 6d02afc0..5a81badc 100644 --- a/tests/integration/ColumnTest.php +++ b/tests/integration/ColumnTest.php @@ -1,65 +1,69 @@ assertEquals(1, $this->app->createProject('A project')); + $this->assertCreateTeamProject(); + $this->assertGetColumns(); + $this->assertUpdateColumn(); + $this->assertAddColumn(); + $this->assertRemoveColumn(); + $this->assertChangeColumnPosition(); } - public function testGetColumns() + public function assertGetColumns() { - $columns = $this->app->getColumns($this->getProjectId()); - $this->assertCount(4, $columns); - $this->assertEquals('Done', $columns[3]['title']); + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(4, $this->columns); + $this->assertEquals('Done', $this->columns[3]['title']); } - public function testUpdateColumn() + public function assertUpdateColumn() { - $this->assertTrue($this->app->updateColumn(4, 'Boo', 2)); + $this->assertTrue($this->app->updateColumn($this->columns[3]['id'], 'Another column', 2)); - $columns = $this->app->getColumns($this->getProjectId()); - $this->assertEquals('Boo', $columns[3]['title']); - $this->assertEquals(2, $columns[3]['task_limit']); + $this->columns = $this->app->getColumns($this->projectId); + $this->assertEquals('Another column', $this->columns[3]['title']); + $this->assertEquals(2, $this->columns[3]['task_limit']); } - public function testAddColumn() + public function assertAddColumn() { - $column_id = $this->app->addColumn($this->getProjectId(), 'New column'); - + $column_id = $this->app->addColumn($this->projectId, 'New column'); $this->assertNotFalse($column_id); - $this->assertInternalType('int', $column_id); $this->assertTrue($column_id > 0); - $columns = $this->app->getColumns($this->getProjectId()); - $this->assertCount(5, $columns); - $this->assertEquals('New column', $columns[4]['title']); + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(5, $this->columns); + $this->assertEquals('New column', $this->columns[4]['title']); } - public function testRemoveColumn() + public function assertRemoveColumn() { - $this->assertTrue($this->app->removeColumn(5)); + $this->assertTrue($this->app->removeColumn($this->columns[3]['id'])); - $columns = $this->app->getColumns($this->getProjectId()); - $this->assertCount(4, $columns); + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(4, $this->columns); } - public function testChangeColumnPosition() + public function assertChangeColumnPosition() { - $this->assertTrue($this->app->changeColumnPosition($this->getProjectId(), 1, 3)); - - $columns = $this->app->getColumns($this->getProjectId()); - $this->assertCount(4, $columns); + $this->assertTrue($this->app->changeColumnPosition($this->projectId, $this->columns[0]['id'], 3)); - $this->assertEquals('Ready', $columns[0]['title']); - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals('Work in progress', $columns[1]['title']); - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals('Backlog', $columns[2]['title']); - $this->assertEquals(3, $columns[2]['position']); - $this->assertEquals('Boo', $columns[3]['title']); - $this->assertEquals(4, $columns[3]['position']); + $this->columns = $this->app->getColumns($this->projectId); + $this->assertEquals('Ready', $this->columns[0]['title']); + $this->assertEquals(1, $this->columns[0]['position']); + $this->assertEquals('Work in progress', $this->columns[1]['title']); + $this->assertEquals(2, $this->columns[1]['position']); + $this->assertEquals('Backlog', $this->columns[2]['title']); + $this->assertEquals(3, $this->columns[2]['position']); + $this->assertEquals('New column', $this->columns[3]['title']); + $this->assertEquals(4, $this->columns[3]['position']); } } diff --git a/tests/integration/CommentTest.php b/tests/integration/CommentTest.php new file mode 100644 index 00000000..34376838 --- /dev/null +++ b/tests/integration/CommentTest.php @@ -0,0 +1,63 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateComment(); + $this->assertUpdateComment(); + $this->assertGetAllComments(); + $this->assertRemoveComment(); + } + + public function assertCreateComment() + { + $this->commentId = $this->app->execute('createComment', array( + 'task_id' => $this->taskId, + 'user_id' => 1, + 'content' => 'foobar', + )); + + $this->assertNotFalse($this->commentId); + } + + public function assertGetComment() + { + $comment = $this->app->getComment($this->commentId); + $this->assertNotFalse($comment); + $this->assertNotEmpty($comment); + $this->assertEquals(1, $comment['user_id']); + $this->assertEquals('foobar', $comment['comment']); + } + + public function assertUpdateComment() + { + $this->assertTrue($this->app->execute('updateComment', array( + 'id' => $this->commentId, + 'content' => 'test', + ))); + + $comment = $this->app->getComment($this->commentId); + $this->assertEquals('test', $comment['comment']); + } + + public function assertGetAllComments() + { + $comments = $this->app->getAllComments($this->taskId); + $this->assertCount(1, $comments); + $this->assertEquals('test', $comments[0]['comment']); + } + + public function assertRemoveComment() + { + $this->assertTrue($this->app->removeComment($this->commentId)); + $this->assertFalse($this->app->removeComment($this->commentId)); + } +} diff --git a/tests/integration/GroupMemberTest.php b/tests/integration/GroupMemberTest.php index d49945b5..f79499a4 100644 --- a/tests/integration/GroupMemberTest.php +++ b/tests/integration/GroupMemberTest.php @@ -1,47 +1,53 @@ assertNotFalse($this->app->createGroup('My Group A')); - $this->assertNotFalse($this->app->createGroup('My Group B')); + $this->assertCreateGroups(); + $this->assertCreateUser(); + $this->assertAddMember(); + $this->assertGetMembers(); + $this->assertIsGroupMember(); + $this->assertGetGroups(); + $this->assertRemove(); + } - $groupId = $this->getGroupId(); - $this->assertTrue($this->app->addGroupMember($groupId, 1)); + public function assertAddMember() + { + $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); } - public function testGetMembers() + public function assertGetMembers() { - $groups = $this->app->getAllGroups(); - $members = $this->app->getGroupMembers($groups[0]['id']); + $members = $this->app->getGroupMembers($this->groupId1); $this->assertCount(1, $members); - $this->assertEquals('admin', $members[0]['username']); - - $this->assertSame(array(), $this->app->getGroupMembers($groups[1]['id'])); + $this->assertEquals($this->username, $members[0]['username']); } - public function testIsGroupMember() + public function assertIsGroupMember() { - $groupId = $this->getGroupId(); - $this->assertTrue($this->app->isGroupMember($groupId, 1)); - $this->assertFalse($this->app->isGroupMember($groupId, 2)); + $this->assertTrue($this->app->isGroupMember($this->groupId1, $this->userId)); + $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->adminUserId)); } - public function testGetGroups() + public function assertGetGroups() { - $groups = $this->app->getMemberGroups(1); + $groups = $this->app->getMemberGroups($this->userId); $this->assertCount(1, $groups); - $this->assertEquals(1, $groups[0]['id']); - $this->assertEquals('My Group A', $groups[0]['name']); + $this->assertEquals($this->groupId1, $groups[0]['id']); + $this->assertEquals($this->groupName1, $groups[0]['name']); } - public function testRemove() + public function assertRemove() { - $groupId = $this->getGroupId(); - $this->assertTrue($this->app->removeGroupMember($groupId, 1)); - $this->assertFalse($this->app->isGroupMember($groupId, 1)); + $this->assertTrue($this->app->removeGroupMember($this->groupId1, $this->userId)); + $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->userId)); } } diff --git a/tests/integration/GroupTest.php b/tests/integration/GroupTest.php index 7a5bccc9..ffcd7a71 100644 --- a/tests/integration/GroupTest.php +++ b/tests/integration/GroupTest.php @@ -1,48 +1,50 @@ assertNotFalse($this->app->createGroup('My Group A')); - $this->assertNotFalse($this->app->createGroup('My Group B', '1234')); + $this->assertCreateGroups(); + $this->assertGetAllGroups(); + $this->assertGetGroup(); + $this->assertUpdateGroup(); + $this->assertRemove(); } - public function testGetter() + public function assertGetAllGroups() { $groups = $this->app->getAllGroups(); - $this->assertCount(2, $groups); - $this->assertEquals('My Group A', $groups[0]['name']); - $this->assertEquals('', $groups[0]['external_id']); - $this->assertEquals('My Group B', $groups[1]['name']); - $this->assertEquals('1234', $groups[1]['external_id']); + $this->assertNotEmpty($groups); + $this->assertArrayHasKey('name', $groups[0]); + $this->assertArrayHasKey('external_id', $groups[0]); + } - $group = $this->app->getGroup($groups[0]['id']); + public function assertGetGroup() + { + $group = $this->app->getGroup($this->groupId1); $this->assertNotEmpty($group); - $this->assertEquals('My Group A', $group['name']); + $this->assertEquals($this->groupName1, $group['name']); $this->assertEquals('', $group['external_id']); } - public function testUpdate() + public function assertUpdateGroup() { - $groups = $this->app->getAllGroups(); - - $this->assertTrue($this->app->updateGroup(array('group_id' => $groups[0]['id'], 'name' => 'ABC', 'external_id' => 'something'))); - $this->assertTrue($this->app->updateGroup(array('group_id' => $groups[1]['id'], 'external_id' => ''))); + $this->assertTrue($this->app->updateGroup(array( + 'group_id' => $this->groupId2, + 'name' => 'My Group C', + 'external_id' => 'something else', + ))); - $groups = $this->app->getAllGroups(); - $this->assertEquals('ABC', $groups[0]['name']); - $this->assertEquals('something', $groups[0]['external_id']); - $this->assertEquals('', $groups[1]['external_id']); + $group = $this->app->getGroup($this->groupId2); + $this->assertNotEmpty($group); + $this->assertEquals('My Group C', $group['name']); + $this->assertEquals('something else', $group['external_id']); } - public function testRemove() + public function assertRemove() { - $groups = $this->app->getAllGroups(); - $this->assertTrue($this->app->removeGroup($groups[0]['id'])); - $this->assertTrue($this->app->removeGroup($groups[1]['id'])); - $this->assertSame(array(), $this->app->getAllGroups()); + $this->assertTrue($this->app->removeGroup($this->groupId1)); } } diff --git a/tests/integration/LinkTest.php b/tests/integration/LinkTest.php new file mode 100644 index 00000000..16b16e50 --- /dev/null +++ b/tests/integration/LinkTest.php @@ -0,0 +1,70 @@ +app->getAllLinks(); + $this->assertNotEmpty($links); + $this->assertArrayHasKey('id', $links[0]); + $this->assertArrayHasKey('label', $links[0]); + $this->assertArrayHasKey('opposite_id', $links[0]); + } + + public function testGetOppositeLink() + { + $link = $this->app->getOppositeLinkId(1); + $this->assertEquals(1, $link); + + $link = $this->app->getOppositeLinkId(2); + $this->assertEquals(3, $link); + } + + public function testGetLinkByLabel() + { + $link = $this->app->getLinkByLabel('blocks'); + $this->assertNotEmpty($link); + $this->assertEquals(2, $link['id']); + $this->assertEquals(3, $link['opposite_id']); + } + + public function testGetLinkById() + { + $link = $this->app->getLinkById(4); + $this->assertNotEmpty($link); + $this->assertEquals(4, $link['id']); + $this->assertEquals(5, $link['opposite_id']); + $this->assertEquals('duplicates', $link['label']); + } + + public function testCreateLink() + { + $link_id = $this->app->createLink(array('label' => 'test')); + $this->assertNotFalse($link_id); + $this->assertInternalType('int', $link_id); + + $link_id = $this->app->createLink(array('label' => 'foo', 'opposite_label' => 'bar')); + $this->assertNotFalse($link_id); + $this->assertInternalType('int', $link_id); + } + + public function testUpdateLink() + { + $link1 = $this->app->getLinkByLabel('bar'); + $this->assertNotEmpty($link1); + + $link2 = $this->app->getLinkByLabel('test'); + $this->assertNotEmpty($link2); + + $this->assertNotFalse($this->app->updateLink($link1['id'], $link2['id'], 'my link')); + + $link = $this->app->getLinkById($link1['id']); + $this->assertNotEmpty($link); + $this->assertEquals($link2['id'], $link['opposite_id']); + $this->assertEquals('my link', $link['label']); + + $this->assertTrue($this->app->removeLink($link1['id'])); + } +} diff --git a/tests/integration/MeTest.php b/tests/integration/MeTest.php index 1b028b84..047ebf85 100644 --- a/tests/integration/MeTest.php +++ b/tests/integration/MeTest.php @@ -1,207 +1,60 @@ assertEquals(1, $this->app->createProject('team project')); - } - - public function testCreateUser() - { - $this->assertEquals(2, $this->app->createUser('user', 'password')); - } - - /** - * @expectedException JsonRPC\Exception\AccessDeniedException - */ - public function testNotAllowedAppProcedure() - { - $this->app->getMe(); - } - - /** - * @expectedException JsonRPC\Exception\AccessDeniedException - */ - public function testNotAllowedUserProcedure() - { - $this->user->getAllProjects(); - } + protected $projectName = 'My private project'; - /** - * @expectedException JsonRPC\Exception\AccessDeniedException - */ - public function testNotAllowedProjectForUser() + public function testAll() { - $this->user->getProjectById(1); + $this->assertGetMe(); + $this->assertCreateMyPrivateProject(); + $this->assertGetMyProjectsList(); + $this->assertGetMyProjects(); + $this->assertCreateTask(); + $this->assertGetMyDashboard(); + $this->assertGetMyActivityStream(); } - public function testAllowedProjectForAdmin() - { - $this->assertNotEmpty($this->admin->getProjectById(1)); - } - - public function testGetTimezone() - { - $this->assertEquals('UTC', $this->user->getTimezone()); - } - - public function testGetVersion() - { - $this->assertEquals('master', $this->user->getVersion()); - } - - public function testGetDefaultColor() - { - $this->assertEquals('yellow', $this->user->getDefaultTaskColor()); - } - - public function testGetDefaultColors() - { - $colors = $this->user->getDefaultTaskColors(); - $this->assertNotEmpty($colors); - $this->assertArrayHasKey('red', $colors); - } - - public function testGetColorList() - { - $colors = $this->user->getColorList(); - $this->assertNotEmpty($colors); - $this->assertArrayHasKey('red', $colors); - $this->assertEquals('Red', $colors['red']); - } - - public function testGetMe() + public function assertGetMe() { $profile = $this->user->getMe(); - $this->assertNotEmpty($profile); - $this->assertEquals(2, $profile['id']); $this->assertEquals('user', $profile['username']); + $this->assertEquals('app-user', $profile['role']); } - public function testCreateMyPrivateProject() + public function assertCreateMyPrivateProject() { - $this->assertEquals(2, $this->user->createMyPrivateProject('my project')); + $this->projectId = $this->user->createMyPrivateProject($this->projectName); + $this->assertNotFalse($this->projectId); } - public function testGetMyProjectsList() + public function assertGetMyProjectsList() { $projects = $this->user->getMyProjectsList(); $this->assertNotEmpty($projects); - $this->assertArrayNotHasKey(1, $projects); - $this->assertArrayHasKey(2, $projects); - $this->assertEquals('my project', $projects[2]); + $this->assertEquals($this->projectName, $projects[$this->projectId]); } - public function testGetMyProjects() + public function assertGetMyProjects() { $projects = $this->user->getMyProjects(); $this->assertNotEmpty($projects); $this->assertCount(1, $projects); - $this->assertEquals(2, $projects[0]['id']); - $this->assertEquals('my project', $projects[0]['name']); + $this->assertEquals($this->projectName, $projects[0]['name']); $this->assertNotEmpty($projects[0]['url']['calendar']); $this->assertNotEmpty($projects[0]['url']['board']); $this->assertNotEmpty($projects[0]['url']['list']); } - public function testGetProjectById() - { - $project = $this->user->getProjectById(2); - $this->assertNotEmpty($project); - $this->assertEquals('my project', $project['name']); - $this->assertEquals(1, $project['is_private']); - } - - public function testCreateTask() - { - $this->assertEquals(1, $this->user->createTask('my user title', 2)); - $this->assertEquals(2, $this->admin->createTask('my admin title', 1)); - } - - public function testCreateTaskWithWrongMember() - { - $this->assertFalse($this->user->createTask(array('title' => 'something', 'project_id' => 2, 'owner_id' => 1))); - $this->assertFalse($this->app->createTask(array('title' => 'something', 'project_id' => 1, 'owner_id' => 2))); - } - - public function testGetTask() - { - $task = $this->user->getTask(1); - $this->assertNotEmpty($task); - $this->assertEquals('my user title', $task['title']); - $this->assertEquals('yellow', $task['color_id']); - $this->assertArrayHasKey('color', $task); - $this->assertArrayHasKey('name', $task['color']); - $this->assertArrayHasKey('border', $task['color']); - $this->assertArrayHasKey('background', $task['color']); - } - - /** - * @expectedException JsonRPC\Exception\AccessDeniedException - */ - public function testGetAdminTask() + public function assertCreateTask() { - $this->user->getTask(2); + $taskId = $this->user->createTask(array('title' => 'My task', 'project_id' => $this->projectId, 'owner_id' => $this->userUserId)); + $this->assertNotFalse($taskId); } - /** - * @expectedException JsonRPC\Exception\AccessDeniedException - */ - public function testGetProjectActivityDenied() - { - $this->user->getProjectActivity(1); - } - - public function testGetProjectActivityAllowed() - { - $activity = $this->user->getProjectActivity(2); - $this->assertNotEmpty($activity); - } - - public function testGetMyActivityStream() - { - $activity = $this->user->getMyActivityStream(); - $this->assertNotEmpty($activity); - } - - public function testCloseTask() - { - $this->assertTrue($this->user->closeTask(1)); - } - - public function testOpenTask() - { - $this->assertTrue($this->user->openTask(1)); - } - - public function testMoveTaskPosition() - { - $this->assertTrue($this->user->moveTaskPosition(2, 1, 2, 1)); - } - - public function testUpdateTaskWithWrongMember() - { - $this->assertFalse($this->user->updateTask(array('id' => 1, 'title' => 'new title', 'reference' => 'test', 'owner_id' => 1))); - } - - public function testUpdateTask() - { - $this->assertTrue($this->user->updateTask(array('id' => 1, 'title' => 'new title', 'reference' => 'test', 'owner_id' => 2))); - } - - public function testGetbyReference() - { - $task = $this->user->getTaskByReference(2, 'test'); - $this->assertNotEmpty($task); - $this->assertEquals('new title', $task['title']); - $this->assertEquals(2, $task['column_id']); - $this->assertEquals(1, $task['position']); - } - - public function testGetMyDashboard() + public function assertGetMyDashboard() { $dashboard = $this->user->getMyDashboard(); $this->assertNotEmpty($dashboard); @@ -212,36 +65,9 @@ class MeTest extends Base $this->assertNotEmpty($dashboard['tasks']); } - public function testGetBoard() - { - $this->assertNotEmpty($this->user->getBoard(2)); - } - - public function testCreateOverdueTask() - { - $this->assertNotFalse($this->user->createTask(array( - 'title' => 'overdue task', - 'project_id' => 2, - 'date_due' => date('Y-m-d', strtotime('-2days')), - 'owner_id' => 2, - ))); - } - - public function testGetMyOverdueTasks() + public function assertGetMyActivityStream() { - $tasks = $this->user->getMyOverdueTasks(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals('my project', $tasks[0]['project_name']); - } - - public function testGetOverdueTasksByProject() - { - $tasks = $this->user->getOverdueTasksByProject(2); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals('my project', $tasks[0]['project_name']); + $activity = $this->user->getMyActivityStream(); + $this->assertNotEmpty($activity); } } diff --git a/tests/integration/OverdueTaskTest.php b/tests/integration/OverdueTaskTest.php new file mode 100644 index 00000000..1dea5030 --- /dev/null +++ b/tests/integration/OverdueTaskTest.php @@ -0,0 +1,43 @@ +assertCreateTeamProject(); + $this->assertCreateOverdueTask(); + $this->assertGetOverdueTasksByProject(); + $this->assertGetOverdueTasks(); + } + + public function assertCreateOverdueTask() + { + $this->assertNotFalse($this->app->createTask(array( + 'title' => 'overdue task', + 'project_id' => $this->projectId, + 'date_due' => date('Y-m-d', strtotime('-2days')), + ))); + } + + public function assertGetOverdueTasksByProject() + { + $tasks = $this->app->getOverdueTasksByProject($this->projectId); + $this->assertNotEmpty($tasks); + $this->assertCount(1, $tasks); + $this->assertEquals('overdue task', $tasks[0]['title']); + $this->assertEquals($this->projectName, $tasks[0]['project_name']); + } + + public function assertGetOverdueTasks() + { + $tasks = $this->app->getOverdueTasks(); + $this->assertNotEmpty($tasks); + $this->assertCount(1, $tasks); + $this->assertEquals('overdue task', $tasks[0]['title']); + $this->assertEquals($this->projectName, $tasks[0]['project_name']); + } +} diff --git a/tests/integration/ProjectPermissionTest.php b/tests/integration/ProjectPermissionTest.php index b06ad4ad..3ceda07d 100644 --- a/tests/integration/ProjectPermissionTest.php +++ b/tests/integration/ProjectPermissionTest.php @@ -1,64 +1,89 @@ assertNotFalse($this->app->createProject('Test')); - $this->assertNotFalse($this->app->createGroup('Test')); - - $projectId = $this->getProjectId(); - $groupId = $this->getGroupId(); + protected $projectName = 'Project with permission'; + protected $username = 'user-project-permission'; + protected $groupName1 = 'My group A for project permission'; + protected $groupName2 = 'My group B for project permission'; - $this->assertTrue($this->app->addGroupMember($projectId, $groupId)); - $this->assertSame(array(), $this->app->getProjectUsers($projectId)); + public function testAll() + { + $this->assertCreateTeamProject(); + $this->assertCreateGroups(); + $this->assertCreateUser(); + + $this->assertAddProjectUser(); + $this->assertGetProjectUsers(); + $this->assertGetAssignableUsers(); + $this->assertChangeProjectUserRole(); + $this->assertRemoveProjectUser(); + + $this->assertAddProjectGroup(); + $this->assertGetProjectUsers(); + $this->assertGetAssignableUsers(); + $this->assertChangeProjectGroupRole(); + $this->assertRemoveProjectGroup(); } - public function testProjectUser() + public function assertAddProjectUser() { - $projectId = $this->getProjectId(); - $this->assertTrue($this->app->addProjectUser($projectId, 1)); - - $users = $this->app->getProjectUsers($projectId); - $this->assertCount(1, $users); - $this->assertEquals('admin', $users[1]); + $this->assertTrue($this->app->addProjectUser($this->projectId, $this->userId)); + } - $users = $this->app->getAssignableUsers($projectId); - $this->assertCount(1, $users); - $this->assertEquals('admin', $users[1]); + public function assertGetProjectUsers() + { + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(1, $members); + $this->assertArrayHasKey($this->userId, $members); + $this->assertEquals($this->username, $members[$this->userId]); + } - $this->assertTrue($this->app->changeProjectUserRole($projectId, 1, 'project-viewer')); + public function assertGetAssignableUsers() + { + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(1, $members); + $this->assertArrayHasKey($this->userId, $members); + $this->assertEquals($this->username, $members[$this->userId]); + } - $users = $this->app->getAssignableUsers($projectId); - $this->assertCount(0, $users); + public function assertChangeProjectUserRole() + { + $this->assertTrue($this->app->changeProjectUserRole($this->projectId, $this->userId, 'project-viewer')); - $this->assertTrue($this->app->removeProjectUser($projectId, 1)); - $this->assertSame(array(), $this->app->getProjectUsers($projectId)); + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(0, $members); } - public function testProjectGroup() + public function assertRemoveProjectUser() { - $projectId = $this->getProjectId(); - $groupId = $this->getGroupId(); + $this->assertTrue($this->app->removeProjectUser($this->projectId, $this->userId)); - $this->assertTrue($this->app->addProjectGroup($projectId, $groupId)); + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(0, $members); + } - $users = $this->app->getProjectUsers($projectId); - $this->assertCount(1, $users); - $this->assertEquals('admin', $users[1]); + public function assertAddProjectGroup() + { + $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); + $this->assertTrue($this->app->addProjectGroup($this->projectId, $this->groupId1)); + } - $users = $this->app->getAssignableUsers($projectId); - $this->assertCount(1, $users); - $this->assertEquals('admin', $users[1]); + public function assertChangeProjectGroupRole() + { + $this->assertTrue($this->app->changeProjectGroupRole($this->projectId, $this->groupId1, 'project-viewer')); - $this->assertTrue($this->app->changeProjectGroupRole($projectId, $groupId, 'project-viewer')); + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(0, $members); + } - $users = $this->app->getAssignableUsers($projectId); - $this->assertCount(0, $users); + public function assertRemoveProjectGroup() + { + $this->assertTrue($this->app->removeProjectGroup($this->projectId, $this->groupId1)); - $this->assertTrue($this->app->removeProjectGroup($projectId, 1)); - $this->assertSame(array(), $this->app->getProjectUsers($projectId)); + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(0, $members); } } diff --git a/tests/integration/ProjectTest.php b/tests/integration/ProjectTest.php new file mode 100644 index 00000000..50d4fc53 --- /dev/null +++ b/tests/integration/ProjectTest.php @@ -0,0 +1,89 @@ +assertCreateTeamProject(); + $this->assertGetProjectById(); + $this->assertGetProjectByName(); + $this->assertGetAllProjects(); + $this->assertUpdateProject(); + $this->assertGetProjectActivity(); + $this->assertGetProjectsActivity(); + $this->assertEnableDisableProject(); + $this->assertEnableDisablePublicAccess(); + $this->assertRemoveProject(); + } + + public function assertGetProjectById() + { + $project = $this->app->getProjectById($this->projectId); + $this->assertNotNull($project); + $this->assertEquals($this->projectName, $project['name']); + $this->assertEquals('Description', $project['description']); + } + + public function assertGetProjectByName() + { + $project = $this->app->getProjectByName($this->projectName); + $this->assertNotNull($project); + $this->assertEquals($this->projectId, $project['id']); + $this->assertEquals($this->projectName, $project['name']); + $this->assertEquals('Description', $project['description']); + } + + public function assertGetAllProjects() + { + $projects = $this->app->getAllProjects(); + $this->assertNotEmpty($projects); + } + + public function assertGetProjectActivity() + { + $activities = $this->app->getProjectActivity($this->projectId); + $this->assertInternalType('array', $activities); + $this->assertCount(0, $activities); + } + + public function assertGetProjectsActivity() + { + $activities = $this->app->getProjectActivities(array('project_ids' => array($this->projectId))); + $this->assertInternalType('array', $activities); + $this->assertCount(0, $activities); + } + + public function assertUpdateProject() + { + $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => 'test', 'description' => 'test'))); + + $project = $this->app->getProjectById($this->projectId); + $this->assertNotNull($project); + $this->assertEquals('test', $project['name']); + $this->assertEquals('test', $project['description']); + + $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => $this->projectName))); + } + + public function assertEnableDisableProject() + { + $this->assertTrue($this->app->disableProject($this->projectId)); + $this->assertTrue($this->app->enableProject($this->projectId)); + } + + public function assertEnableDisablePublicAccess() + { + $this->assertTrue($this->app->disableProjectPublicAccess($this->projectId)); + $this->assertTrue($this->app->enableProjectPublicAccess($this->projectId)); + } + + public function assertRemoveProject() + { + $this->assertTrue($this->app->removeProject($this->projectId)); + $this->assertNull($this->app->getProjectById($this->projectId)); + } +} diff --git a/tests/integration/SubtaskTest.php b/tests/integration/SubtaskTest.php new file mode 100644 index 00000000..10082e60 --- /dev/null +++ b/tests/integration/SubtaskTest.php @@ -0,0 +1,64 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateSubtask(); + $this->assertGetSubtask(); + $this->assertUpdateSubtask(); + $this->assertGetAllSubtasks(); + $this->assertRemoveSubtask(); + } + + public function assertCreateSubtask() + { + $this->subtaskId = $this->app->createSubtask(array( + 'task_id' => $this->taskId, + 'title' => 'subtask #1', + )); + + $this->assertNotFalse($this->subtaskId); + } + + public function assertGetSubtask() + { + $subtask = $this->app->getSubtask($this->subtaskId); + $this->assertEquals($this->taskId, $subtask['task_id']); + $this->assertEquals('subtask #1', $subtask['title']); + } + + public function assertUpdateSubtask() + { + $this->assertTrue($this->app->execute('updateSubtask', array( + 'id' => $this->subtaskId, + 'task_id' => $this->taskId, + 'title' => 'test', + ))); + + $subtask = $this->app->getSubtask($this->subtaskId); + $this->assertEquals('test', $subtask['title']); + } + + public function assertGetAllSubtasks() + { + $subtasks = $this->app->getAllSubtasks($this->taskId); + $this->assertCount(1, $subtasks); + $this->assertEquals('test', $subtasks[0]['title']); + } + + public function assertRemoveSubtask() + { + $this->assertTrue($this->app->removeSubtask($this->subtaskId)); + + $subtasks = $this->app->getAllSubtasks($this->taskId); + $this->assertCount(0, $subtasks); + } +} diff --git a/tests/integration/SwimlaneTest.php b/tests/integration/SwimlaneTest.php index 88747204..4f703414 100644 --- a/tests/integration/SwimlaneTest.php +++ b/tests/integration/SwimlaneTest.php @@ -1,103 +1,93 @@ assertEquals(1, $this->app->createProject('A project')); + $this->assertCreateTeamProject(); } - public function testGetDefaultSwimlane() + public function assertGetDefaultSwimlane() { - $swimlane = $this->app->getDefaultSwimlane(1); + $swimlane = $this->app->getDefaultSwimlane($this->projectId); $this->assertNotEmpty($swimlane); $this->assertEquals('Default swimlane', $swimlane['default_swimlane']); } - public function testAddSwimlane() + public function assertAddSwimlane() { - $swimlane_id = $this->app->addSwimlane(1, 'Swimlane 1'); - $this->assertNotFalse($swimlane_id); - $this->assertInternalType('int', $swimlane_id); - - $swimlane = $this->app->getSwimlaneById($swimlane_id); - $this->assertNotEmpty($swimlane); - $this->assertInternalType('array', $swimlane); - $this->assertEquals('Swimlane 1', $swimlane['name']); + $this->swimlaneId = $this->app->addSwimlane($this->projectId, 'Swimlane 1'); + $this->assertNotFalse($this->swimlaneId); + $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane 2')); } - public function testGetSwimlane() + public function assertGetSwimlane() { - $swimlane = $this->app->getSwimlane(1); + $swimlane = $this->app->getSwimlane($this->swimlaneId); $this->assertInternalType('array', $swimlane); $this->assertEquals('Swimlane 1', $swimlane['name']); } - public function testUpdateSwimlane() + public function assertUpdateSwimlane() { - $swimlane = $this->app->getSwimlaneByName(1, 'Swimlane 1'); - $this->assertInternalType('array', $swimlane); - $this->assertEquals(1, $swimlane['id']); - $this->assertEquals('Swimlane 1', $swimlane['name']); + $this->assertTrue($this->app->updateSwimlane($this->swimlaneId, 'Another swimlane')); - $this->assertTrue($this->app->updateSwimlane($swimlane['id'], 'Another swimlane')); - - $swimlane = $this->app->getSwimlaneById($swimlane['id']); + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); $this->assertEquals('Another swimlane', $swimlane['name']); } - public function testDisableSwimlane() + public function assertDisableSwimlane() { - $this->assertTrue($this->app->disableSwimlane(1, 1)); + $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); - $swimlane = $this->app->getSwimlaneById(1); + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); $this->assertEquals(0, $swimlane['is_active']); } - public function testEnableSwimlane() + public function assertEnableSwimlane() { - $this->assertTrue($this->app->enableSwimlane(1, 1)); + $this->assertTrue($this->app->enableSwimlane($this->projectId, $this->swimlaneId)); - $swimlane = $this->app->getSwimlaneById(1); + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); $this->assertEquals(1, $swimlane['is_active']); } - public function testGetAllSwimlanes() + public function assertGetAllSwimlanes() { - $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane A')); - - $swimlanes = $this->app->getAllSwimlanes(1); + $swimlanes = $this->app->getAllSwimlanes($this->projectId); $this->assertCount(2, $swimlanes); $this->assertEquals('Another swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane A', $swimlanes[1]['name']); + $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); } - public function testGetActiveSwimlane() + public function assertGetActiveSwimlane() { - $this->assertTrue($this->app->disableSwimlane(1, 1)); + $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); - $swimlanes = $this->app->getActiveSwimlanes(1); + $swimlanes = $this->app->getActiveSwimlanes($this->projectId); $this->assertCount(2, $swimlanes); $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane A', $swimlanes[1]['name']); + $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); } - public function testRemoveSwimlane() + public function assertRemoveSwimlane() { - $this->assertTrue($this->app->removeSwimlane(1, 2)); + $this->assertTrue($this->app->removeSwimlane($this->projectId, $this->swimlaneId)); } - public function testChangePosition() + public function assertChangePosition() { - $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane 1')); - $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane 2')); + $swimlaneId1 = $this->app->addSwimlane($this->projectId, 'Swimlane A'); + $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane B')); - $swimlanes = $this->app->getAllSwimlanes(1); + $swimlanes = $this->app->getAllSwimlanes($this->projectId); $this->assertCount(3, $swimlanes); - $this->assertTrue($this->app->changeSwimlanePosition(1, 1, 3)); - $this->assertFalse($this->app->changeSwimlanePosition(1, 1, 6)); + $this->assertTrue($this->app->changeSwimlanePosition($this->projectId, $swimlaneId1, 3)); } } diff --git a/tests/integration/TaskFileTest.php b/tests/integration/TaskFileTest.php new file mode 100644 index 00000000..7e9e943b --- /dev/null +++ b/tests/integration/TaskFileTest.php @@ -0,0 +1,67 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateTaskFile(); + $this->assertGetTaskFile(); + $this->assertDownloadTaskFile(); + $this->assertGetAllFiles(); + $this->assertRemoveTaskFile(); + $this->assertRemoveAllTaskFiles(); + } + + public function assertCreateTaskFile() + { + $this->fileId = $this->app->createTaskFile(1, $this->taskId, 'My file', base64_encode('plain text file')); + $this->assertNotFalse($this->fileId); + } + + public function assertGetTaskFile() + { + $file = $this->app->getTaskFile($this->fileId); + $this->assertNotEmpty($file); + $this->assertEquals('My file', $file['name']); + } + + public function assertDownloadTaskFile() + { + $content = $this->app->downloadTaskFile($this->fileId); + $this->assertNotEmpty($content); + $this->assertEquals('plain text file', base64_decode($content)); + } + + public function assertGetAllFiles() + { + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertCount(1, $files); + $this->assertEquals('My file', $files[0]['name']); + } + + public function assertRemoveTaskFile() + { + $this->assertTrue($this->app->removeTaskFile($this->fileId)); + + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertEmpty($files); + } + + public function assertRemoveAllTaskFiles() + { + $this->assertCreateTaskFile(); + $this->assertCreateTaskFile(); + + $this->assertTrue($this->app->removeAllTaskFiles($this->taskId)); + + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertEmpty($files); + } +} diff --git a/tests/integration/TaskLinkTest.php b/tests/integration/TaskLinkTest.php new file mode 100644 index 00000000..03bc437b --- /dev/null +++ b/tests/integration/TaskLinkTest.php @@ -0,0 +1,68 @@ +assertCreateTeamProject(); + + $this->taskId1 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 1')); + $this->taskId2 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 2')); + + $this->assertNotFalse($this->taskId1); + $this->assertNotFalse($this->taskId2); + + $this->assertCreateTaskLink(); + $this->assertGetTaskLink(); + $this->assertGetAllTaskLinks(); + $this->assertUpdateTaskLink(); + $this->assertRemoveTaskLink(); + } + + public function assertCreateTaskLink() + { + $this->taskLinkId = $this->app->createTaskLink($this->taskId1, $this->taskId2, 1); + $this->assertNotFalse($this->taskLinkId); + } + + public function assertGetTaskLink() + { + $link = $this->app->getTaskLinkById($this->taskLinkId); + $this->assertNotNull($link); + $this->assertEquals($this->taskId1, $link['task_id']); + $this->assertEquals($this->taskId2, $link['opposite_task_id']); + $this->assertEquals(1, $link['link_id']); + } + + public function assertGetAllTaskLinks() + { + $links = $this->app->getAllTaskLinks($this->taskId2); + $this->assertCount(1, $links); + } + + public function assertUpdateTaskLink() + { + $this->assertTrue($this->app->updateTaskLink($this->taskLinkId, $this->taskId1, $this->taskId2, 3)); + + $link = $this->app->getTaskLinkById($this->taskLinkId); + $this->assertNotNull($link); + $this->assertEquals($this->taskId1, $link['task_id']); + $this->assertEquals($this->taskId2, $link['opposite_task_id']); + $this->assertEquals(3, $link['link_id']); + } + + public function assertRemoveTaskLink() + { + $this->assertTrue($this->app->removeTaskLink($this->taskLinkId)); + + $links = $this->app->getAllTaskLinks($this->taskId2); + $this->assertCount(0, $links); + } +} diff --git a/tests/integration/TaskTest.php b/tests/integration/TaskTest.php index 0c398761..6f1d9d62 100644 --- a/tests/integration/TaskTest.php +++ b/tests/integration/TaskTest.php @@ -1,132 +1,55 @@ app->createProject('My project'); - $project_id2 = $this->app->createProject('My project'); - $this->assertNotFalse($project_id1); - $this->assertNotFalse($project_id2); - - $this->assertNotFalse($this->app->createTask(array('project_id' => $project_id1, 'title' => 'T1'))); - $this->assertNotFalse($this->app->createTask(array('project_id' => $project_id1, 'title' => 'T2'))); - $this->assertNotFalse($this->app->createTask(array('project_id' => $project_id2, 'title' => 'T3'))); - - $tasks = $this->app->searchTasks($project_id1, 't2'); - $this->assertCount(1, $tasks); - $this->assertEquals('T2', $tasks[0]['title']); - - $tasks = $this->app->searchTasks(array('project_id' => $project_id2, 'query' => 'assignee:nobody')); - $this->assertCount(1, $tasks); - $this->assertEquals('T3', $tasks[0]['title']); - } + protected $projectName = 'My project to test tasks'; - public function testPriorityAttribute() + public function testAll() { - $project_id = $this->app->createProject('My project'); - $this->assertNotFalse($project_id); - - $task_id = $this->app->createTask(array('project_id' => $project_id, 'title' => 'My task', 'priority' => 2)); - - $task = $this->app->getTask($task_id); - $this->assertEquals(2, $task['priority']); - - $this->assertTrue($this->app->updateTask(array('id' => $task_id, 'project_id' => $project_id, 'priority' => 3))); - - $task = $this->app->getTask($task_id); - $this->assertEquals(3, $task['priority']); + $this->assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertUpdateTask(); + $this->assertGetTaskById(); + $this->assertGetTaskByReference(); + $this->assertGetAllTasks(); + $this->assertOpenCloseTask(); } - public function testChangeAssigneeToAssignableUser() + public function assertUpdateTask() { - $project_id = $this->app->createProject('My project'); - $this->assertNotFalse($project_id); - - $user_id = $this->app->createUser('user0', 'password'); - $this->assertNotFalse($user_id); - - $this->assertTrue($this->app->addProjectUser($project_id, $user_id, 'project-member')); - - $task_id = $this->app->createTask(array('project_id' => $project_id, 'title' => 'My task')); - $this->assertNotFalse($task_id); - - $this->assertTrue($this->app->updateTask(array('id' => $task_id, 'project_id' => $project_id, 'owner_id' => $user_id))); - - $task = $this->app->getTask($task_id); - $this->assertEquals($user_id, $task['owner_id']); + $this->assertTrue($this->app->updateTask(array('id' => $this->taskId, 'color_id' => 'red'))); } - public function testChangeAssigneeToNotAssignableUser() + public function assertGetTaskById() { - $project_id = $this->app->createProject('My project'); - $this->assertNotFalse($project_id); - - $task_id = $this->app->createTask(array('project_id' => $project_id, 'title' => 'My task')); - $this->assertNotFalse($task_id); - - $this->assertFalse($this->app->updateTask(array('id' => $task_id, 'project_id' => $project_id, 'owner_id' => 1))); - - $task = $this->app->getTask($task_id); - $this->assertEquals(0, $task['owner_id']); + $task = $this->app->getTask($this->taskId); + $this->assertNotNull($task); + $this->assertEquals('red', $task['color_id']); + $this->assertEquals($this->taskTitle, $task['title']); } - public function testChangeAssigneeToNobody() + public function assertGetTaskByReference() { - $project_id = $this->app->createProject('My project'); - $this->assertNotFalse($project_id); - - $user_id = $this->app->createUser('user1', 'password'); - $this->assertNotFalse($user_id); - - $this->assertTrue($this->app->addProjectUser($project_id, $user_id, 'project-member')); + $taskId = $this->app->createTask(array('title' => 'task with reference', 'project_id' => $this->projectId, 'reference' => 'test')); + $this->assertNotFalse($taskId); - $task_id = $this->app->createTask(array('project_id' => $project_id, 'title' => 'My task', 'owner_id' => $user_id)); - $this->assertNotFalse($task_id); - - $this->assertTrue($this->app->updateTask(array('id' => $task_id, 'project_id' => $project_id, 'owner_id' => 0))); - - $task = $this->app->getTask($task_id); - $this->assertEquals(0, $task['owner_id']); + $task = $this->app->getTaskByReference($this->projectId, 'test'); + $this->assertNotNull($task); + $this->assertEquals($taskId, $task['id']); } - public function testMoveTaskToAnotherProject() + public function assertGetAllTasks() { - $project_id1 = $this->app->createProject('My project'); - $this->assertNotFalse($project_id1); - - $project_id2 = $this->app->createProject('My project'); - $this->assertNotFalse($project_id2); - - $task_id = $this->app->createTask(array('project_id' => $project_id1, 'title' => 'My task')); - $this->assertNotFalse($task_id); - - $this->assertTrue($this->app->moveTaskToProject($task_id, $project_id2)); - - $task = $this->app->getTask($task_id); - $this->assertEquals($project_id2, $task['project_id']); + $tasks = $this->app->getAllTasks($this->projectId); + $this->assertInternalType('array', $tasks); + $this->assertNotEmpty($tasks); } - public function testMoveCopyToAnotherProject() + public function assertOpenCloseTask() { - $project_id1 = $this->app->createProject('My project'); - $this->assertNotFalse($project_id1); - - $project_id2 = $this->app->createProject('My project'); - $this->assertNotFalse($project_id2); - - $task_id1 = $this->app->createTask(array('project_id' => $project_id1, 'title' => 'My task')); - $this->assertNotFalse($task_id1); - - $task_id2 = $this->app->duplicateTaskToProject($task_id1, $project_id2); - $this->assertNotFalse($task_id2); - - $task = $this->app->getTask($task_id1); - $this->assertEquals($project_id1, $task['project_id']); - - $task = $this->app->getTask($task_id2); - $this->assertEquals($project_id2, $task['project_id']); + $this->assertTrue($this->app->closeTask($this->taskId)); + $this->assertTrue($this->app->openTask($this->taskId)); } } diff --git a/tests/integration/UserTest.php b/tests/integration/UserTest.php index 10da051c..c407c918 100644 --- a/tests/integration/UserTest.php +++ b/tests/integration/UserTest.php @@ -1,18 +1,63 @@ assertEquals(2, $this->app->createUser(array('username' => 'someone', 'password' => 'test123'))); - $this->assertTrue($this->app->isActiveUser(2)); + $this->assertCreateUser(); + $this->assertGetUserById(); + $this->assertGetUserByName(); + $this->assertGetAllUsers(); + $this->assertEnableDisableUser(); + $this->assertUpdateUser(); + $this->assertRemoveUser(); + } - $this->assertTrue($this->app->disableUser(2)); - $this->assertFalse($this->app->isActiveUser(2)); + public function assertGetUserById() + { + $user = $this->app->getUser($this->userId); + $this->assertNotNull($user); + $this->assertEquals($this->username, $user['username']); + } + + public function assertGetUserByName() + { + $user = $this->app->getUserByName($this->username); + $this->assertNotNull($user); + $this->assertEquals($this->username, $user['username']); + } + + public function assertGetAllUsers() + { + $users = $this->app->getAllUsers(); + $this->assertInternalType('array', $users); + $this->assertNotEmpty($users); + } - $this->assertTrue($this->app->enableUser(2)); - $this->assertTrue($this->app->isActiveUser(2)); + public function assertEnableDisableUser() + { + $this->assertTrue($this->app->disableUser($this->userId)); + $this->assertFalse($this->app->isActiveUser($this->userId)); + $this->assertTrue($this->app->enableUser($this->userId)); + $this->assertTrue($this->app->isActiveUser($this->userId)); + } + + public function assertUpdateUser() + { + $this->assertTrue($this->app->updateUser(array( + 'id' => $this->userId, + 'name' => 'My user', + ))); + + $user = $this->app->getUser($this->userId); + $this->assertNotNull($user); + $this->assertEquals('My user', $user['name']); + } + + public function assertRemoveUser() + { + $this->assertTrue($this->app->removeUser($this->userId)); } } -- cgit v1.2.3 From 4a230d331ec220fc32a48525afb308af0d9787fa Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 10:25:13 -0400 Subject: Added application and project roles validation for API procedure calls --- ChangeLog | 4 + app/Api/ActionApi.php | 87 ---- app/Api/AppApi.php | 49 -- app/Api/Authorization/ActionAuthorization.php | 19 + app/Api/Authorization/CategoryAuthorization.php | 19 + app/Api/Authorization/ColumnAuthorization.php | 19 + app/Api/Authorization/CommentAuthorization.php | 19 + app/Api/Authorization/ProcedureAuthorization.php | 32 ++ app/Api/Authorization/ProjectAuthorization.php | 35 ++ app/Api/Authorization/SubtaskAuthorization.php | 19 + app/Api/Authorization/TaskAuthorization.php | 19 + app/Api/Authorization/TaskFileAuthorization.php | 19 + app/Api/Authorization/TaskLinkAuthorization.php | 19 + app/Api/Authorization/UserAuthorization.php | 22 + app/Api/BaseApi.php | 85 ---- app/Api/BoardApi.php | 24 - app/Api/CategoryApi.php | 51 -- app/Api/ColumnApi.php | 42 -- app/Api/CommentApi.php | 54 --- app/Api/GroupApi.php | 51 -- app/Api/GroupMemberApi.php | 39 -- app/Api/LinkApi.php | 113 ----- app/Api/MeApi.php | 72 --- app/Api/Middleware/AuthenticationApiMiddleware.php | 137 ------ app/Api/Middleware/AuthenticationMiddleware.php | 82 ++++ app/Api/Procedure/ActionProcedure.php | 91 ++++ app/Api/Procedure/AppProcedure.php | 47 ++ app/Api/Procedure/BaseProcedure.php | 86 ++++ app/Api/Procedure/BoardProcedure.php | 25 + app/Api/Procedure/CategoryProcedure.php | 59 +++ app/Api/Procedure/ColumnProcedure.php | 51 ++ app/Api/Procedure/CommentProcedure.php | 62 +++ app/Api/Procedure/GroupMemberProcedure.php | 37 ++ app/Api/Procedure/GroupProcedure.php | 49 ++ app/Api/Procedure/LinkProcedure.php | 111 +++++ app/Api/Procedure/MeProcedure.php | 72 +++ app/Api/Procedure/ProjectPermissionProcedure.php | 69 +++ app/Api/Procedure/ProjectProcedure.php | 106 +++++ app/Api/Procedure/SubtaskProcedure.php | 74 +++ app/Api/Procedure/SubtaskTimeTrackingProcedure.php | 39 ++ app/Api/Procedure/SwimlaneProcedure.php | 91 ++++ app/Api/Procedure/TaskFileProcedure.php | 70 +++ app/Api/Procedure/TaskLinkProcedure.php | 85 ++++ app/Api/Procedure/TaskProcedure.php | 167 +++++++ app/Api/Procedure/UserProcedure.php | 131 +++++ app/Api/ProjectApi.php | 87 ---- app/Api/ProjectPermissionApi.php | 55 --- app/Api/SubtaskApi.php | 66 --- app/Api/SubtaskTimeTrackingApi.php | 34 -- app/Api/SwimlaneApi.php | 80 ---- app/Api/TaskApi.php | 163 ------- app/Api/TaskFileApi.php | 59 --- app/Api/TaskLinkApi.php | 79 --- app/Api/UserApi.php | 131 ----- app/Core/Base.php | 4 + app/Model/ActionModel.php | 12 + app/Model/CategoryModel.php | 12 + app/Model/ColumnModel.php | 12 + app/Model/CommentModel.php | 16 + app/Model/SubtaskModel.php | 16 + app/Model/TaskFileModel.php | 16 + app/Model/TaskLinkModel.php | 16 + app/ServiceProvider/ApiProvider.php | 83 ++-- app/ServiceProvider/AuthenticationProvider.php | 57 +++ composer.json | 2 +- composer.lock | 251 ++++++++-- doc/api-authentication.markdown | 55 +-- doc/api-json-rpc.markdown | 12 +- doc/api-project-permission-procedures.markdown | 33 ++ doc/api-project-procedures.markdown | 4 + tests/integration/ActionProcedureTest.php | 66 +++ tests/integration/ActionTest.php | 66 --- tests/integration/AppProcedureTest.php | 54 +++ tests/integration/AppTest.php | 54 --- tests/integration/BaseIntegrationTest.php | 122 ----- tests/integration/BaseProcedureTest.php | 122 +++++ tests/integration/BoardProcedureTest.php | 25 + tests/integration/BoardTest.php | 25 - tests/integration/CategoryProcedureTest.php | 76 +++ tests/integration/CategoryTest.php | 76 --- tests/integration/ColumnProcedureTest.php | 69 +++ tests/integration/ColumnTest.php | 69 --- tests/integration/CommentProcedureTest.php | 63 +++ tests/integration/CommentTest.php | 63 --- tests/integration/GroupMemberProcedureTest.php | 53 +++ tests/integration/GroupMemberTest.php | 53 --- tests/integration/GroupProcedureTest.php | 50 ++ tests/integration/GroupTest.php | 50 -- tests/integration/LinkProcedureTest.php | 70 +++ tests/integration/LinkTest.php | 70 --- tests/integration/MeProcedureTest.php | 68 +++ tests/integration/MeTest.php | 73 --- tests/integration/OverdueTaskProcedureTest.php | 43 ++ tests/integration/OverdueTaskTest.php | 43 -- tests/integration/ProcedureAuthorizationTest.php | 306 ++++++++++++ .../integration/ProjectPermissionProcedureTest.php | 89 ++++ tests/integration/ProjectPermissionTest.php | 89 ---- tests/integration/ProjectProcedureTest.php | 89 ++++ tests/integration/ProjectTest.php | 89 ---- tests/integration/SubtaskProcedureTest.php | 64 +++ tests/integration/SubtaskTest.php | 64 --- tests/integration/SwimlaneProcedureTest.php | 93 ++++ tests/integration/SwimlaneTest.php | 93 ---- tests/integration/TaskFileProcedureTest.php | 67 +++ tests/integration/TaskFileTest.php | 67 --- tests/integration/TaskLinkProcedureTest.php | 68 +++ tests/integration/TaskLinkTest.php | 68 --- tests/integration/TaskProcedureTest.php | 55 +++ tests/integration/TaskTest.php | 55 --- tests/integration/UserProcedureTest.php | 63 +++ tests/integration/UserTest.php | 63 --- tests/units/Model/ActionModelTest.php | 527 +++++++++++++++++++++ tests/units/Model/ActionTest.php | 509 -------------------- tests/units/Model/CategoryModelTest.php | 229 +++++++++ tests/units/Model/CategoryTest.php | 217 --------- tests/units/Model/CommentTest.php | 86 ++-- tests/units/Model/SubtaskModelTest.php | 400 ++++++++++++++++ tests/units/Model/SubtaskTest.php | 388 --------------- tests/units/Model/TaskFileModelTest.php | 458 ++++++++++++++++++ tests/units/Model/TaskFileTest.php | 445 ----------------- tests/units/Model/TaskLinkModelTest.php | 211 +++++++++ tests/units/Model/TaskLinkTest.php | 196 -------- 122 files changed, 5845 insertions(+), 4834 deletions(-) delete mode 100644 app/Api/ActionApi.php delete mode 100644 app/Api/AppApi.php create mode 100644 app/Api/Authorization/ActionAuthorization.php create mode 100644 app/Api/Authorization/CategoryAuthorization.php create mode 100644 app/Api/Authorization/ColumnAuthorization.php create mode 100644 app/Api/Authorization/CommentAuthorization.php create mode 100644 app/Api/Authorization/ProcedureAuthorization.php create mode 100644 app/Api/Authorization/ProjectAuthorization.php create mode 100644 app/Api/Authorization/SubtaskAuthorization.php create mode 100644 app/Api/Authorization/TaskAuthorization.php create mode 100644 app/Api/Authorization/TaskFileAuthorization.php create mode 100644 app/Api/Authorization/TaskLinkAuthorization.php create mode 100644 app/Api/Authorization/UserAuthorization.php delete mode 100644 app/Api/BaseApi.php delete mode 100644 app/Api/BoardApi.php delete mode 100644 app/Api/CategoryApi.php delete mode 100644 app/Api/ColumnApi.php delete mode 100644 app/Api/CommentApi.php delete mode 100644 app/Api/GroupApi.php delete mode 100644 app/Api/GroupMemberApi.php delete mode 100644 app/Api/LinkApi.php delete mode 100644 app/Api/MeApi.php delete mode 100644 app/Api/Middleware/AuthenticationApiMiddleware.php create mode 100644 app/Api/Middleware/AuthenticationMiddleware.php create mode 100644 app/Api/Procedure/ActionProcedure.php create mode 100644 app/Api/Procedure/AppProcedure.php create mode 100644 app/Api/Procedure/BaseProcedure.php create mode 100644 app/Api/Procedure/BoardProcedure.php create mode 100644 app/Api/Procedure/CategoryProcedure.php create mode 100644 app/Api/Procedure/ColumnProcedure.php create mode 100644 app/Api/Procedure/CommentProcedure.php create mode 100644 app/Api/Procedure/GroupMemberProcedure.php create mode 100644 app/Api/Procedure/GroupProcedure.php create mode 100644 app/Api/Procedure/LinkProcedure.php create mode 100644 app/Api/Procedure/MeProcedure.php create mode 100644 app/Api/Procedure/ProjectPermissionProcedure.php create mode 100644 app/Api/Procedure/ProjectProcedure.php create mode 100644 app/Api/Procedure/SubtaskProcedure.php create mode 100644 app/Api/Procedure/SubtaskTimeTrackingProcedure.php create mode 100644 app/Api/Procedure/SwimlaneProcedure.php create mode 100644 app/Api/Procedure/TaskFileProcedure.php create mode 100644 app/Api/Procedure/TaskLinkProcedure.php create mode 100644 app/Api/Procedure/TaskProcedure.php create mode 100644 app/Api/Procedure/UserProcedure.php delete mode 100644 app/Api/ProjectApi.php delete mode 100644 app/Api/ProjectPermissionApi.php delete mode 100644 app/Api/SubtaskApi.php delete mode 100644 app/Api/SubtaskTimeTrackingApi.php delete mode 100644 app/Api/SwimlaneApi.php delete mode 100644 app/Api/TaskApi.php delete mode 100644 app/Api/TaskFileApi.php delete mode 100644 app/Api/TaskLinkApi.php delete mode 100644 app/Api/UserApi.php create mode 100644 tests/integration/ActionProcedureTest.php delete mode 100644 tests/integration/ActionTest.php create mode 100644 tests/integration/AppProcedureTest.php delete mode 100644 tests/integration/AppTest.php delete mode 100644 tests/integration/BaseIntegrationTest.php create mode 100644 tests/integration/BaseProcedureTest.php create mode 100644 tests/integration/BoardProcedureTest.php delete mode 100644 tests/integration/BoardTest.php create mode 100644 tests/integration/CategoryProcedureTest.php delete mode 100644 tests/integration/CategoryTest.php create mode 100644 tests/integration/ColumnProcedureTest.php delete mode 100644 tests/integration/ColumnTest.php create mode 100644 tests/integration/CommentProcedureTest.php delete mode 100644 tests/integration/CommentTest.php create mode 100644 tests/integration/GroupMemberProcedureTest.php delete mode 100644 tests/integration/GroupMemberTest.php create mode 100644 tests/integration/GroupProcedureTest.php delete mode 100644 tests/integration/GroupTest.php create mode 100644 tests/integration/LinkProcedureTest.php delete mode 100644 tests/integration/LinkTest.php create mode 100644 tests/integration/MeProcedureTest.php delete mode 100644 tests/integration/MeTest.php create mode 100644 tests/integration/OverdueTaskProcedureTest.php delete mode 100644 tests/integration/OverdueTaskTest.php create mode 100644 tests/integration/ProcedureAuthorizationTest.php create mode 100644 tests/integration/ProjectPermissionProcedureTest.php delete mode 100644 tests/integration/ProjectPermissionTest.php create mode 100644 tests/integration/ProjectProcedureTest.php delete mode 100644 tests/integration/ProjectTest.php create mode 100644 tests/integration/SubtaskProcedureTest.php delete mode 100644 tests/integration/SubtaskTest.php create mode 100644 tests/integration/SwimlaneProcedureTest.php delete mode 100644 tests/integration/SwimlaneTest.php create mode 100644 tests/integration/TaskFileProcedureTest.php delete mode 100644 tests/integration/TaskFileTest.php create mode 100644 tests/integration/TaskLinkProcedureTest.php delete mode 100644 tests/integration/TaskLinkTest.php create mode 100644 tests/integration/TaskProcedureTest.php delete mode 100644 tests/integration/TaskTest.php create mode 100644 tests/integration/UserProcedureTest.php delete mode 100644 tests/integration/UserTest.php create mode 100644 tests/units/Model/ActionModelTest.php delete mode 100644 tests/units/Model/ActionTest.php create mode 100644 tests/units/Model/CategoryModelTest.php delete mode 100644 tests/units/Model/CategoryTest.php create mode 100644 tests/units/Model/SubtaskModelTest.php delete mode 100644 tests/units/Model/SubtaskTest.php create mode 100644 tests/units/Model/TaskFileModelTest.php delete mode 100644 tests/units/Model/TaskFileTest.php create mode 100644 tests/units/Model/TaskLinkModelTest.php delete mode 100644 tests/units/Model/TaskLinkTest.php (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index b86fea57..42af8ee3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,10 @@ Version 1.0.31 (unreleased) -------------- +New features: + +* Added application and project roles validation for API procedure calls + Improvements: * Rewrite integration tests to run with Docker containers diff --git a/app/Api/ActionApi.php b/app/Api/ActionApi.php deleted file mode 100644 index 116742d8..00000000 --- a/app/Api/ActionApi.php +++ /dev/null @@ -1,87 +0,0 @@ -actionManager->getAvailableActions(); - } - - public function getAvailableActionEvents() - { - return $this->eventManager->getAll(); - } - - public function getCompatibleActionEvents($action_name) - { - return $this->actionManager->getCompatibleEvents($action_name); - } - - public function removeAction($action_id) - { - return $this->actionModel->remove($action_id); - } - - public function getActions($project_id) - { - return $this->actionModel->getAllByProject($project_id); - } - - public function createAction($project_id, $event_name, $action_name, array $params) - { - $values = array( - 'project_id' => $project_id, - 'event_name' => $event_name, - 'action_name' => $action_name, - 'params' => $params, - ); - - list($valid, ) = $this->actionValidator->validateCreation($values); - - if (! $valid) { - return false; - } - - // Check if the action exists - $actions = $this->actionManager->getAvailableActions(); - - if (! isset($actions[$action_name])) { - return false; - } - - // Check the event - $action = $this->actionManager->getAction($action_name); - - if (! in_array($event_name, $action->getEvents())) { - return false; - } - - $required_params = $action->getActionRequiredParameters(); - - // Check missing parameters - foreach ($required_params as $param => $value) { - if (! isset($params[$param])) { - return false; - } - } - - // Check extra parameters - foreach ($params as $param => $value) { - if (! isset($required_params[$param])) { - return false; - } - } - - return $this->actionModel->create($values); - } -} diff --git a/app/Api/AppApi.php b/app/Api/AppApi.php deleted file mode 100644 index 637de5c5..00000000 --- a/app/Api/AppApi.php +++ /dev/null @@ -1,49 +0,0 @@ -timezoneModel->getCurrentTimezone(); - } - - public function getVersion() - { - return APP_VERSION; - } - - public function getDefaultTaskColor() - { - return $this->colorModel->getDefaultColor(); - } - - public function getDefaultTaskColors() - { - return $this->colorModel->getDefaultColors(); - } - - public function getColorList() - { - return $this->colorModel->getList(); - } - - public function getApplicationRoles() - { - return $this->role->getApplicationRoles(); - } - - public function getProjectRoles() - { - return $this->role->getProjectRoles(); - } -} diff --git a/app/Api/Authorization/ActionAuthorization.php b/app/Api/Authorization/ActionAuthorization.php new file mode 100644 index 00000000..4b41ad82 --- /dev/null +++ b/app/Api/Authorization/ActionAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->actionModel->getProjectId($action_id)); + } + } +} diff --git a/app/Api/Authorization/CategoryAuthorization.php b/app/Api/Authorization/CategoryAuthorization.php new file mode 100644 index 00000000..f17265a2 --- /dev/null +++ b/app/Api/Authorization/CategoryAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->categoryModel->getProjectId($category_id)); + } + } +} diff --git a/app/Api/Authorization/ColumnAuthorization.php b/app/Api/Authorization/ColumnAuthorization.php new file mode 100644 index 00000000..37aecda2 --- /dev/null +++ b/app/Api/Authorization/ColumnAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->columnModel->getProjectId($column_id)); + } + } +} diff --git a/app/Api/Authorization/CommentAuthorization.php b/app/Api/Authorization/CommentAuthorization.php new file mode 100644 index 00000000..ed15512e --- /dev/null +++ b/app/Api/Authorization/CommentAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->commentModel->getProjectId($comment_id)); + } + } +} diff --git a/app/Api/Authorization/ProcedureAuthorization.php b/app/Api/Authorization/ProcedureAuthorization.php new file mode 100644 index 00000000..070a6371 --- /dev/null +++ b/app/Api/Authorization/ProcedureAuthorization.php @@ -0,0 +1,32 @@ +userSession->isLogged() && in_array($procedure, $this->userSpecificProcedures)) { + throw new AccessDeniedException('This procedure is not available with the API credentials'); + } + } +} diff --git a/app/Api/Authorization/ProjectAuthorization.php b/app/Api/Authorization/ProjectAuthorization.php new file mode 100644 index 00000000..21ecf311 --- /dev/null +++ b/app/Api/Authorization/ProjectAuthorization.php @@ -0,0 +1,35 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $project_id); + } + } + + protected function checkProjectPermission($class, $method, $project_id) + { + if (empty($project_id)) { + throw new AccessDeniedException('Project not found'); + } + + $role = $this->projectUserRoleModel->getUserRole($project_id, $this->userSession->getId()); + + if (! $this->apiProjectAuthorization->isAllowed($class, $method, $role)) { + throw new AccessDeniedException('Project access denied'); + } + } +} diff --git a/app/Api/Authorization/SubtaskAuthorization.php b/app/Api/Authorization/SubtaskAuthorization.php new file mode 100644 index 00000000..fcb57929 --- /dev/null +++ b/app/Api/Authorization/SubtaskAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->subtaskModel->getProjectId($subtask_id)); + } + } +} diff --git a/app/Api/Authorization/TaskAuthorization.php b/app/Api/Authorization/TaskAuthorization.php new file mode 100644 index 00000000..db93b76b --- /dev/null +++ b/app/Api/Authorization/TaskAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskFinderModel->getProjectId($category_id)); + } + } +} diff --git a/app/Api/Authorization/TaskFileAuthorization.php b/app/Api/Authorization/TaskFileAuthorization.php new file mode 100644 index 00000000..e40783eb --- /dev/null +++ b/app/Api/Authorization/TaskFileAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskFileModel->getProjectId($file_id)); + } + } +} diff --git a/app/Api/Authorization/TaskLinkAuthorization.php b/app/Api/Authorization/TaskLinkAuthorization.php new file mode 100644 index 00000000..2f5fc8d5 --- /dev/null +++ b/app/Api/Authorization/TaskLinkAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskLinkModel->getProjectId($task_link_id)); + } + } +} diff --git a/app/Api/Authorization/UserAuthorization.php b/app/Api/Authorization/UserAuthorization.php new file mode 100644 index 00000000..3fd6865c --- /dev/null +++ b/app/Api/Authorization/UserAuthorization.php @@ -0,0 +1,22 @@ +userSession->isLogged() && ! $this->apiAuthorization->isAllowed($class, $method, $this->userSession->getRole())) { + throw new AccessDeniedException('You are not allowed to access to this resource'); + } + } +} diff --git a/app/Api/BaseApi.php b/app/Api/BaseApi.php deleted file mode 100644 index 8f18802c..00000000 --- a/app/Api/BaseApi.php +++ /dev/null @@ -1,85 +0,0 @@ -userSession->isLogged() && ! $this->projectPermissionModel->isUserAllowed($project_id, $this->userSession->getId())) { - throw new AccessDeniedException('Permission denied'); - } - } - - public function checkTaskPermission($task_id) - { - if ($this->userSession->isLogged()) { - $this->checkProjectPermission($this->taskFinderModel->getProjectId($task_id)); - } - } - - protected function formatTask($task) - { - if (! empty($task)) { - $task['url'] = $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); - $task['color'] = $this->colorModel->getColorProperties($task['color_id']); - } - - return $task; - } - - protected function formatTasks($tasks) - { - if (! empty($tasks)) { - foreach ($tasks as &$task) { - $task = $this->formatTask($task); - } - } - - return $tasks; - } - - protected function formatProject($project) - { - if (! empty($project)) { - $project['url'] = array( - 'board' => $this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id']), '', true), - 'calendar' => $this->helper->url->to('CalendarController', 'show', array('project_id' => $project['id']), '', true), - 'list' => $this->helper->url->to('TaskListController', 'show', array('project_id' => $project['id']), '', true), - ); - } - - return $project; - } - - protected function formatProjects($projects) - { - if (! empty($projects)) { - foreach ($projects as &$project) { - $project = $this->formatProject($project); - } - } - - return $projects; - } - - protected function filterValues(array $values) - { - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } - - return $values; - } -} diff --git a/app/Api/BoardApi.php b/app/Api/BoardApi.php deleted file mode 100644 index 70f21c0e..00000000 --- a/app/Api/BoardApi.php +++ /dev/null @@ -1,24 +0,0 @@ -checkProjectPermission($project_id); - - return BoardFormatter::getInstance($this->container) - ->withProjectId($project_id) - ->withQuery($this->taskFinderModel->getExtendedQuery()) - ->format(); - } -} diff --git a/app/Api/CategoryApi.php b/app/Api/CategoryApi.php deleted file mode 100644 index c56cfb35..00000000 --- a/app/Api/CategoryApi.php +++ /dev/null @@ -1,51 +0,0 @@ -categoryModel->getById($category_id); - } - - public function getAllCategories($project_id) - { - return $this->categoryModel->getAll($project_id); - } - - public function removeCategory($category_id) - { - return $this->categoryModel->remove($category_id); - } - - public function createCategory($project_id, $name) - { - $values = array( - 'project_id' => $project_id, - 'name' => $name, - ); - - list($valid, ) = $this->categoryValidator->validateCreation($values); - return $valid ? $this->categoryModel->create($values) : false; - } - - public function updateCategory($id, $name) - { - $values = array( - 'id' => $id, - 'name' => $name, - ); - - list($valid, ) = $this->categoryValidator->validateModification($values); - return $valid && $this->categoryModel->update($values); - } -} diff --git a/app/Api/ColumnApi.php b/app/Api/ColumnApi.php deleted file mode 100644 index aa4026f6..00000000 --- a/app/Api/ColumnApi.php +++ /dev/null @@ -1,42 +0,0 @@ -columnModel->getAll($project_id); - } - - public function getColumn($column_id) - { - return $this->columnModel->getById($column_id); - } - - public function updateColumn($column_id, $title, $task_limit = 0, $description = '') - { - return $this->columnModel->update($column_id, $title, $task_limit, $description); - } - - public function addColumn($project_id, $title, $task_limit = 0, $description = '') - { - return $this->columnModel->create($project_id, $title, $task_limit, $description); - } - - public function removeColumn($column_id) - { - return $this->columnModel->remove($column_id); - } - - public function changeColumnPosition($project_id, $column_id, $position) - { - return $this->columnModel->changePosition($project_id, $column_id, $position); - } -} diff --git a/app/Api/CommentApi.php b/app/Api/CommentApi.php deleted file mode 100644 index 8358efee..00000000 --- a/app/Api/CommentApi.php +++ /dev/null @@ -1,54 +0,0 @@ -commentModel->getById($comment_id); - } - - public function getAllComments($task_id) - { - return $this->commentModel->getAll($task_id); - } - - public function removeComment($comment_id) - { - return $this->commentModel->remove($comment_id); - } - - public function createComment($task_id, $user_id, $content, $reference = '') - { - $values = array( - 'task_id' => $task_id, - 'user_id' => $user_id, - 'comment' => $content, - 'reference' => $reference, - ); - - list($valid, ) = $this->commentValidator->validateCreation($values); - - return $valid ? $this->commentModel->create($values) : false; - } - - public function updateComment($id, $content) - { - $values = array( - 'id' => $id, - 'comment' => $content, - ); - - list($valid, ) = $this->commentValidator->validateModification($values); - return $valid && $this->commentModel->update($values); - } -} diff --git a/app/Api/GroupApi.php b/app/Api/GroupApi.php deleted file mode 100644 index 1701edc3..00000000 --- a/app/Api/GroupApi.php +++ /dev/null @@ -1,51 +0,0 @@ -groupModel->create($name, $external_id); - } - - public function updateGroup($group_id, $name = null, $external_id = null) - { - $values = array( - 'id' => $group_id, - 'name' => $name, - 'external_id' => $external_id, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } - - return $this->groupModel->update($values); - } - - public function removeGroup($group_id) - { - return $this->groupModel->remove($group_id); - } - - public function getGroup($group_id) - { - return $this->groupModel->getById($group_id); - } - - public function getAllGroups() - { - return $this->groupModel->getAll(); - } -} diff --git a/app/Api/GroupMemberApi.php b/app/Api/GroupMemberApi.php deleted file mode 100644 index e09f6975..00000000 --- a/app/Api/GroupMemberApi.php +++ /dev/null @@ -1,39 +0,0 @@ -groupMemberModel->getGroups($user_id); - } - - public function getGroupMembers($group_id) - { - return $this->groupMemberModel->getMembers($group_id); - } - - public function addGroupMember($group_id, $user_id) - { - return $this->groupMemberModel->addUser($group_id, $user_id); - } - - public function removeGroupMember($group_id, $user_id) - { - return $this->groupMemberModel->removeUser($group_id, $user_id); - } - - public function isGroupMember($group_id, $user_id) - { - return $this->groupMemberModel->isMember($group_id, $user_id); - } -} diff --git a/app/Api/LinkApi.php b/app/Api/LinkApi.php deleted file mode 100644 index d8e525e4..00000000 --- a/app/Api/LinkApi.php +++ /dev/null @@ -1,113 +0,0 @@ -linkModel->getById($link_id); - } - - /** - * Get a link by name - * - * @access public - * @param string $label - * @return array - */ - public function getLinkByLabel($label) - { - return $this->linkModel->getByLabel($label); - } - - /** - * Get the opposite link id - * - * @access public - * @param integer $link_id Link id - * @return integer - */ - public function getOppositeLinkId($link_id) - { - return $this->linkModel->getOppositeLinkId($link_id); - } - - /** - * Get all links - * - * @access public - * @return array - */ - public function getAllLinks() - { - return $this->linkModel->getAll(); - } - - /** - * Create a new link label - * - * @access public - * @param string $label - * @param string $opposite_label - * @return boolean|integer - */ - public function createLink($label, $opposite_label = '') - { - $values = array( - 'label' => $label, - 'opposite_label' => $opposite_label, - ); - - list($valid, ) = $this->linkValidator->validateCreation($values); - return $valid ? $this->linkModel->create($label, $opposite_label) : false; - } - - /** - * Update a link - * - * @access public - * @param integer $link_id - * @param integer $opposite_link_id - * @param string $label - * @return boolean - */ - public function updateLink($link_id, $opposite_link_id, $label) - { - $values = array( - 'id' => $link_id, - 'opposite_id' => $opposite_link_id, - 'label' => $label, - ); - - list($valid, ) = $this->linkValidator->validateModification($values); - return $valid && $this->linkModel->update($values); - } - - /** - * Remove a link a the relation to its opposite - * - * @access public - * @param integer $link_id - * @return boolean - */ - public function removeLink($link_id) - { - return $this->linkModel->remove($link_id); - } -} diff --git a/app/Api/MeApi.php b/app/Api/MeApi.php deleted file mode 100644 index 497749b6..00000000 --- a/app/Api/MeApi.php +++ /dev/null @@ -1,72 +0,0 @@ -sessionStorage->user; - } - - public function getMyDashboard() - { - $user_id = $this->userSession->getId(); - $projects = $this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))->findAll(); - $tasks = $this->taskFinderModel->getUserQuery($user_id)->findAll(); - - return array( - 'projects' => $this->formatProjects($projects), - 'tasks' => $this->formatTasks($tasks), - 'subtasks' => $this->subtaskModel->getUserQuery($user_id, array(SubtaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), - ); - } - - public function getMyActivityStream() - { - $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); - return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); - } - - public function createMyPrivateProject($name, $description = null) - { - if ($this->configModel->get('disable_private_project', 0) == 1) { - return false; - } - - $values = array( - 'name' => $name, - 'description' => $description, - 'is_private' => 1, - ); - - list($valid, ) = $this->projectValidator->validateCreation($values); - return $valid ? $this->projectModel->create($values, $this->userSession->getId(), true) : false; - } - - public function getMyProjectsList() - { - return $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId()); - } - - public function getMyOverdueTasks() - { - return $this->taskFinderModel->getOverdueTasksByUser($this->userSession->getId()); - } - - public function getMyProjects() - { - $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); - $projects = $this->projectModel->getAllByIds($project_ids); - - return $this->formatProjects($projects); - } -} diff --git a/app/Api/Middleware/AuthenticationApiMiddleware.php b/app/Api/Middleware/AuthenticationApiMiddleware.php deleted file mode 100644 index b16e10b8..00000000 --- a/app/Api/Middleware/AuthenticationApiMiddleware.php +++ /dev/null @@ -1,137 +0,0 @@ -dispatcher->dispatch('app.bootstrap'); - - if ($this->isUserAuthenticated($username, $password)) { - $this->checkProcedurePermission(true, $procedureName); - $this->userSession->initialize($this->userModel->getByUsername($username)); - } elseif ($this->isAppAuthenticated($username, $password)) { - $this->checkProcedurePermission(false, $procedureName); - } else { - $this->logger->error('API authentication failure for '.$username); - throw new AuthenticationFailureException('Wrong credentials'); - } - } - - /** - * Check user credentials - * - * @access public - * @param string $username - * @param string $password - * @return boolean - */ - private function isUserAuthenticated($username, $password) - { - return $username !== 'jsonrpc' && - ! $this->userLockingModel->isLocked($username) && - $this->authenticationManager->passwordAuthentication($username, $password); - } - - /** - * Check administrative credentials - * - * @access public - * @param string $username - * @param string $password - * @return boolean - */ - private function isAppAuthenticated($username, $password) - { - return $username === 'jsonrpc' && $password === $this->getApiToken(); - } - - /** - * Get API Token - * - * @access private - * @return string - */ - private function getApiToken() - { - if (defined('API_AUTHENTICATION_TOKEN')) { - return API_AUTHENTICATION_TOKEN; - } - - return $this->configModel->get('api_token'); - } - - public function checkProcedurePermission($is_user, $procedure) - { - $is_both_procedure = in_array($procedure, $this->both_allowed_procedures); - $is_user_procedure = in_array($procedure, $this->user_allowed_procedures); - - if ($is_user && ! $is_both_procedure && ! $is_user_procedure) { - throw new AccessDeniedException('Permission denied'); - } elseif (! $is_user && ! $is_both_procedure && $is_user_procedure) { - throw new AccessDeniedException('Permission denied'); - } - - $this->logger->debug('API call: '.$procedure); - } -} diff --git a/app/Api/Middleware/AuthenticationMiddleware.php b/app/Api/Middleware/AuthenticationMiddleware.php new file mode 100644 index 00000000..8e309593 --- /dev/null +++ b/app/Api/Middleware/AuthenticationMiddleware.php @@ -0,0 +1,82 @@ +dispatcher->dispatch('app.bootstrap'); + + if ($this->isUserAuthenticated($username, $password)) { + $this->userSession->initialize($this->userModel->getByUsername($username)); + } elseif (! $this->isAppAuthenticated($username, $password)) { + $this->logger->error('API authentication failure for '.$username); + throw new AuthenticationFailureException('Wrong credentials'); + } + } + + /** + * Check user credentials + * + * @access public + * @param string $username + * @param string $password + * @return boolean + */ + private function isUserAuthenticated($username, $password) + { + return $username !== 'jsonrpc' && + ! $this->userLockingModel->isLocked($username) && + $this->authenticationManager->passwordAuthentication($username, $password); + } + + /** + * Check administrative credentials + * + * @access public + * @param string $username + * @param string $password + * @return boolean + */ + private function isAppAuthenticated($username, $password) + { + return $username === 'jsonrpc' && $password === $this->getApiToken(); + } + + /** + * Get API Token + * + * @access private + * @return string + */ + private function getApiToken() + { + if (defined('API_AUTHENTICATION_TOKEN')) { + return API_AUTHENTICATION_TOKEN; + } + + return $this->configModel->get('api_token'); + } +} diff --git a/app/Api/Procedure/ActionProcedure.php b/app/Api/Procedure/ActionProcedure.php new file mode 100644 index 00000000..4043dbb9 --- /dev/null +++ b/app/Api/Procedure/ActionProcedure.php @@ -0,0 +1,91 @@ +actionManager->getAvailableActions(); + } + + public function getAvailableActionEvents() + { + return $this->eventManager->getAll(); + } + + public function getCompatibleActionEvents($action_name) + { + return $this->actionManager->getCompatibleEvents($action_name); + } + + public function removeAction($action_id) + { + ActionAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAction', $action_id); + return $this->actionModel->remove($action_id); + } + + public function getActions($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getActions', $project_id); + return $this->actionModel->getAllByProject($project_id); + } + + public function createAction($project_id, $event_name, $action_name, array $params) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createAction', $project_id); + $values = array( + 'project_id' => $project_id, + 'event_name' => $event_name, + 'action_name' => $action_name, + 'params' => $params, + ); + + list($valid, ) = $this->actionValidator->validateCreation($values); + + if (! $valid) { + return false; + } + + // Check if the action exists + $actions = $this->actionManager->getAvailableActions(); + + if (! isset($actions[$action_name])) { + return false; + } + + // Check the event + $action = $this->actionManager->getAction($action_name); + + if (! in_array($event_name, $action->getEvents())) { + return false; + } + + $required_params = $action->getActionRequiredParameters(); + + // Check missing parameters + foreach ($required_params as $param => $value) { + if (! isset($params[$param])) { + return false; + } + } + + // Check extra parameters + foreach ($params as $param => $value) { + if (! isset($required_params[$param])) { + return false; + } + } + + return $this->actionModel->create($values); + } +} diff --git a/app/Api/Procedure/AppProcedure.php b/app/Api/Procedure/AppProcedure.php new file mode 100644 index 00000000..60af4a60 --- /dev/null +++ b/app/Api/Procedure/AppProcedure.php @@ -0,0 +1,47 @@ +timezoneModel->getCurrentTimezone(); + } + + public function getVersion() + { + return APP_VERSION; + } + + public function getDefaultTaskColor() + { + return $this->colorModel->getDefaultColor(); + } + + public function getDefaultTaskColors() + { + return $this->colorModel->getDefaultColors(); + } + + public function getColorList() + { + return $this->colorModel->getList(); + } + + public function getApplicationRoles() + { + return $this->role->getApplicationRoles(); + } + + public function getProjectRoles() + { + return $this->role->getProjectRoles(); + } +} diff --git a/app/Api/Procedure/BaseProcedure.php b/app/Api/Procedure/BaseProcedure.php new file mode 100644 index 00000000..0aa43428 --- /dev/null +++ b/app/Api/Procedure/BaseProcedure.php @@ -0,0 +1,86 @@ +container)->check($procedure); + UserAuthorization::getInstance($this->container)->check($this->getClassName(), $procedure); + } + + protected function formatTask($task) + { + if (! empty($task)) { + $task['url'] = $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); + $task['color'] = $this->colorModel->getColorProperties($task['color_id']); + } + + return $task; + } + + protected function formatTasks($tasks) + { + if (! empty($tasks)) { + foreach ($tasks as &$task) { + $task = $this->formatTask($task); + } + } + + return $tasks; + } + + protected function formatProject($project) + { + if (! empty($project)) { + $project['url'] = array( + 'board' => $this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id']), '', true), + 'calendar' => $this->helper->url->to('CalendarController', 'show', array('project_id' => $project['id']), '', true), + 'list' => $this->helper->url->to('TaskListController', 'show', array('project_id' => $project['id']), '', true), + ); + } + + return $project; + } + + protected function formatProjects($projects) + { + if (! empty($projects)) { + foreach ($projects as &$project) { + $project = $this->formatProject($project); + } + } + + return $projects; + } + + protected function filterValues(array $values) + { + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $values; + } + + protected function getClassName() + { + $reflection = new ReflectionClass(get_called_class()); + return $reflection->getShortName(); + } +} diff --git a/app/Api/Procedure/BoardProcedure.php b/app/Api/Procedure/BoardProcedure.php new file mode 100644 index 00000000..674b5466 --- /dev/null +++ b/app/Api/Procedure/BoardProcedure.php @@ -0,0 +1,25 @@ +container)->check($this->getClassName(), 'getBoard', $project_id); + + return BoardFormatter::getInstance($this->container) + ->withProjectId($project_id) + ->withQuery($this->taskFinderModel->getExtendedQuery()) + ->format(); + } +} diff --git a/app/Api/Procedure/CategoryProcedure.php b/app/Api/Procedure/CategoryProcedure.php new file mode 100644 index 00000000..3ebbd908 --- /dev/null +++ b/app/Api/Procedure/CategoryProcedure.php @@ -0,0 +1,59 @@ +container)->check($this->getClassName(), 'getCategory', $category_id); + return $this->categoryModel->getById($category_id); + } + + public function getAllCategories($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllCategories', $project_id); + return $this->categoryModel->getAll($project_id); + } + + public function removeCategory($category_id) + { + CategoryAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeCategory', $category_id); + return $this->categoryModel->remove($category_id); + } + + public function createCategory($project_id, $name) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createCategory', $project_id); + + $values = array( + 'project_id' => $project_id, + 'name' => $name, + ); + + list($valid, ) = $this->categoryValidator->validateCreation($values); + return $valid ? $this->categoryModel->create($values) : false; + } + + public function updateCategory($id, $name) + { + CategoryAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateCategory', $id); + + $values = array( + 'id' => $id, + 'name' => $name, + ); + + list($valid, ) = $this->categoryValidator->validateModification($values); + return $valid && $this->categoryModel->update($values); + } +} diff --git a/app/Api/Procedure/ColumnProcedure.php b/app/Api/Procedure/ColumnProcedure.php new file mode 100644 index 00000000..ab9d173b --- /dev/null +++ b/app/Api/Procedure/ColumnProcedure.php @@ -0,0 +1,51 @@ +container)->check($this->getClassName(), 'getColumns', $project_id); + return $this->columnModel->getAll($project_id); + } + + public function getColumn($column_id) + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'getColumn', $column_id); + return $this->columnModel->getById($column_id); + } + + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateColumn', $column_id); + return $this->columnModel->update($column_id, $title, $task_limit, $description); + } + + public function addColumn($project_id, $title, $task_limit = 0, $description = '') + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addColumn', $project_id); + return $this->columnModel->create($project_id, $title, $task_limit, $description); + } + + public function removeColumn($column_id) + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeColumn', $column_id); + return $this->columnModel->remove($column_id); + } + + public function changeColumnPosition($project_id, $column_id, $position) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeColumnPosition', $project_id); + return $this->columnModel->changePosition($project_id, $column_id, $position); + } +} diff --git a/app/Api/Procedure/CommentProcedure.php b/app/Api/Procedure/CommentProcedure.php new file mode 100644 index 00000000..019a49bb --- /dev/null +++ b/app/Api/Procedure/CommentProcedure.php @@ -0,0 +1,62 @@ +container)->check($this->getClassName(), 'getComment', $comment_id); + return $this->commentModel->getById($comment_id); + } + + public function getAllComments($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllComments', $task_id); + return $this->commentModel->getAll($task_id); + } + + public function removeComment($comment_id) + { + CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeComment', $comment_id); + return $this->commentModel->remove($comment_id); + } + + public function createComment($task_id, $user_id, $content, $reference = '') + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createComment', $task_id); + + $values = array( + 'task_id' => $task_id, + 'user_id' => $user_id, + 'comment' => $content, + 'reference' => $reference, + ); + + list($valid, ) = $this->commentValidator->validateCreation($values); + + return $valid ? $this->commentModel->create($values) : false; + } + + public function updateComment($id, $content) + { + CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateComment', $id); + + $values = array( + 'id' => $id, + 'comment' => $content, + ); + + list($valid, ) = $this->commentValidator->validateModification($values); + return $valid && $this->commentModel->update($values); + } +} diff --git a/app/Api/Procedure/GroupMemberProcedure.php b/app/Api/Procedure/GroupMemberProcedure.php new file mode 100644 index 00000000..081d6ac8 --- /dev/null +++ b/app/Api/Procedure/GroupMemberProcedure.php @@ -0,0 +1,37 @@ +groupMemberModel->getGroups($user_id); + } + + public function getGroupMembers($group_id) + { + return $this->groupMemberModel->getMembers($group_id); + } + + public function addGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->addUser($group_id, $user_id); + } + + public function removeGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->removeUser($group_id, $user_id); + } + + public function isGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->isMember($group_id, $user_id); + } +} diff --git a/app/Api/Procedure/GroupProcedure.php b/app/Api/Procedure/GroupProcedure.php new file mode 100644 index 00000000..804940a2 --- /dev/null +++ b/app/Api/Procedure/GroupProcedure.php @@ -0,0 +1,49 @@ +groupModel->create($name, $external_id); + } + + public function updateGroup($group_id, $name = null, $external_id = null) + { + $values = array( + 'id' => $group_id, + 'name' => $name, + 'external_id' => $external_id, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $this->groupModel->update($values); + } + + public function removeGroup($group_id) + { + return $this->groupModel->remove($group_id); + } + + public function getGroup($group_id) + { + return $this->groupModel->getById($group_id); + } + + public function getAllGroups() + { + return $this->groupModel->getAll(); + } +} diff --git a/app/Api/Procedure/LinkProcedure.php b/app/Api/Procedure/LinkProcedure.php new file mode 100644 index 00000000..b4cecf3a --- /dev/null +++ b/app/Api/Procedure/LinkProcedure.php @@ -0,0 +1,111 @@ +linkModel->getById($link_id); + } + + /** + * Get a link by name + * + * @access public + * @param string $label + * @return array + */ + public function getLinkByLabel($label) + { + return $this->linkModel->getByLabel($label); + } + + /** + * Get the opposite link id + * + * @access public + * @param integer $link_id Link id + * @return integer + */ + public function getOppositeLinkId($link_id) + { + return $this->linkModel->getOppositeLinkId($link_id); + } + + /** + * Get all links + * + * @access public + * @return array + */ + public function getAllLinks() + { + return $this->linkModel->getAll(); + } + + /** + * Create a new link label + * + * @access public + * @param string $label + * @param string $opposite_label + * @return boolean|integer + */ + public function createLink($label, $opposite_label = '') + { + $values = array( + 'label' => $label, + 'opposite_label' => $opposite_label, + ); + + list($valid, ) = $this->linkValidator->validateCreation($values); + return $valid ? $this->linkModel->create($label, $opposite_label) : false; + } + + /** + * Update a link + * + * @access public + * @param integer $link_id + * @param integer $opposite_link_id + * @param string $label + * @return boolean + */ + public function updateLink($link_id, $opposite_link_id, $label) + { + $values = array( + 'id' => $link_id, + 'opposite_id' => $opposite_link_id, + 'label' => $label, + ); + + list($valid, ) = $this->linkValidator->validateModification($values); + return $valid && $this->linkModel->update($values); + } + + /** + * Remove a link a the relation to its opposite + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function removeLink($link_id) + { + return $this->linkModel->remove($link_id); + } +} diff --git a/app/Api/Procedure/MeProcedure.php b/app/Api/Procedure/MeProcedure.php new file mode 100644 index 00000000..e59e6522 --- /dev/null +++ b/app/Api/Procedure/MeProcedure.php @@ -0,0 +1,72 @@ +sessionStorage->user; + } + + public function getMyDashboard() + { + $user_id = $this->userSession->getId(); + $projects = $this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))->findAll(); + $tasks = $this->taskFinderModel->getUserQuery($user_id)->findAll(); + + return array( + 'projects' => $this->formatProjects($projects), + 'tasks' => $this->formatTasks($tasks), + 'subtasks' => $this->subtaskModel->getUserQuery($user_id, array(SubtaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), + ); + } + + public function getMyActivityStream() + { + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); + } + + public function createMyPrivateProject($name, $description = null) + { + if ($this->configModel->get('disable_private_project', 0) == 1) { + return false; + } + + $values = array( + 'name' => $name, + 'description' => $description, + 'is_private' => 1, + ); + + list($valid, ) = $this->projectValidator->validateCreation($values); + return $valid ? $this->projectModel->create($values, $this->userSession->getId(), true) : false; + } + + public function getMyProjectsList() + { + return $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId()); + } + + public function getMyOverdueTasks() + { + return $this->taskFinderModel->getOverdueTasksByUser($this->userSession->getId()); + } + + public function getMyProjects() + { + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + $projects = $this->projectModel->getAllByIds($project_ids); + + return $this->formatProjects($projects); + } +} diff --git a/app/Api/Procedure/ProjectPermissionProcedure.php b/app/Api/Procedure/ProjectPermissionProcedure.php new file mode 100644 index 00000000..e22e1d62 --- /dev/null +++ b/app/Api/Procedure/ProjectPermissionProcedure.php @@ -0,0 +1,69 @@ +container)->check($this->getClassName(), 'getProjectUsers', $project_id); + return $this->projectUserRoleModel->getAllUsers($project_id); + } + + public function getAssignableUsers($project_id, $prepend_unassigned = false) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAssignableUsers', $project_id); + return $this->projectUserRoleModel->getAssignableUsersList($project_id, $prepend_unassigned); + } + + public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addProjectUser', $project_id); + return $this->projectUserRoleModel->addUser($project_id, $user_id, $role); + } + + public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addProjectGroup', $project_id); + return $this->projectGroupRoleModel->addGroup($project_id, $group_id, $role); + } + + public function removeProjectUser($project_id, $user_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectUser', $project_id); + return $this->projectUserRoleModel->removeUser($project_id, $user_id); + } + + public function removeProjectGroup($project_id, $group_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectGroup', $project_id); + return $this->projectGroupRoleModel->removeGroup($project_id, $group_id); + } + + public function changeProjectUserRole($project_id, $user_id, $role) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeProjectUserRole', $project_id); + return $this->projectUserRoleModel->changeUserRole($project_id, $user_id, $role); + } + + public function changeProjectGroupRole($project_id, $group_id, $role) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeProjectGroupRole', $project_id); + return $this->projectGroupRoleModel->changeGroupRole($project_id, $group_id, $role); + } + + public function getProjectUserRole($project_id, $user_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectUserRole', $project_id); + return $this->projectUserRoleModel->getUserRole($project_id, $user_id); + } +} diff --git a/app/Api/Procedure/ProjectProcedure.php b/app/Api/Procedure/ProjectProcedure.php new file mode 100644 index 00000000..9187f221 --- /dev/null +++ b/app/Api/Procedure/ProjectProcedure.php @@ -0,0 +1,106 @@ +container)->check($this->getClassName(), 'getProjectById', $project_id); + return $this->formatProject($this->projectModel->getById($project_id)); + } + + public function getProjectByName($name) + { + $project = $this->projectModel->getByName($name); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByName', $project['id']); + return $this->formatProject($project); + } + + public function getAllProjects() + { + return $this->formatProjects($this->projectModel->getAll()); + } + + public function removeProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProject', $project_id); + return $this->projectModel->remove($project_id); + } + + public function enableProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProject', $project_id); + return $this->projectModel->enable($project_id); + } + + public function disableProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProject', $project_id); + return $this->projectModel->disable($project_id); + } + + public function enableProjectPublicAccess($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProjectPublicAccess', $project_id); + return $this->projectModel->enablePublicAccess($project_id); + } + + public function disableProjectPublicAccess($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProjectPublicAccess', $project_id); + return $this->projectModel->disablePublicAccess($project_id); + } + + public function getProjectActivities(array $project_ids) + { + foreach ($project_ids as $project_id) { + ProjectAuthorization::getInstance($this->container) + ->check($this->getClassName(), 'getProjectActivities', $project_id); + } + + return $this->helper->projectActivity->getProjectsEvents($project_ids); + } + + public function getProjectActivity($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectActivity', $project_id); + return $this->helper->projectActivity->getProjectEvents($project_id); + } + + public function createProject($name, $description = null, $owner_id = 0, $identifier = null) + { + $values = array( + 'name' => $name, + 'description' => $description, + 'identifier' => $identifier, + ); + + list($valid, ) = $this->projectValidator->validateCreation($values); + return $valid ? $this->projectModel->create($values, $owner_id, $this->userSession->isLogged()) : false; + } + + public function updateProject($project_id, $name, $description = null, $owner_id = null, $identifier = null) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateProject', $project_id); + + $values = $this->filterValues(array( + 'id' => $project_id, + 'name' => $name, + 'description' => $description, + 'owner_id' => $owner_id, + 'identifier' => $identifier, + )); + + list($valid, ) = $this->projectValidator->validateModification($values); + return $valid && $this->projectModel->update($values); + } +} diff --git a/app/Api/Procedure/SubtaskProcedure.php b/app/Api/Procedure/SubtaskProcedure.php new file mode 100644 index 00000000..e2400912 --- /dev/null +++ b/app/Api/Procedure/SubtaskProcedure.php @@ -0,0 +1,74 @@ +container)->check($this->getClassName(), 'getSubtask', $subtask_id); + return $this->subtaskModel->getById($subtask_id); + } + + public function getAllSubtasks($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllSubtasks', $task_id); + return $this->subtaskModel->getAll($task_id); + } + + public function removeSubtask($subtask_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeSubtask', $subtask_id); + return $this->subtaskModel->remove($subtask_id); + } + + public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createSubtask', $task_id); + + $values = array( + 'title' => $title, + 'task_id' => $task_id, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + list($valid, ) = $this->subtaskValidator->validateCreation($values); + return $valid ? $this->subtaskModel->create($values) : false; + } + + public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateSubtask', $task_id); + + $values = array( + 'id' => $id, + 'task_id' => $task_id, + 'title' => $title, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid, ) = $this->subtaskValidator->validateApiModification($values); + return $valid && $this->subtaskModel->update($values); + } +} diff --git a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php new file mode 100644 index 00000000..5d1988d6 --- /dev/null +++ b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php @@ -0,0 +1,39 @@ +container)->check($this->getClassName(), 'hasSubtaskTimer', $subtask_id); + return $this->subtaskTimeTrackingModel->hasTimer($subtask_id, $user_id); + } + + public function logSubtaskStartTime($subtask_id, $user_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskStartTime', $subtask_id); + return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); + } + + public function logSubtaskEndTime($subtask_id,$user_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskEndTime', $subtask_id); + return $this->subtaskTimeTrackingModel->logEndTime($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/SwimlaneProcedure.php b/app/Api/Procedure/SwimlaneProcedure.php new file mode 100644 index 00000000..9b7d181d --- /dev/null +++ b/app/Api/Procedure/SwimlaneProcedure.php @@ -0,0 +1,91 @@ +container)->check($this->getClassName(), 'getActiveSwimlanes', $project_id); + return $this->swimlaneModel->getSwimlanes($project_id); + } + + public function getAllSwimlanes($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllSwimlanes', $project_id); + return $this->swimlaneModel->getAll($project_id); + } + + public function getSwimlaneById($swimlane_id) + { + $swimlane = $this->swimlaneModel->getById($swimlane_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSwimlaneById', $swimlane['project_id']); + return $swimlane; + } + + public function getSwimlaneByName($project_id, $name) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSwimlaneByName', $project_id); + return $this->swimlaneModel->getByName($project_id, $name); + } + + public function getSwimlane($swimlane_id) + { + return $this->swimlaneModel->getById($swimlane_id); + } + + public function getDefaultSwimlane($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getDefaultSwimlane', $project_id); + return $this->swimlaneModel->getDefault($project_id); + } + + public function addSwimlane($project_id, $name, $description = '') + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addSwimlane', $project_id); + return $this->swimlaneModel->create(array('project_id' => $project_id, 'name' => $name, 'description' => $description)); + } + + public function updateSwimlane($swimlane_id, $name, $description = null) + { + $values = array('id' => $swimlane_id, 'name' => $name); + + if (!is_null($description)) { + $values['description'] = $description; + } + + return $this->swimlaneModel->update($values); + } + + public function removeSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeSwimlane', $project_id); + return $this->swimlaneModel->remove($project_id, $swimlane_id); + } + + public function disableSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableSwimlane', $project_id); + return $this->swimlaneModel->disable($project_id, $swimlane_id); + } + + public function enableSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableSwimlane', $project_id); + return $this->swimlaneModel->enable($project_id, $swimlane_id); + } + + public function changeSwimlanePosition($project_id, $swimlane_id, $position) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeSwimlanePosition', $project_id); + return $this->swimlaneModel->changePosition($project_id, $swimlane_id, $position); + } +} diff --git a/app/Api/Procedure/TaskFileProcedure.php b/app/Api/Procedure/TaskFileProcedure.php new file mode 100644 index 00000000..5aa7ea0b --- /dev/null +++ b/app/Api/Procedure/TaskFileProcedure.php @@ -0,0 +1,70 @@ +container)->check($this->getClassName(), 'getTaskFile', $file_id); + return $this->taskFileModel->getById($file_id); + } + + public function getAllTaskFiles($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTaskFiles', $task_id); + return $this->taskFileModel->getAll($task_id); + } + + public function downloadTaskFile($file_id) + { + TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadTaskFile', $file_id); + + try { + $file = $this->taskFileModel->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 createTaskFile($project_id, $task_id, $filename, $blob) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTaskFile', $project_id); + + try { + return $this->taskFileModel->uploadContent($task_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + public function removeTaskFile($file_id) + { + TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTaskFile', $file_id); + return $this->taskFileModel->remove($file_id); + } + + public function removeAllTaskFiles($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAllTaskFiles', $task_id); + return $this->taskFileModel->removeAll($task_id); + } +} diff --git a/app/Api/Procedure/TaskLinkProcedure.php b/app/Api/Procedure/TaskLinkProcedure.php new file mode 100644 index 00000000..375266fb --- /dev/null +++ b/app/Api/Procedure/TaskLinkProcedure.php @@ -0,0 +1,85 @@ +container)->check($this->getClassName(), 'getTaskLinkById', $task_link_id); + return $this->taskLinkModel->getById($task_link_id); + } + + /** + * Get all links attached to a task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAllTaskLinks($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTaskLinks', $task_id); + return $this->taskLinkModel->getAll($task_id); + } + + /** + * Create a new link + * + * @access public + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return integer Task link id + */ + public function createTaskLink($task_id, $opposite_task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTaskLink', $task_id); + return $this->taskLinkModel->create($task_id, $opposite_task_id, $link_id); + } + + /** + * Update a task link + * + * @access public + * @param integer $task_link_id Task link id + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return boolean + */ + public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateTaskLink', $task_id); + return $this->taskLinkModel->update($task_link_id, $task_id, $opposite_task_id, $link_id); + } + + /** + * Remove a link between two tasks + * + * @access public + * @param integer $task_link_id + * @return boolean + */ + public function removeTaskLink($task_link_id) + { + TaskLinkAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTaskLink', $task_link_id); + return $this->taskLinkModel->remove($task_link_id); + } +} diff --git a/app/Api/Procedure/TaskProcedure.php b/app/Api/Procedure/TaskProcedure.php new file mode 100644 index 00000000..2d29a4ef --- /dev/null +++ b/app/Api/Procedure/TaskProcedure.php @@ -0,0 +1,167 @@ +container)->check($this->getClassName(), 'searchTasks', $project_id); + return $this->taskLexer->build($query)->withFilter(new TaskProjectFilter($project_id))->toArray(); + } + + public function getTask($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getTask', $task_id); + return $this->formatTask($this->taskFinderModel->getById($task_id)); + } + + public function getTaskByReference($project_id, $reference) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getTaskByReference', $project_id); + return $this->formatTask($this->taskFinderModel->getByReference($project_id, $reference)); + } + + public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTasks', $project_id); + return $this->formatTasks($this->taskFinderModel->getAll($project_id, $status_id)); + } + + public function getOverdueTasks() + { + return $this->taskFinderModel->getOverdueTasks(); + } + + public function getOverdueTasksByProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getOverdueTasksByProject', $project_id); + return $this->taskFinderModel->getOverdueTasksByProject($project_id); + } + + public function openTask($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'openTask', $task_id); + return $this->taskStatusModel->open($task_id); + } + + public function closeTask($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'closeTask', $task_id); + return $this->taskStatusModel->close($task_id); + } + + public function removeTask($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTask', $task_id); + return $this->taskModel->remove($task_id); + } + + public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'moveTaskPosition', $project_id); + return $this->taskPositionModel->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); + } + + 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); + } + + 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); + } + + public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, + $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, $priority = 0, + $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, + $recurrence_basedate = 0, $reference = '') + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTask', $project_id); + + if ($owner_id !== 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { + return false; + } + + if ($this->userSession->isLogged()) { + $creator_id = $this->userSession->getId(); + } + + $values = array( + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'column_id' => $column_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + 'swimlane_id' => $swimlane_id, + 'recurrence_status' => $recurrence_status, + 'recurrence_trigger' => $recurrence_trigger, + 'recurrence_factor' => $recurrence_factor, + 'recurrence_timeframe' => $recurrence_timeframe, + 'recurrence_basedate' => $recurrence_basedate, + 'reference' => $reference, + 'priority' => $priority, + ); + + list($valid, ) = $this->taskValidator->validateCreation($values); + + return $valid ? $this->taskCreationModel->create($values) : false; + } + + public function updateTask($id, $title = null, $color_id = null, $owner_id = null, + $date_due = null, $description = null, $category_id = null, $score = null, $priority = null, + $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, + $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateTask', $id); + $project_id = $this->taskFinderModel->getProjectId($id); + + if ($project_id === 0) { + return false; + } + + if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { + return false; + } + + $values = $this->filterValues(array( + 'id' => $id, + 'title' => $title, + 'color_id' => $color_id, + 'owner_id' => $owner_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + 'recurrence_status' => $recurrence_status, + 'recurrence_trigger' => $recurrence_trigger, + 'recurrence_factor' => $recurrence_factor, + 'recurrence_timeframe' => $recurrence_timeframe, + 'recurrence_basedate' => $recurrence_basedate, + 'reference' => $reference, + 'priority' => $priority, + )); + + list($valid) = $this->taskValidator->validateApiModification($values); + return $valid && $this->taskModificationModel->update($values); + } +} diff --git a/app/Api/Procedure/UserProcedure.php b/app/Api/Procedure/UserProcedure.php new file mode 100644 index 00000000..145f85bf --- /dev/null +++ b/app/Api/Procedure/UserProcedure.php @@ -0,0 +1,131 @@ +userModel->getById($user_id); + } + + public function getUserByName($username) + { + return $this->userModel->getByUsername($username); + } + + public function getAllUsers() + { + return $this->userModel->getAll(); + } + + public function removeUser($user_id) + { + return $this->userModel->remove($user_id); + } + + public function disableUser($user_id) + { + return $this->userModel->disable($user_id); + } + + public function enableUser($user_id) + { + return $this->userModel->enable($user_id); + } + + public function isActiveUser($user_id) + { + return $this->userModel->isActive($user_id); + } + + public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER) + { + $values = array( + 'username' => $username, + 'password' => $password, + 'confirmation' => $password, + 'name' => $name, + 'email' => $email, + 'role' => $role, + ); + + list($valid, ) = $this->userValidator->validateCreation($values); + return $valid ? $this->userModel->create($values) : false; + } + + /** + * Create LDAP user in the database + * + * Only "anonymous" and "proxy" LDAP authentication are supported by this method + * + * User information will be fetched from the LDAP server + * + * @access public + * @param string $username + * @return bool|int + */ + public function createLdapUser($username) + { + if (LDAP_BIND_TYPE === 'user') { + $this->logger->error('LDAP authentication "user" is not supported by this API call'); + return false; + } + + try { + + $ldap = LdapClient::connect(); + $ldap->setLogger($this->logger); + $user = LdapUser::getUser($ldap, $username); + + if ($user === null) { + $this->logger->info('User not found in LDAP server'); + return false; + } + + if ($user->getUsername() === '') { + throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME'); + } + + $values = array( + 'username' => $user->getUsername(), + 'name' => $user->getName(), + 'email' => $user->getEmail(), + 'role' => $user->getRole(), + 'is_ldap_user' => 1, + ); + + return $this->userModel->create($values); + + } catch (LdapException $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + public function updateUser($id, $username = null, $name = null, $email = null, $role = null) + { + $values = $this->filterValues(array( + 'id' => $id, + 'username' => $username, + 'name' => $name, + 'email' => $email, + 'role' => $role, + )); + + list($valid, ) = $this->userValidator->validateApiModification($values); + return $valid && $this->userModel->update($values); + } +} diff --git a/app/Api/ProjectApi.php b/app/Api/ProjectApi.php deleted file mode 100644 index a726d4eb..00000000 --- a/app/Api/ProjectApi.php +++ /dev/null @@ -1,87 +0,0 @@ -checkProjectPermission($project_id); - return $this->formatProject($this->projectModel->getById($project_id)); - } - - public function getProjectByName($name) - { - return $this->formatProject($this->projectModel->getByName($name)); - } - - public function getAllProjects() - { - return $this->formatProjects($this->projectModel->getAll()); - } - - public function removeProject($project_id) - { - return $this->projectModel->remove($project_id); - } - - public function enableProject($project_id) - { - return $this->projectModel->enable($project_id); - } - - public function disableProject($project_id) - { - return $this->projectModel->disable($project_id); - } - - public function enableProjectPublicAccess($project_id) - { - return $this->projectModel->enablePublicAccess($project_id); - } - - public function disableProjectPublicAccess($project_id) - { - return $this->projectModel->disablePublicAccess($project_id); - } - - public function getProjectActivities(array $project_ids) - { - return $this->helper->projectActivity->getProjectsEvents($project_ids); - } - - public function getProjectActivity($project_id) - { - $this->checkProjectPermission($project_id); - return $this->helper->projectActivity->getProjectEvents($project_id); - } - - public function createProject($name, $description = null) - { - $values = array( - 'name' => $name, - 'description' => $description - ); - - list($valid, ) = $this->projectValidator->validateCreation($values); - return $valid ? $this->projectModel->create($values) : false; - } - - public function updateProject($project_id, $name, $description = null) - { - $values = $this->filterValues(array( - 'id' => $project_id, - 'name' => $name, - 'description' => $description - )); - - list($valid, ) = $this->projectValidator->validateModification($values); - return $valid && $this->projectModel->update($values); - } -} diff --git a/app/Api/ProjectPermissionApi.php b/app/Api/ProjectPermissionApi.php deleted file mode 100644 index 37c5e13c..00000000 --- a/app/Api/ProjectPermissionApi.php +++ /dev/null @@ -1,55 +0,0 @@ -projectUserRoleModel->getAllUsers($project_id); - } - - public function getAssignableUsers($project_id, $prepend_unassigned = false) - { - return $this->projectUserRoleModel->getAssignableUsersList($project_id, $prepend_unassigned); - } - - public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) - { - return $this->projectUserRoleModel->addUser($project_id, $user_id, $role); - } - - public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) - { - return $this->projectGroupRoleModel->addGroup($project_id, $group_id, $role); - } - - public function removeProjectUser($project_id, $user_id) - { - return $this->projectUserRoleModel->removeUser($project_id, $user_id); - } - - public function removeProjectGroup($project_id, $group_id) - { - return $this->projectGroupRoleModel->removeGroup($project_id, $group_id); - } - - public function changeProjectUserRole($project_id, $user_id, $role) - { - return $this->projectUserRoleModel->changeUserRole($project_id, $user_id, $role); - } - - public function changeProjectGroupRole($project_id, $group_id, $role) - { - return $this->projectGroupRoleModel->changeGroupRole($project_id, $group_id, $role); - } -} diff --git a/app/Api/SubtaskApi.php b/app/Api/SubtaskApi.php deleted file mode 100644 index 5764ff7d..00000000 --- a/app/Api/SubtaskApi.php +++ /dev/null @@ -1,66 +0,0 @@ -subtaskModel->getById($subtask_id); - } - - public function getAllSubtasks($task_id) - { - return $this->subtaskModel->getAll($task_id); - } - - public function removeSubtask($subtask_id) - { - return $this->subtaskModel->remove($subtask_id); - } - - public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0) - { - $values = array( - 'title' => $title, - 'task_id' => $task_id, - 'user_id' => $user_id, - 'time_estimated' => $time_estimated, - 'time_spent' => $time_spent, - 'status' => $status, - ); - - list($valid, ) = $this->subtaskValidator->validateCreation($values); - return $valid ? $this->subtaskModel->create($values) : false; - } - - public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null) - { - $values = array( - 'id' => $id, - 'task_id' => $task_id, - 'title' => $title, - 'user_id' => $user_id, - 'time_estimated' => $time_estimated, - 'time_spent' => $time_spent, - 'status' => $status, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } - - list($valid, ) = $this->subtaskValidator->validateApiModification($values); - return $valid && $this->subtaskModel->update($values); - } -} diff --git a/app/Api/SubtaskTimeTrackingApi.php b/app/Api/SubtaskTimeTrackingApi.php deleted file mode 100644 index 0e700b31..00000000 --- a/app/Api/SubtaskTimeTrackingApi.php +++ /dev/null @@ -1,34 +0,0 @@ -subtaskTimeTrackingModel->hasTimer($subtask_id,$user_id); - } - - public function logStartTime($subtask_id,$user_id) - { - return $this->subtaskTimeTrackingModel->logStartTime($subtask_id,$user_id); - } - - public function logEndTime($subtask_id,$user_id) - { - return $this->subtaskTimeTrackingModel->logEndTime($subtask_id,$user_id); - } - - public function getTimeSpent($subtask_id,$user_id) - { - return $this->subtaskTimeTrackingModel->getTimeSpent($subtask_id,$user_id); - } -} diff --git a/app/Api/SwimlaneApi.php b/app/Api/SwimlaneApi.php deleted file mode 100644 index c3c56a71..00000000 --- a/app/Api/SwimlaneApi.php +++ /dev/null @@ -1,80 +0,0 @@ -swimlaneModel->getSwimlanes($project_id); - } - - public function getAllSwimlanes($project_id) - { - return $this->swimlaneModel->getAll($project_id); - } - - public function getSwimlaneById($swimlane_id) - { - return $this->swimlaneModel->getById($swimlane_id); - } - - public function getSwimlaneByName($project_id, $name) - { - return $this->swimlaneModel->getByName($project_id, $name); - } - - public function getSwimlane($swimlane_id) - { - return $this->swimlaneModel->getById($swimlane_id); - } - - public function getDefaultSwimlane($project_id) - { - return $this->swimlaneModel->getDefault($project_id); - } - - public function addSwimlane($project_id, $name, $description = '') - { - return $this->swimlaneModel->create(array('project_id' => $project_id, 'name' => $name, 'description' => $description)); - } - - public function updateSwimlane($swimlane_id, $name, $description = null) - { - $values = array('id' => $swimlane_id, 'name' => $name); - - if (!is_null($description)) { - $values['description'] = $description; - } - - return $this->swimlaneModel->update($values); - } - - public function removeSwimlane($project_id, $swimlane_id) - { - return $this->swimlaneModel->remove($project_id, $swimlane_id); - } - - public function disableSwimlane($project_id, $swimlane_id) - { - return $this->swimlaneModel->disable($project_id, $swimlane_id); - } - - public function enableSwimlane($project_id, $swimlane_id) - { - return $this->swimlaneModel->enable($project_id, $swimlane_id); - } - - public function changeSwimlanePosition($project_id, $swimlane_id, $position) - { - return $this->swimlaneModel->changePosition($project_id, $swimlane_id, $position); - } -} diff --git a/app/Api/TaskApi.php b/app/Api/TaskApi.php deleted file mode 100644 index 523bfaa0..00000000 --- a/app/Api/TaskApi.php +++ /dev/null @@ -1,163 +0,0 @@ -checkProjectPermission($project_id); - return $this->taskLexer->build($query)->withFilter(new TaskProjectFilter($project_id))->toArray(); - } - - public function getTask($task_id) - { - $this->checkTaskPermission($task_id); - return $this->formatTask($this->taskFinderModel->getById($task_id)); - } - - public function getTaskByReference($project_id, $reference) - { - $this->checkProjectPermission($project_id); - return $this->formatTask($this->taskFinderModel->getByReference($project_id, $reference)); - } - - public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) - { - $this->checkProjectPermission($project_id); - return $this->formatTasks($this->taskFinderModel->getAll($project_id, $status_id)); - } - - public function getOverdueTasks() - { - return $this->taskFinderModel->getOverdueTasks(); - } - - public function getOverdueTasksByProject($project_id) - { - $this->checkProjectPermission($project_id); - return $this->taskFinderModel->getOverdueTasksByProject($project_id); - } - - public function openTask($task_id) - { - $this->checkTaskPermission($task_id); - return $this->taskStatusModel->open($task_id); - } - - public function closeTask($task_id) - { - $this->checkTaskPermission($task_id); - return $this->taskStatusModel->close($task_id); - } - - public function removeTask($task_id) - { - return $this->taskModel->remove($task_id); - } - - public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) - { - $this->checkProjectPermission($project_id); - return $this->taskPositionModel->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); - } - - public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) - { - return $this->taskDuplicationModel->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) - { - return $this->taskDuplicationModel->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, - $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, $priority = 0, - $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, - $recurrence_basedate = 0, $reference = '') - { - $this->checkProjectPermission($project_id); - - if ($owner_id !== 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { - return false; - } - - if ($this->userSession->isLogged()) { - $creator_id = $this->userSession->getId(); - } - - $values = array( - 'title' => $title, - 'project_id' => $project_id, - 'color_id' => $color_id, - 'column_id' => $column_id, - 'owner_id' => $owner_id, - 'creator_id' => $creator_id, - 'date_due' => $date_due, - 'description' => $description, - 'category_id' => $category_id, - 'score' => $score, - 'swimlane_id' => $swimlane_id, - 'recurrence_status' => $recurrence_status, - 'recurrence_trigger' => $recurrence_trigger, - 'recurrence_factor' => $recurrence_factor, - 'recurrence_timeframe' => $recurrence_timeframe, - 'recurrence_basedate' => $recurrence_basedate, - 'reference' => $reference, - 'priority' => $priority, - ); - - list($valid, ) = $this->taskValidator->validateCreation($values); - - return $valid ? $this->taskCreationModel->create($values) : false; - } - - public function updateTask($id, $title = null, $color_id = null, $owner_id = null, - $date_due = null, $description = null, $category_id = null, $score = null, $priority = null, - $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, - $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) - { - $this->checkTaskPermission($id); - - $project_id = $this->taskFinderModel->getProjectId($id); - - if ($project_id === 0) { - return false; - } - - if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { - return false; - } - - $values = $this->filterValues(array( - 'id' => $id, - 'title' => $title, - 'color_id' => $color_id, - 'owner_id' => $owner_id, - 'date_due' => $date_due, - 'description' => $description, - 'category_id' => $category_id, - 'score' => $score, - 'recurrence_status' => $recurrence_status, - 'recurrence_trigger' => $recurrence_trigger, - 'recurrence_factor' => $recurrence_factor, - 'recurrence_timeframe' => $recurrence_timeframe, - 'recurrence_basedate' => $recurrence_basedate, - 'reference' => $reference, - 'priority' => $priority, - )); - - list($valid) = $this->taskValidator->validateApiModification($values); - return $valid && $this->taskModificationModel->update($values); - } -} diff --git a/app/Api/TaskFileApi.php b/app/Api/TaskFileApi.php deleted file mode 100644 index 7b27477c..00000000 --- a/app/Api/TaskFileApi.php +++ /dev/null @@ -1,59 +0,0 @@ -taskFileModel->getById($file_id); - } - - public function getAllTaskFiles($task_id) - { - return $this->taskFileModel->getAll($task_id); - } - - public function downloadTaskFile($file_id) - { - try { - $file = $this->taskFileModel->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 createTaskFile($project_id, $task_id, $filename, $blob) - { - try { - return $this->taskFileModel->uploadContent($task_id, $filename, $blob); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return false; - } - } - - public function removeTaskFile($file_id) - { - return $this->taskFileModel->remove($file_id); - } - - public function removeAllTaskFiles($task_id) - { - return $this->taskFileModel->removeAll($task_id); - } -} diff --git a/app/Api/TaskLinkApi.php b/app/Api/TaskLinkApi.php deleted file mode 100644 index bb809133..00000000 --- a/app/Api/TaskLinkApi.php +++ /dev/null @@ -1,79 +0,0 @@ -taskLinkModel->getById($task_link_id); - } - - /** - * Get all links attached to a task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAllTaskLinks($task_id) - { - return $this->taskLinkModel->getAll($task_id); - } - - /** - * Create a new link - * - * @access public - * @param integer $task_id Task id - * @param integer $opposite_task_id Opposite task id - * @param integer $link_id Link id - * @return integer Task link id - */ - public function createTaskLink($task_id, $opposite_task_id, $link_id) - { - return $this->taskLinkModel->create($task_id, $opposite_task_id, $link_id); - } - - /** - * Update a task link - * - * @access public - * @param integer $task_link_id Task link id - * @param integer $task_id Task id - * @param integer $opposite_task_id Opposite task id - * @param integer $link_id Link id - * @return boolean - */ - public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id) - { - return $this->taskLinkModel->update($task_link_id, $task_id, $opposite_task_id, $link_id); - } - - /** - * Remove a link between two tasks - * - * @access public - * @param integer $task_link_id - * @return boolean - */ - public function removeTaskLink($task_link_id) - { - return $this->taskLinkModel->remove($task_link_id); - } -} diff --git a/app/Api/UserApi.php b/app/Api/UserApi.php deleted file mode 100644 index 6cb9df1c..00000000 --- a/app/Api/UserApi.php +++ /dev/null @@ -1,131 +0,0 @@ -userModel->getById($user_id); - } - - public function getUserByName($username) - { - return $this->userModel->getByUsername($username); - } - - public function getAllUsers() - { - return $this->userModel->getAll(); - } - - public function removeUser($user_id) - { - return $this->userModel->remove($user_id); - } - - public function disableUser($user_id) - { - return $this->userModel->disable($user_id); - } - - public function enableUser($user_id) - { - return $this->userModel->enable($user_id); - } - - public function isActiveUser($user_id) - { - return $this->userModel->isActive($user_id); - } - - public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER) - { - $values = array( - 'username' => $username, - 'password' => $password, - 'confirmation' => $password, - 'name' => $name, - 'email' => $email, - 'role' => $role, - ); - - list($valid, ) = $this->userValidator->validateCreation($values); - return $valid ? $this->userModel->create($values) : false; - } - - /** - * Create LDAP user in the database - * - * Only "anonymous" and "proxy" LDAP authentication are supported by this method - * - * User information will be fetched from the LDAP server - * - * @access public - * @param string $username - * @return bool|int - */ - public function createLdapUser($username) - { - if (LDAP_BIND_TYPE === 'user') { - $this->logger->error('LDAP authentication "user" is not supported by this API call'); - return false; - } - - try { - - $ldap = LdapClient::connect(); - $ldap->setLogger($this->logger); - $user = LdapUser::getUser($ldap, $username); - - if ($user === null) { - $this->logger->info('User not found in LDAP server'); - return false; - } - - if ($user->getUsername() === '') { - throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME'); - } - - $values = array( - 'username' => $user->getUsername(), - 'name' => $user->getName(), - 'email' => $user->getEmail(), - 'role' => $user->getRole(), - 'is_ldap_user' => 1, - ); - - return $this->userModel->create($values); - - } catch (LdapException $e) { - $this->logger->error($e->getMessage()); - return false; - } - } - - public function updateUser($id, $username = null, $name = null, $email = null, $role = null) - { - $values = $this->filterValues(array( - 'id' => $id, - 'username' => $username, - 'name' => $name, - 'email' => $email, - 'role' => $role, - )); - - list($valid, ) = $this->userValidator->validateApiModification($values); - return $valid && $this->userModel->update($values); - } -} diff --git a/app/Core/Base.php b/app/Core/Base.php index e5dd6ad9..eacca65d 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -35,8 +35,12 @@ use Pimple\Container; * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap * @property \Kanboard\Core\Security\AccessMap $projectAccessMap + * @property \Kanboard\Core\Security\AccessMap $apiAccessMap + * @property \Kanboard\Core\Security\AccessMap $apiProjectAccessMap * @property \Kanboard\Core\Security\Authorization $applicationAuthorization * @property \Kanboard\Core\Security\Authorization $projectAuthorization + * @property \Kanboard\Core\Security\Authorization $apiAuthorization + * @property \Kanboard\Core\Security\Authorization $apiProjectAuthorization * @property \Kanboard\Core\Security\Role $role * @property \Kanboard\Core\Security\Token $token * @property \Kanboard\Core\Session\FlashMessage $flash diff --git a/app/Model/ActionModel.php b/app/Model/ActionModel.php index 53393ed5..b5d2bd06 100644 --- a/app/Model/ActionModel.php +++ b/app/Model/ActionModel.php @@ -85,6 +85,18 @@ class ActionModel extends Base return $action; } + /** + * Get the projectId by the actionId + * + * @access public + * @param integer $action_id + * @return integer + */ + public function getProjectId($action_id) + { + return $this->db->table(self::TABLE)->eq('id', $action_id)->findOneColumn('project_id') ?: 0; + } + /** * Attach parameters to actions * diff --git a/app/Model/CategoryModel.php b/app/Model/CategoryModel.php index 62fb5611..024d0026 100644 --- a/app/Model/CategoryModel.php +++ b/app/Model/CategoryModel.php @@ -55,6 +55,18 @@ class CategoryModel extends Base return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: ''; } + /** + * Get the projectId by the category id + * + * @access public + * @param integer $category_id Category id + * @return integer + */ + public function getProjectId($category_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('project_id') ?: 0; + } + /** * Get a category id by the category name and project id * diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 1adac0f2..795fe692 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -31,6 +31,18 @@ class ColumnModel extends Base return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); } + /** + * Get projectId by the columnId + * + * @access public + * @param integer $column_id Column id + * @return integer + */ + public function getProjectId($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('project_id'); + } + /** * Get the first column id for a given project * diff --git a/app/Model/CommentModel.php b/app/Model/CommentModel.php index 36e1fc48..4231f29d 100644 --- a/app/Model/CommentModel.php +++ b/app/Model/CommentModel.php @@ -29,6 +29,22 @@ class CommentModel extends Base const EVENT_CREATE = 'comment.create'; const EVENT_USER_MENTION = 'comment.user.mention'; + /** + * Get projectId from commentId + * + * @access public + * @param integer $comment_id + * @return integer + */ + public function getProjectId($comment_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $comment_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get all comments for a given task * diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index 019064ad..a97bddbf 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -51,6 +51,22 @@ class SubtaskModel extends Base const EVENT_CREATE = 'subtask.create'; const EVENT_DELETE = 'subtask.delete'; + /** + * Get projectId from subtaskId + * + * @access public + * @param integer $subtask_id + * @return integer + */ + public function getProjectId($subtask_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get available status * diff --git a/app/Model/TaskFileModel.php b/app/Model/TaskFileModel.php index 24c1ad4b..7603019a 100644 --- a/app/Model/TaskFileModel.php +++ b/app/Model/TaskFileModel.php @@ -72,6 +72,22 @@ class TaskFileModel extends FileModel return self::EVENT_CREATE; } + /** + * Get projectId from fileId + * + * @access public + * @param integer $file_id + * @return integer + */ + public function getProjectId($file_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $file_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Handle screenshot upload * diff --git a/app/Model/TaskLinkModel.php b/app/Model/TaskLinkModel.php index 45225e35..09978eae 100644 --- a/app/Model/TaskLinkModel.php +++ b/app/Model/TaskLinkModel.php @@ -28,6 +28,22 @@ class TaskLinkModel extends Base */ const EVENT_CREATE_UPDATE = 'tasklink.create_update'; + /** + * Get projectId from $task_link_id + * + * @access public + * @param integer $task_link_id + * @return integer + */ + public function getProjectId($task_link_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $task_link_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get a task link * diff --git a/app/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php index e0312056..f88d9b4f 100644 --- a/app/ServiceProvider/ApiProvider.php +++ b/app/ServiceProvider/ApiProvider.php @@ -3,26 +3,26 @@ namespace Kanboard\ServiceProvider; use JsonRPC\Server; -use Kanboard\Api\ActionApi; -use Kanboard\Api\AppApi; -use Kanboard\Api\BoardApi; -use Kanboard\Api\CategoryApi; -use Kanboard\Api\ColumnApi; -use Kanboard\Api\CommentApi; -use Kanboard\Api\TaskFileApi; -use Kanboard\Api\GroupApi; -use Kanboard\Api\GroupMemberApi; -use Kanboard\Api\LinkApi; -use Kanboard\Api\MeApi; -use Kanboard\Api\Middleware\AuthenticationApiMiddleware; -use Kanboard\Api\ProjectApi; -use Kanboard\Api\ProjectPermissionApi; -use Kanboard\Api\SubtaskApi; -use Kanboard\Api\SubtaskTimeTrackingApi; -use Kanboard\Api\SwimlaneApi; -use Kanboard\Api\TaskApi; -use Kanboard\Api\TaskLinkApi; -use Kanboard\Api\UserApi; +use Kanboard\Api\Procedure\ActionProcedure; +use Kanboard\Api\Procedure\AppProcedure; +use Kanboard\Api\Procedure\BoardProcedure; +use Kanboard\Api\Procedure\CategoryProcedure; +use Kanboard\Api\Procedure\ColumnProcedure; +use Kanboard\Api\Procedure\CommentProcedure; +use Kanboard\Api\Procedure\TaskFileProcedure; +use Kanboard\Api\Procedure\GroupProcedure; +use Kanboard\Api\Procedure\GroupMemberProcedure; +use Kanboard\Api\Procedure\LinkProcedure; +use Kanboard\Api\Procedure\MeProcedure; +use Kanboard\Api\Middleware\AuthenticationMiddleware; +use Kanboard\Api\Procedure\ProjectProcedure; +use Kanboard\Api\Procedure\ProjectPermissionProcedure; +use Kanboard\Api\Procedure\SubtaskProcedure; +use Kanboard\Api\Procedure\SubtaskTimeTrackingProcedure; +use Kanboard\Api\Procedure\SwimlaneProcedure; +use Kanboard\Api\Procedure\TaskProcedure; +use Kanboard\Api\Procedure\TaskLinkProcedure; +use Kanboard\Api\Procedure\UserProcedure; use Pimple\Container; use Pimple\ServiceProviderInterface; @@ -45,31 +45,32 @@ class ApiProvider implements ServiceProviderInterface $server = new Server(); $server->setAuthenticationHeader(API_AUTHENTICATION_HEADER); $server->getMiddlewareHandler() - ->withMiddleware(new AuthenticationApiMiddleware($container)) + ->withMiddleware(new AuthenticationMiddleware($container)) ; $server->getProcedureHandler() - ->withObject(new MeApi($container)) - ->withObject(new ActionApi($container)) - ->withObject(new AppApi($container)) - ->withObject(new BoardApi($container)) - ->withObject(new ColumnApi($container)) - ->withObject(new CategoryApi($container)) - ->withObject(new CommentApi($container)) - ->withObject(new TaskFileApi($container)) - ->withObject(new LinkApi($container)) - ->withObject(new ProjectApi($container)) - ->withObject(new ProjectPermissionApi($container)) - ->withObject(new SubtaskApi($container)) - ->withObject(new SubtaskTimeTrackingApi($container)) - ->withObject(new SwimlaneApi($container)) - ->withObject(new TaskApi($container)) - ->withObject(new TaskLinkApi($container)) - ->withObject(new UserApi($container)) - ->withObject(new GroupApi($container)) - ->withObject(new GroupMemberApi($container)) + ->withObject(new MeProcedure($container)) + ->withObject(new ActionProcedure($container)) + ->withObject(new AppProcedure($container)) + ->withObject(new BoardProcedure($container)) + ->withObject(new ColumnProcedure($container)) + ->withObject(new CategoryProcedure($container)) + ->withObject(new CommentProcedure($container)) + ->withObject(new TaskFileProcedure($container)) + ->withObject(new LinkProcedure($container)) + ->withObject(new ProjectProcedure($container)) + ->withObject(new ProjectPermissionProcedure($container)) + ->withObject(new SubtaskProcedure($container)) + ->withObject(new SubtaskTimeTrackingProcedure($container)) + ->withObject(new SwimlaneProcedure($container)) + ->withObject(new TaskProcedure($container)) + ->withObject(new TaskLinkProcedure($container)) + ->withObject(new UserProcedure($container)) + ->withObject(new GroupProcedure($container)) + ->withObject(new GroupMemberProcedure($container)) + ->withBeforeMethod('beforeProcedure') ; - + $container['api'] = $server; return $container; } diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index 84e4354d..751fe514 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -46,9 +46,13 @@ class AuthenticationProvider implements ServiceProviderInterface $container['projectAccessMap'] = $this->getProjectAccessMap(); $container['applicationAccessMap'] = $this->getApplicationAccessMap(); + $container['apiAccessMap'] = $this->getApiAccessMap(); + $container['apiProjectAccessMap'] = $this->getApiProjectAccessMap(); $container['projectAuthorization'] = new Authorization($container['projectAccessMap']); $container['applicationAuthorization'] = new Authorization($container['applicationAccessMap']); + $container['apiAuthorization'] = new Authorization($container['apiAccessMap']); + $container['apiProjectAuthorization'] = new Authorization($container['apiProjectAccessMap']); return $container; } @@ -151,4 +155,57 @@ class AuthenticationProvider implements ServiceProviderInterface return $acl; } + + /** + * Get ACL for the API + * + * @access public + * @return AccessMap + */ + public function getApiAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::APP_USER); + $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER, Role::APP_PUBLIC)); + $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC)); + + $acl->add('UserProcedure', '*', Role::APP_ADMIN); + $acl->add('GroupMemberProcedure', '*', Role::APP_ADMIN); + $acl->add('GroupProcedure', '*', Role::APP_ADMIN); + $acl->add('LinkProcedure', '*', Role::APP_ADMIN); + $acl->add('TaskProcedure', array('getOverdueTasks'), Role::APP_ADMIN); + $acl->add('ProjectProcedure', array('getAllProjects'), Role::APP_ADMIN); + $acl->add('ProjectProcedure', array('createProject'), Role::APP_MANAGER); + + return $acl; + } + + /** + * Get ACL for the API + * + * @access public + * @return AccessMap + */ + public function getApiProjectAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::PROJECT_VIEWER); + $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)); + $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER)); + + $acl->add('ActionProcedure', array('removeAction', 'getActions', 'createAction'), Role::PROJECT_MANAGER); + $acl->add('CategoryProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ColumnProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('CommentProcedure', array('removeComment', 'createComment', 'updateComment'), Role::PROJECT_MEMBER); + $acl->add('ProjectPermissionProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectProcedure', array('updateProject', 'removeProject', 'enableProject', 'disableProject', 'enableProjectPublicAccess', 'disableProjectPublicAccess'), Role::PROJECT_MANAGER); + $acl->add('SubtaskProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskTimeTrackingProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('SwimlaneProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('TaskFileProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskLinkProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskProcedure', '*', Role::PROJECT_MEMBER); + + return $acl; + } } diff --git a/composer.json b/composer.json index 85fdb5ad..bcac020e 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "christian-riesen/otp" : "1.4", "eluceo/ical": "0.8.0", "erusev/parsedown" : "1.6.0", - "fguillot/json-rpc" : "1.2.0", + "fguillot/json-rpc" : "1.2.1", "fguillot/picodb" : "1.0.12", "fguillot/simpleLogger" : "1.0.1", "fguillot/simple-validator" : "1.0.0", diff --git a/composer.lock b/composer.lock index e0177ed5..e6a72582 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "2de2026649db7bc41653bef80f974c6a", - "content-hash": "ea8ef74f1f1cf53b9f96b7609d756873", + "hash": "283af0b856598f5bc3d8ee0b226959e5", + "content-hash": "18c0bbff5406ceb8b567d9655de26746", "packages": [ { "name": "christian-riesen/base32", @@ -203,16 +203,16 @@ }, { "name": "fguillot/json-rpc", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/fguillot/JsonRPC.git", - "reference": "b002320b10aa1eeb7aee83f7b703cd6a6e99ff78" + "reference": "d491bb549bfa11aff4c37abcea2ffb28c9523f69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/b002320b10aa1eeb7aee83f7b703cd6a6e99ff78", - "reference": "b002320b10aa1eeb7aee83f7b703cd6a6e99ff78", + "url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/d491bb549bfa11aff4c37abcea2ffb28c9523f69", + "reference": "d491bb549bfa11aff4c37abcea2ffb28c9523f69", "shasum": "" }, "require": { @@ -238,7 +238,7 @@ ], "description": "Simple Json-RPC client/server library that just works", "homepage": "https://github.com/fguillot/JsonRPC", - "time": "2016-05-29 13:06:36" + "time": "2016-06-25 23:11:10" }, { "name": "fguillot/picodb", @@ -682,16 +682,16 @@ }, { "name": "symfony/console", - "version": "v2.8.6", + "version": "v2.8.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609" + "reference": "5ac8bc9aa77bb2edf06af3a1bb6bc1020d23acd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/48221d3de4dc22d2cd57c97e8b9361821da86609", - "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609", + "url": "https://api.github.com/repos/symfony/console/zipball/5ac8bc9aa77bb2edf06af3a1bb6bc1020d23acd3", + "reference": "5ac8bc9aa77bb2edf06af3a1bb6bc1020d23acd3", "shasum": "" }, "require": { @@ -738,20 +738,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-04-26 12:00:47" + "time": "2016-06-06 15:06:25" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.6", + "version": "v2.8.7", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a158f13992a3147d466af7a23b564ac719a4ddd8" + "reference": "2a6b8713f8bdb582058cfda463527f195b066110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a158f13992a3147d466af7a23b564ac719a4ddd8", - "reference": "a158f13992a3147d466af7a23b564ac719a4ddd8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2a6b8713f8bdb582058cfda463527f195b066110", + "reference": "2a6b8713f8bdb582058cfda463527f195b066110", "shasum": "" }, "require": { @@ -798,7 +798,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-05-03 18:59:18" + "time": "2016-06-06 11:11:27" }, { "name": "symfony/polyfill-mbstring", @@ -915,39 +915,136 @@ ], "time": "2015-06-14 21:17:01" }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, { "name": "phpdocumentor/reflection-docblock", - "version": "2.0.4", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd", + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-06-10 09:48:41" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "psr-0": { - "phpDocumentor": [ + "psr-4": { + "phpDocumentor\\Reflection\\": [ "src/" ] } @@ -959,39 +1056,39 @@ "authors": [ { "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" + "email": "me@mikevanriel.com" } ], - "time": "2015-02-03 12:10:50" + "time": "2016-06-10 07:14:17" }, { "name": "phpspec/prophecy", - "version": "v1.6.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" + "reference": "58a8137754bc24b25740d4281399a4a3596058e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "~2.0", - "sebastian/comparator": "~1.1", - "sebastian/recursion-context": "~1.0" + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0" }, "require-dev": { - "phpspec/phpspec": "~2.0" + "phpspec/phpspec": "^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -1024,7 +1121,7 @@ "spy", "stub" ], - "time": "2016-02-15 07:46:21" + "time": "2016-06-07 08:13:47" }, { "name": "phpunit/php-code-coverage", @@ -1629,16 +1726,16 @@ }, { "name": "sebastian/exporter", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e" + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", "shasum": "" }, "require": { @@ -1646,12 +1743,13 @@ "sebastian/recursion-context": "~1.0" }, "require-dev": { + "ext-mbstring": "*", "phpunit/phpunit": "~4.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -1691,7 +1789,7 @@ "export", "exporter" ], - "time": "2015-06-21 07:55:53" + "time": "2016-06-17 09:04:28" }, { "name": "sebastian/global-state", @@ -1834,16 +1932,16 @@ }, { "name": "symfony/stopwatch", - "version": "v2.8.6", + "version": "v2.8.7", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e" + "reference": "5e628055488bcc42dbace3af65be435d094e37e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/9e24824b2a9a16e17ab997f61d70bc03948e434e", - "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5e628055488bcc42dbace3af65be435d094e37e4", + "reference": "5e628055488bcc42dbace3af65be435d094e37e4", "shasum": "" }, "require": { @@ -1879,7 +1977,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2016-03-04 07:54:35" + "time": "2016-06-06 11:11:27" }, { "name": "symfony/yaml", @@ -1927,6 +2025,55 @@ "description": "Symfony Yaml Component", "homepage": "http://symfony.com", "time": "2012-08-22 13:48:41" + }, + { + "name": "webmozart/assert", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", + "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2015-08-24 13:29:44" } ], "aliases": [], diff --git a/doc/api-authentication.markdown b/doc/api-authentication.markdown index 962e5b1b..3ba1e8f5 100644 --- a/doc/api-authentication.markdown +++ b/doc/api-authentication.markdown @@ -1,48 +1,26 @@ API Authentication ================== -Default method (HTTP Basic) ---------------------------- +API endpoint +------------ + +URL: `https://YOUR_SERVER/jsonrpc.php` -The API credentials are available on the settings page. -- API end-point: `https://YOUR_SERVER/jsonrpc.php` +Default method (HTTP Basic) +--------------------------- -If you want to use the "application api": +### Application credentials - Username: `jsonrpc` - Password: API token on the settings page -Otherwise for the "user api", just use the real username/passsword. +### User credentials + +- Use the real username and password The API use the [HTTP Basic Authentication Scheme described in the RFC2617](http://www.ietf.org/rfc/rfc2617.txt). -If there is an authentication error, you will receive the HTTP status code `401 Not Authorized`. - -### Authorized User API procedures - -- getMe -- getMyDashboard -- getMyActivityStream -- createMyPrivateProject -- getMyProjectsList -- getMyProjects -- getTimezone -- getVersion -- getDefaultTaskColor -- getDefaultTaskColors -- getColorList -- getProjectById -- getTask -- getTaskByReference -- getAllTasks -- openTask -- closeTask -- moveTaskPosition -- createTask -- updateTask -- getBoard -- getProjectActivity -- getMyOverdueTasks + Custom HTTP header ------------------ @@ -64,3 +42,14 @@ curl \ -d '{"jsonrpc": "2.0", "method": "getAllProjects", "id": 1}' \ http://localhost/kanboard/jsonrpc.php ``` + +Authentication error +-------------------- + +If the credentials are wrong, you will receive a `401 Not Authorized` and the corresponding JSON response. + + +Authorization error +------------------- + +If the connected user is not allowed to access to the resource, you will receive a `403 Forbidden`. diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown index bb14b008..0f922a7c 100644 --- a/doc/api-json-rpc.markdown +++ b/doc/api-json-rpc.markdown @@ -8,25 +8,25 @@ There are two types of API access: ### Application API -- Access to the API with the user "jsonrpc" and the token available in settings +- Access to the API with the user "jsonrpc" and the token available on the settings page - Access to all procedures - No permission checked - There is no user session on the server +- No access to procedures that starts with "My..." (example: "getMe" or "getMyProjects") - Example of possible clients: tools to migrate/import data, create tasks from another system, etc... ### User API - Access to the API with the user credentials (username and password) -- Access to a restricted set of procedures -- The project permissions are checked +- Application role and project permissions are checked for each procedure - A user session is created on the server -- Example of possible clients: mobile/desktop application, command line utility, etc... +- Example of possible clients: native mobile/desktop application, command line utility, etc... Security -------- -- Always use HTTPS with a valid certificate -- If you make a mobile application, it's your job to store securely the user credentials on the device +- Always use HTTPS with a valid certificate (avoid clear text communication) +- If you make a mobile application, it's your responsability to store securely the user credentials on the device - After 3 authentication failure on the user api, the end-user have to unlock his account by using the login form - Two factor authentication is not yet available through the API diff --git a/doc/api-project-permission-procedures.markdown b/doc/api-project-permission-procedures.markdown index 2844ae3c..d5e9b066 100644 --- a/doc/api-project-permission-procedures.markdown +++ b/doc/api-project-permission-procedures.markdown @@ -272,3 +272,36 @@ Response example: "result": true } ``` + +## getProjectUserRole + +- Purpose: **Get the role of a user for a given project** +- Parameters: + - **project_id** (integer, required) + - **user_id** (integer, required) +- Result on success: **role name** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getProjectUserRole", + "id": 2114673298, + "params": [ + "2", + "3" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2114673298, + "result": "project-viewer" +} +``` diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown index f8e2cc5e..3f8d33c2 100644 --- a/doc/api-project-procedures.markdown +++ b/doc/api-project-procedures.markdown @@ -7,6 +7,8 @@ API Project Procedures - Parameters: - **name** (string, required) - **description** (string, optional) + - **owner_id** (integer, optional) + - **identifier** (string, optional) - Result on success: **project_id** - Result on failure: **false** @@ -186,6 +188,8 @@ Response example: - **project_id** (integer, required) - **name** (string, required) - **description** (string, optional) + - **owner_id** (integer, optional) + - **identifier** (string, optional) - Result on success: **true** - Result on failure: **false** diff --git a/tests/integration/ActionProcedureTest.php b/tests/integration/ActionProcedureTest.php new file mode 100644 index 00000000..432de3d3 --- /dev/null +++ b/tests/integration/ActionProcedureTest.php @@ -0,0 +1,66 @@ +app->getAvailableActions(); + $this->assertNotEmpty($actions); + $this->assertInternalType('array', $actions); + $this->assertArrayHasKey('\Kanboard\Action\TaskCloseColumn', $actions); + } + + public function testGetAvailableActionEvents() + { + $events = $this->app->getAvailableActionEvents(); + $this->assertNotEmpty($events); + $this->assertInternalType('array', $events); + $this->assertArrayHasKey('task.move.column', $events); + } + + public function testGetCompatibleActionEvents() + { + $events = $this->app->getCompatibleActionEvents('\Kanboard\Action\TaskCloseColumn'); + $this->assertNotEmpty($events); + $this->assertInternalType('array', $events); + $this->assertArrayHasKey('task.move.column', $events); + } + + public function testCRUD() + { + $this->assertCreateTeamProject(); + $this->assertCreateAction(); + $this->assertGetActions(); + $this->assertRemoveAction(); + } + + public function assertCreateAction() + { + $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertNotFalse($actionId); + $this->assertTrue($actionId > 0); + } + + public function assertGetActions() + { + $actions = $this->app->getActions($this->projectId); + $this->assertNotEmpty($actions); + $this->assertInternalType('array', $actions); + $this->assertArrayHasKey('id', $actions[0]); + $this->assertArrayHasKey('project_id', $actions[0]); + $this->assertArrayHasKey('event_name', $actions[0]); + $this->assertArrayHasKey('action_name', $actions[0]); + $this->assertArrayHasKey('params', $actions[0]); + $this->assertArrayHasKey('column_id', $actions[0]['params']); + } + + public function assertRemoveAction() + { + $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertTrue($this->app->removeAction($actionId)); + } +} diff --git a/tests/integration/ActionTest.php b/tests/integration/ActionTest.php deleted file mode 100644 index 7a5adc4a..00000000 --- a/tests/integration/ActionTest.php +++ /dev/null @@ -1,66 +0,0 @@ -app->getAvailableActions(); - $this->assertNotEmpty($actions); - $this->assertInternalType('array', $actions); - $this->assertArrayHasKey('\Kanboard\Action\TaskCloseColumn', $actions); - } - - public function testGetAvailableActionEvents() - { - $events = $this->app->getAvailableActionEvents(); - $this->assertNotEmpty($events); - $this->assertInternalType('array', $events); - $this->assertArrayHasKey('task.move.column', $events); - } - - public function testGetCompatibleActionEvents() - { - $events = $this->app->getCompatibleActionEvents('\Kanboard\Action\TaskCloseColumn'); - $this->assertNotEmpty($events); - $this->assertInternalType('array', $events); - $this->assertArrayHasKey('task.move.column', $events); - } - - public function testCRUD() - { - $this->assertCreateTeamProject(); - $this->assertCreateAction(); - $this->assertGetActions(); - $this->assertRemoveAction(); - } - - public function assertCreateAction() - { - $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); - $this->assertNotFalse($actionId); - $this->assertTrue($actionId > 0); - } - - public function assertGetActions() - { - $actions = $this->app->getActions($this->projectId); - $this->assertNotEmpty($actions); - $this->assertInternalType('array', $actions); - $this->assertArrayHasKey('id', $actions[0]); - $this->assertArrayHasKey('project_id', $actions[0]); - $this->assertArrayHasKey('event_name', $actions[0]); - $this->assertArrayHasKey('action_name', $actions[0]); - $this->assertArrayHasKey('params', $actions[0]); - $this->assertArrayHasKey('column_id', $actions[0]['params']); - } - - public function assertRemoveAction() - { - $actionId = $this->app->createAction($this->projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); - $this->assertTrue($this->app->removeAction($actionId)); - } -} diff --git a/tests/integration/AppProcedureTest.php b/tests/integration/AppProcedureTest.php new file mode 100644 index 00000000..06135dac --- /dev/null +++ b/tests/integration/AppProcedureTest.php @@ -0,0 +1,54 @@ +assertEquals('UTC', $this->app->getTimezone()); + } + + public function testGetVersion() + { + $this->assertEquals('master', $this->app->getVersion()); + } + + public function testGetApplicationRoles() + { + $roles = $this->app->getApplicationRoles(); + $this->assertCount(3, $roles); + $this->assertEquals('Administrator', $roles['app-admin']); + $this->assertEquals('Manager', $roles['app-manager']); + $this->assertEquals('User', $roles['app-user']); + } + + public function testGetProjectRoles() + { + $roles = $this->app->getProjectRoles(); + $this->assertCount(3, $roles); + $this->assertEquals('Project Manager', $roles['project-manager']); + $this->assertEquals('Project Member', $roles['project-member']); + $this->assertEquals('Project Viewer', $roles['project-viewer']); + } + + public function testGetDefaultColor() + { + $this->assertEquals('yellow', $this->user->getDefaultTaskColor()); + } + + public function testGetDefaultColors() + { + $colors = $this->user->getDefaultTaskColors(); + $this->assertNotEmpty($colors); + $this->assertArrayHasKey('red', $colors); + } + + public function testGetColorList() + { + $colors = $this->user->getColorList(); + $this->assertNotEmpty($colors); + $this->assertArrayHasKey('red', $colors); + $this->assertEquals('Red', $colors['red']); + } +} diff --git a/tests/integration/AppTest.php b/tests/integration/AppTest.php deleted file mode 100644 index 287e6299..00000000 --- a/tests/integration/AppTest.php +++ /dev/null @@ -1,54 +0,0 @@ -assertEquals('UTC', $this->app->getTimezone()); - } - - public function testGetVersion() - { - $this->assertEquals('master', $this->app->getVersion()); - } - - public function testGetApplicationRoles() - { - $roles = $this->app->getApplicationRoles(); - $this->assertCount(3, $roles); - $this->assertEquals('Administrator', $roles['app-admin']); - $this->assertEquals('Manager', $roles['app-manager']); - $this->assertEquals('User', $roles['app-user']); - } - - public function testGetProjectRoles() - { - $roles = $this->app->getProjectRoles(); - $this->assertCount(3, $roles); - $this->assertEquals('Project Manager', $roles['project-manager']); - $this->assertEquals('Project Member', $roles['project-member']); - $this->assertEquals('Project Viewer', $roles['project-viewer']); - } - - public function testGetDefaultColor() - { - $this->assertEquals('yellow', $this->user->getDefaultTaskColor()); - } - - public function testGetDefaultColors() - { - $colors = $this->user->getDefaultTaskColors(); - $this->assertNotEmpty($colors); - $this->assertArrayHasKey('red', $colors); - } - - public function testGetColorList() - { - $colors = $this->user->getColorList(); - $this->assertNotEmpty($colors); - $this->assertArrayHasKey('red', $colors); - $this->assertEquals('Red', $colors['red']); - } -} diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php deleted file mode 100644 index cd837173..00000000 --- a/tests/integration/BaseIntegrationTest.php +++ /dev/null @@ -1,122 +0,0 @@ -setUpAppClient(); - $this->setUpAdminUser(); - $this->setUpManagerUser(); - $this->setUpStandardUser(); - } - - public function setUpAppClient() - { - $this->app = new JsonRPC\Client(API_URL); - $this->app->authentication('jsonrpc', API_KEY); - $this->app->getHttpClient()->withDebug()->withTimeout(10); - } - - public function setUpAdminUser() - { - $this->adminUserId = $this->getUserId('superuser'); - - if (! $this->adminUserId) { - $this->adminUserId = $this->app->createUser('superuser', 'password', 'Admin User', 'user@localhost', 'app-admin'); - $this->assertNotFalse($this->adminUserId); - } - - $this->admin = new JsonRPC\Client(API_URL); - $this->admin->authentication('superuser', 'password'); - $this->admin->getHttpClient()->withDebug(); - } - - public function setUpManagerUser() - { - $this->managerUserId = $this->getUserId('manager'); - - if (! $this->managerUserId) { - $this->managerUserId = $this->app->createUser('manager', 'password', 'Manager User', 'user@localhost', 'app-manager'); - $this->assertNotFalse($this->managerUserId); - } - - $this->manager = new JsonRPC\Client(API_URL); - $this->manager->authentication('manager', 'password'); - $this->manager->getHttpClient()->withDebug(); - } - - public function setUpStandardUser() - { - $this->userUserId = $this->getUserId('user'); - - if (! $this->userUserId) { - $this->userUserId = $this->app->createUser('user', 'password', 'Standard User', 'user@localhost', 'app-user'); - $this->assertNotFalse($this->userUserId); - } - - $this->user = new JsonRPC\Client(API_URL); - $this->user->authentication('user', 'password'); - $this->user->getHttpClient()->withDebug(); - } - - public function getUserId($username) - { - $user = $this->app->getUserByName($username); - - if (! empty($user)) { - return $user['id']; - } - - return 0; - } - - public function assertCreateTeamProject() - { - $this->projectId = $this->app->createProject($this->projectName, 'Description'); - $this->assertNotFalse($this->projectId); - } - - public function assertCreateUser() - { - $this->userId = $this->app->createUser($this->username, 'password'); - $this->assertNotFalse($this->userId); - } - - public function assertCreateGroups() - { - $this->groupId1 = $this->app->createGroup($this->groupName1); - $this->groupId2 = $this->app->createGroup($this->groupName2, 'External ID'); - $this->assertNotFalse($this->groupId1); - $this->assertNotFalse($this->groupId2); - } - - public function assertCreateTask() - { - $this->taskId = $this->app->createTask(array('title' => $this->taskTitle, 'project_id' => $this->projectId)); - $this->assertNotFalse($this->taskId); - } -} diff --git a/tests/integration/BaseProcedureTest.php b/tests/integration/BaseProcedureTest.php new file mode 100644 index 00000000..e3382e82 --- /dev/null +++ b/tests/integration/BaseProcedureTest.php @@ -0,0 +1,122 @@ +setUpAppClient(); + $this->setUpAdminUser(); + $this->setUpManagerUser(); + $this->setUpStandardUser(); + } + + public function setUpAppClient() + { + $this->app = new JsonRPC\Client(API_URL); + $this->app->authentication('jsonrpc', API_KEY); + $this->app->getHttpClient()->withDebug()->withTimeout(10); + } + + public function setUpAdminUser() + { + $this->adminUserId = $this->getUserId('superuser'); + + if (! $this->adminUserId) { + $this->adminUserId = $this->app->createUser('superuser', 'password', 'Admin User', 'user@localhost', 'app-admin'); + $this->assertNotFalse($this->adminUserId); + } + + $this->admin = new JsonRPC\Client(API_URL); + $this->admin->authentication('superuser', 'password'); + $this->admin->getHttpClient()->withDebug(); + } + + public function setUpManagerUser() + { + $this->managerUserId = $this->getUserId('manager'); + + if (! $this->managerUserId) { + $this->managerUserId = $this->app->createUser('manager', 'password', 'Manager User', 'user@localhost', 'app-manager'); + $this->assertNotFalse($this->managerUserId); + } + + $this->manager = new JsonRPC\Client(API_URL); + $this->manager->authentication('manager', 'password'); + $this->manager->getHttpClient()->withDebug(); + } + + public function setUpStandardUser() + { + $this->userUserId = $this->getUserId('user'); + + if (! $this->userUserId) { + $this->userUserId = $this->app->createUser('user', 'password', 'Standard User', 'user@localhost', 'app-user'); + $this->assertNotFalse($this->userUserId); + } + + $this->user = new JsonRPC\Client(API_URL); + $this->user->authentication('user', 'password'); + $this->user->getHttpClient()->withDebug(); + } + + public function getUserId($username) + { + $user = $this->app->getUserByName($username); + + if (! empty($user)) { + return $user['id']; + } + + return 0; + } + + public function assertCreateTeamProject() + { + $this->projectId = $this->app->createProject($this->projectName, 'Description'); + $this->assertNotFalse($this->projectId); + } + + public function assertCreateUser() + { + $this->userId = $this->app->createUser($this->username, 'password'); + $this->assertNotFalse($this->userId); + } + + public function assertCreateGroups() + { + $this->groupId1 = $this->app->createGroup($this->groupName1); + $this->groupId2 = $this->app->createGroup($this->groupName2, 'External ID'); + $this->assertNotFalse($this->groupId1); + $this->assertNotFalse($this->groupId2); + } + + public function assertCreateTask() + { + $this->taskId = $this->app->createTask(array('title' => $this->taskTitle, 'project_id' => $this->projectId)); + $this->assertNotFalse($this->taskId); + } +} diff --git a/tests/integration/BoardProcedureTest.php b/tests/integration/BoardProcedureTest.php new file mode 100644 index 00000000..273e93c7 --- /dev/null +++ b/tests/integration/BoardProcedureTest.php @@ -0,0 +1,25 @@ +assertCreateTeamProject(); + $this->assertGetBoard(); + } + + public function assertGetBoard() + { + $board = $this->app->getBoard($this->projectId); + $this->assertNotNull($board); + $this->assertCount(1, $board); + $this->assertEquals('Default swimlane', $board[0]['name']); + + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals('Ready', $board[0]['columns'][1]['title']); + } +} diff --git a/tests/integration/BoardTest.php b/tests/integration/BoardTest.php deleted file mode 100644 index aa0f61ff..00000000 --- a/tests/integration/BoardTest.php +++ /dev/null @@ -1,25 +0,0 @@ -assertCreateTeamProject(); - $this->assertGetBoard(); - } - - public function assertGetBoard() - { - $board = $this->app->getBoard($this->projectId); - $this->assertNotNull($board); - $this->assertCount(1, $board); - $this->assertEquals('Default swimlane', $board[0]['name']); - - $this->assertCount(4, $board[0]['columns']); - $this->assertEquals('Ready', $board[0]['columns'][1]['title']); - } -} diff --git a/tests/integration/CategoryProcedureTest.php b/tests/integration/CategoryProcedureTest.php new file mode 100644 index 00000000..2f5294ba --- /dev/null +++ b/tests/integration/CategoryProcedureTest.php @@ -0,0 +1,76 @@ +assertCreateTeamProject(); + $this->assertCreateCategory(); + $this->assertThatCategoriesAreUnique(); + $this->assertGetCategory(); + $this->assertGetAllCategories(); + $this->assertCategoryUpdate(); + $this->assertRemoveCategory(); + } + + public function assertCreateCategory() + { + $this->categoryId = $this->app->createCategory(array( + 'name' => 'Category', + 'project_id' => $this->projectId, + )); + + $this->assertNotFalse($this->categoryId); + } + + public function assertThatCategoriesAreUnique() + { + $this->assertFalse($this->app->execute('createCategory', array( + 'name' => 'Category', + 'project_id' => $this->projectId, + ))); + } + + public function assertGetCategory() + { + $category = $this->app->getCategory($this->categoryId); + + $this->assertInternalType('array', $category); + $this->assertEquals($this->categoryId, $category['id']); + $this->assertEquals('Category', $category['name']); + $this->assertEquals($this->projectId, $category['project_id']); + } + + public function assertGetAllCategories() + { + $categories = $this->app->getAllCategories($this->projectId); + + $this->assertCount(1, $categories); + $this->assertEquals($this->categoryId, $categories[0]['id']); + $this->assertEquals('Category', $categories[0]['name']); + $this->assertEquals($this->projectId, $categories[0]['project_id']); + } + + public function assertCategoryUpdate() + { + $this->assertTrue($this->app->execute('updateCategory', array( + 'id' => $this->categoryId, + 'name' => 'Renamed category', + ))); + + $category = $this->app->getCategory($this->categoryId); + $this->assertEquals('Renamed category', $category['name']); + } + + public function assertRemoveCategory() + { + $this->assertTrue($this->app->removeCategory($this->categoryId)); + $this->assertFalse($this->app->removeCategory($this->categoryId)); + $this->assertFalse($this->app->removeCategory(1111)); + } +} diff --git a/tests/integration/CategoryTest.php b/tests/integration/CategoryTest.php deleted file mode 100644 index c1aec0bc..00000000 --- a/tests/integration/CategoryTest.php +++ /dev/null @@ -1,76 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateCategory(); - $this->assertThatCategoriesAreUnique(); - $this->assertGetCategory(); - $this->assertGetAllCategories(); - $this->assertCategoryUpdate(); - $this->assertRemoveCategory(); - } - - public function assertCreateCategory() - { - $this->categoryId = $this->app->createCategory(array( - 'name' => 'Category', - 'project_id' => $this->projectId, - )); - - $this->assertNotFalse($this->categoryId); - } - - public function assertThatCategoriesAreUnique() - { - $this->assertFalse($this->app->execute('createCategory', array( - 'name' => 'Category', - 'project_id' => $this->projectId, - ))); - } - - public function assertGetCategory() - { - $category = $this->app->getCategory($this->categoryId); - - $this->assertInternalType('array', $category); - $this->assertEquals($this->categoryId, $category['id']); - $this->assertEquals('Category', $category['name']); - $this->assertEquals($this->projectId, $category['project_id']); - } - - public function assertGetAllCategories() - { - $categories = $this->app->getAllCategories($this->projectId); - - $this->assertCount(1, $categories); - $this->assertEquals($this->categoryId, $categories[0]['id']); - $this->assertEquals('Category', $categories[0]['name']); - $this->assertEquals($this->projectId, $categories[0]['project_id']); - } - - public function assertCategoryUpdate() - { - $this->assertTrue($this->app->execute('updateCategory', array( - 'id' => $this->categoryId, - 'name' => 'Renamed category', - ))); - - $category = $this->app->getCategory($this->categoryId); - $this->assertEquals('Renamed category', $category['name']); - } - - public function assertRemoveCategory() - { - $this->assertTrue($this->app->removeCategory($this->categoryId)); - $this->assertFalse($this->app->removeCategory($this->categoryId)); - $this->assertFalse($this->app->removeCategory(1111)); - } -} diff --git a/tests/integration/ColumnProcedureTest.php b/tests/integration/ColumnProcedureTest.php new file mode 100644 index 00000000..fb6a27c3 --- /dev/null +++ b/tests/integration/ColumnProcedureTest.php @@ -0,0 +1,69 @@ +assertCreateTeamProject(); + $this->assertGetColumns(); + $this->assertUpdateColumn(); + $this->assertAddColumn(); + $this->assertRemoveColumn(); + $this->assertChangeColumnPosition(); + } + + public function assertGetColumns() + { + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(4, $this->columns); + $this->assertEquals('Done', $this->columns[3]['title']); + } + + public function assertUpdateColumn() + { + $this->assertTrue($this->app->updateColumn($this->columns[3]['id'], 'Another column', 2)); + + $this->columns = $this->app->getColumns($this->projectId); + $this->assertEquals('Another column', $this->columns[3]['title']); + $this->assertEquals(2, $this->columns[3]['task_limit']); + } + + public function assertAddColumn() + { + $column_id = $this->app->addColumn($this->projectId, 'New column'); + $this->assertNotFalse($column_id); + $this->assertTrue($column_id > 0); + + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(5, $this->columns); + $this->assertEquals('New column', $this->columns[4]['title']); + } + + public function assertRemoveColumn() + { + $this->assertTrue($this->app->removeColumn($this->columns[3]['id'])); + + $this->columns = $this->app->getColumns($this->projectId); + $this->assertCount(4, $this->columns); + } + + public function assertChangeColumnPosition() + { + $this->assertTrue($this->app->changeColumnPosition($this->projectId, $this->columns[0]['id'], 3)); + + $this->columns = $this->app->getColumns($this->projectId); + $this->assertEquals('Ready', $this->columns[0]['title']); + $this->assertEquals(1, $this->columns[0]['position']); + $this->assertEquals('Work in progress', $this->columns[1]['title']); + $this->assertEquals(2, $this->columns[1]['position']); + $this->assertEquals('Backlog', $this->columns[2]['title']); + $this->assertEquals(3, $this->columns[2]['position']); + $this->assertEquals('New column', $this->columns[3]['title']); + $this->assertEquals(4, $this->columns[3]['position']); + } +} diff --git a/tests/integration/ColumnTest.php b/tests/integration/ColumnTest.php deleted file mode 100644 index 5a81badc..00000000 --- a/tests/integration/ColumnTest.php +++ /dev/null @@ -1,69 +0,0 @@ -assertCreateTeamProject(); - $this->assertGetColumns(); - $this->assertUpdateColumn(); - $this->assertAddColumn(); - $this->assertRemoveColumn(); - $this->assertChangeColumnPosition(); - } - - public function assertGetColumns() - { - $this->columns = $this->app->getColumns($this->projectId); - $this->assertCount(4, $this->columns); - $this->assertEquals('Done', $this->columns[3]['title']); - } - - public function assertUpdateColumn() - { - $this->assertTrue($this->app->updateColumn($this->columns[3]['id'], 'Another column', 2)); - - $this->columns = $this->app->getColumns($this->projectId); - $this->assertEquals('Another column', $this->columns[3]['title']); - $this->assertEquals(2, $this->columns[3]['task_limit']); - } - - public function assertAddColumn() - { - $column_id = $this->app->addColumn($this->projectId, 'New column'); - $this->assertNotFalse($column_id); - $this->assertTrue($column_id > 0); - - $this->columns = $this->app->getColumns($this->projectId); - $this->assertCount(5, $this->columns); - $this->assertEquals('New column', $this->columns[4]['title']); - } - - public function assertRemoveColumn() - { - $this->assertTrue($this->app->removeColumn($this->columns[3]['id'])); - - $this->columns = $this->app->getColumns($this->projectId); - $this->assertCount(4, $this->columns); - } - - public function assertChangeColumnPosition() - { - $this->assertTrue($this->app->changeColumnPosition($this->projectId, $this->columns[0]['id'], 3)); - - $this->columns = $this->app->getColumns($this->projectId); - $this->assertEquals('Ready', $this->columns[0]['title']); - $this->assertEquals(1, $this->columns[0]['position']); - $this->assertEquals('Work in progress', $this->columns[1]['title']); - $this->assertEquals(2, $this->columns[1]['position']); - $this->assertEquals('Backlog', $this->columns[2]['title']); - $this->assertEquals(3, $this->columns[2]['position']); - $this->assertEquals('New column', $this->columns[3]['title']); - $this->assertEquals(4, $this->columns[3]['position']); - } -} diff --git a/tests/integration/CommentProcedureTest.php b/tests/integration/CommentProcedureTest.php new file mode 100644 index 00000000..881d938c --- /dev/null +++ b/tests/integration/CommentProcedureTest.php @@ -0,0 +1,63 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateComment(); + $this->assertUpdateComment(); + $this->assertGetAllComments(); + $this->assertRemoveComment(); + } + + public function assertCreateComment() + { + $this->commentId = $this->app->execute('createComment', array( + 'task_id' => $this->taskId, + 'user_id' => 1, + 'content' => 'foobar', + )); + + $this->assertNotFalse($this->commentId); + } + + public function assertGetComment() + { + $comment = $this->app->getComment($this->commentId); + $this->assertNotFalse($comment); + $this->assertNotEmpty($comment); + $this->assertEquals(1, $comment['user_id']); + $this->assertEquals('foobar', $comment['comment']); + } + + public function assertUpdateComment() + { + $this->assertTrue($this->app->execute('updateComment', array( + 'id' => $this->commentId, + 'content' => 'test', + ))); + + $comment = $this->app->getComment($this->commentId); + $this->assertEquals('test', $comment['comment']); + } + + public function assertGetAllComments() + { + $comments = $this->app->getAllComments($this->taskId); + $this->assertCount(1, $comments); + $this->assertEquals('test', $comments[0]['comment']); + } + + public function assertRemoveComment() + { + $this->assertTrue($this->app->removeComment($this->commentId)); + $this->assertFalse($this->app->removeComment($this->commentId)); + } +} diff --git a/tests/integration/CommentTest.php b/tests/integration/CommentTest.php deleted file mode 100644 index 34376838..00000000 --- a/tests/integration/CommentTest.php +++ /dev/null @@ -1,63 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateTask(); - $this->assertCreateComment(); - $this->assertUpdateComment(); - $this->assertGetAllComments(); - $this->assertRemoveComment(); - } - - public function assertCreateComment() - { - $this->commentId = $this->app->execute('createComment', array( - 'task_id' => $this->taskId, - 'user_id' => 1, - 'content' => 'foobar', - )); - - $this->assertNotFalse($this->commentId); - } - - public function assertGetComment() - { - $comment = $this->app->getComment($this->commentId); - $this->assertNotFalse($comment); - $this->assertNotEmpty($comment); - $this->assertEquals(1, $comment['user_id']); - $this->assertEquals('foobar', $comment['comment']); - } - - public function assertUpdateComment() - { - $this->assertTrue($this->app->execute('updateComment', array( - 'id' => $this->commentId, - 'content' => 'test', - ))); - - $comment = $this->app->getComment($this->commentId); - $this->assertEquals('test', $comment['comment']); - } - - public function assertGetAllComments() - { - $comments = $this->app->getAllComments($this->taskId); - $this->assertCount(1, $comments); - $this->assertEquals('test', $comments[0]['comment']); - } - - public function assertRemoveComment() - { - $this->assertTrue($this->app->removeComment($this->commentId)); - $this->assertFalse($this->app->removeComment($this->commentId)); - } -} diff --git a/tests/integration/GroupMemberProcedureTest.php b/tests/integration/GroupMemberProcedureTest.php new file mode 100644 index 00000000..fe243533 --- /dev/null +++ b/tests/integration/GroupMemberProcedureTest.php @@ -0,0 +1,53 @@ +assertCreateGroups(); + $this->assertCreateUser(); + $this->assertAddMember(); + $this->assertGetMembers(); + $this->assertIsGroupMember(); + $this->assertGetGroups(); + $this->assertRemove(); + } + + public function assertAddMember() + { + $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); + } + + public function assertGetMembers() + { + $members = $this->app->getGroupMembers($this->groupId1); + $this->assertCount(1, $members); + $this->assertEquals($this->username, $members[0]['username']); + } + + public function assertIsGroupMember() + { + $this->assertTrue($this->app->isGroupMember($this->groupId1, $this->userId)); + $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->adminUserId)); + } + + public function assertGetGroups() + { + $groups = $this->app->getMemberGroups($this->userId); + $this->assertCount(1, $groups); + $this->assertEquals($this->groupId1, $groups[0]['id']); + $this->assertEquals($this->groupName1, $groups[0]['name']); + } + + public function assertRemove() + { + $this->assertTrue($this->app->removeGroupMember($this->groupId1, $this->userId)); + $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->userId)); + } +} diff --git a/tests/integration/GroupMemberTest.php b/tests/integration/GroupMemberTest.php deleted file mode 100644 index f79499a4..00000000 --- a/tests/integration/GroupMemberTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertCreateGroups(); - $this->assertCreateUser(); - $this->assertAddMember(); - $this->assertGetMembers(); - $this->assertIsGroupMember(); - $this->assertGetGroups(); - $this->assertRemove(); - } - - public function assertAddMember() - { - $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); - } - - public function assertGetMembers() - { - $members = $this->app->getGroupMembers($this->groupId1); - $this->assertCount(1, $members); - $this->assertEquals($this->username, $members[0]['username']); - } - - public function assertIsGroupMember() - { - $this->assertTrue($this->app->isGroupMember($this->groupId1, $this->userId)); - $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->adminUserId)); - } - - public function assertGetGroups() - { - $groups = $this->app->getMemberGroups($this->userId); - $this->assertCount(1, $groups); - $this->assertEquals($this->groupId1, $groups[0]['id']); - $this->assertEquals($this->groupName1, $groups[0]['name']); - } - - public function assertRemove() - { - $this->assertTrue($this->app->removeGroupMember($this->groupId1, $this->userId)); - $this->assertFalse($this->app->isGroupMember($this->groupId1, $this->userId)); - } -} diff --git a/tests/integration/GroupProcedureTest.php b/tests/integration/GroupProcedureTest.php new file mode 100644 index 00000000..610c121d --- /dev/null +++ b/tests/integration/GroupProcedureTest.php @@ -0,0 +1,50 @@ +assertCreateGroups(); + $this->assertGetAllGroups(); + $this->assertGetGroup(); + $this->assertUpdateGroup(); + $this->assertRemove(); + } + + public function assertGetAllGroups() + { + $groups = $this->app->getAllGroups(); + $this->assertNotEmpty($groups); + $this->assertArrayHasKey('name', $groups[0]); + $this->assertArrayHasKey('external_id', $groups[0]); + } + + public function assertGetGroup() + { + $group = $this->app->getGroup($this->groupId1); + $this->assertNotEmpty($group); + $this->assertEquals($this->groupName1, $group['name']); + $this->assertEquals('', $group['external_id']); + } + + public function assertUpdateGroup() + { + $this->assertTrue($this->app->updateGroup(array( + 'group_id' => $this->groupId2, + 'name' => 'My Group C', + 'external_id' => 'something else', + ))); + + $group = $this->app->getGroup($this->groupId2); + $this->assertNotEmpty($group); + $this->assertEquals('My Group C', $group['name']); + $this->assertEquals('something else', $group['external_id']); + } + + public function assertRemove() + { + $this->assertTrue($this->app->removeGroup($this->groupId1)); + } +} diff --git a/tests/integration/GroupTest.php b/tests/integration/GroupTest.php deleted file mode 100644 index ffcd7a71..00000000 --- a/tests/integration/GroupTest.php +++ /dev/null @@ -1,50 +0,0 @@ -assertCreateGroups(); - $this->assertGetAllGroups(); - $this->assertGetGroup(); - $this->assertUpdateGroup(); - $this->assertRemove(); - } - - public function assertGetAllGroups() - { - $groups = $this->app->getAllGroups(); - $this->assertNotEmpty($groups); - $this->assertArrayHasKey('name', $groups[0]); - $this->assertArrayHasKey('external_id', $groups[0]); - } - - public function assertGetGroup() - { - $group = $this->app->getGroup($this->groupId1); - $this->assertNotEmpty($group); - $this->assertEquals($this->groupName1, $group['name']); - $this->assertEquals('', $group['external_id']); - } - - public function assertUpdateGroup() - { - $this->assertTrue($this->app->updateGroup(array( - 'group_id' => $this->groupId2, - 'name' => 'My Group C', - 'external_id' => 'something else', - ))); - - $group = $this->app->getGroup($this->groupId2); - $this->assertNotEmpty($group); - $this->assertEquals('My Group C', $group['name']); - $this->assertEquals('something else', $group['external_id']); - } - - public function assertRemove() - { - $this->assertTrue($this->app->removeGroup($this->groupId1)); - } -} diff --git a/tests/integration/LinkProcedureTest.php b/tests/integration/LinkProcedureTest.php new file mode 100644 index 00000000..fb07e694 --- /dev/null +++ b/tests/integration/LinkProcedureTest.php @@ -0,0 +1,70 @@ +app->getAllLinks(); + $this->assertNotEmpty($links); + $this->assertArrayHasKey('id', $links[0]); + $this->assertArrayHasKey('label', $links[0]); + $this->assertArrayHasKey('opposite_id', $links[0]); + } + + public function testGetOppositeLink() + { + $link = $this->app->getOppositeLinkId(1); + $this->assertEquals(1, $link); + + $link = $this->app->getOppositeLinkId(2); + $this->assertEquals(3, $link); + } + + public function testGetLinkByLabel() + { + $link = $this->app->getLinkByLabel('blocks'); + $this->assertNotEmpty($link); + $this->assertEquals(2, $link['id']); + $this->assertEquals(3, $link['opposite_id']); + } + + public function testGetLinkById() + { + $link = $this->app->getLinkById(4); + $this->assertNotEmpty($link); + $this->assertEquals(4, $link['id']); + $this->assertEquals(5, $link['opposite_id']); + $this->assertEquals('duplicates', $link['label']); + } + + public function testCreateLink() + { + $link_id = $this->app->createLink(array('label' => 'test')); + $this->assertNotFalse($link_id); + $this->assertInternalType('int', $link_id); + + $link_id = $this->app->createLink(array('label' => 'foo', 'opposite_label' => 'bar')); + $this->assertNotFalse($link_id); + $this->assertInternalType('int', $link_id); + } + + public function testUpdateLink() + { + $link1 = $this->app->getLinkByLabel('bar'); + $this->assertNotEmpty($link1); + + $link2 = $this->app->getLinkByLabel('test'); + $this->assertNotEmpty($link2); + + $this->assertNotFalse($this->app->updateLink($link1['id'], $link2['id'], 'my link')); + + $link = $this->app->getLinkById($link1['id']); + $this->assertNotEmpty($link); + $this->assertEquals($link2['id'], $link['opposite_id']); + $this->assertEquals('my link', $link['label']); + + $this->assertTrue($this->app->removeLink($link1['id'])); + } +} diff --git a/tests/integration/LinkTest.php b/tests/integration/LinkTest.php deleted file mode 100644 index 16b16e50..00000000 --- a/tests/integration/LinkTest.php +++ /dev/null @@ -1,70 +0,0 @@ -app->getAllLinks(); - $this->assertNotEmpty($links); - $this->assertArrayHasKey('id', $links[0]); - $this->assertArrayHasKey('label', $links[0]); - $this->assertArrayHasKey('opposite_id', $links[0]); - } - - public function testGetOppositeLink() - { - $link = $this->app->getOppositeLinkId(1); - $this->assertEquals(1, $link); - - $link = $this->app->getOppositeLinkId(2); - $this->assertEquals(3, $link); - } - - public function testGetLinkByLabel() - { - $link = $this->app->getLinkByLabel('blocks'); - $this->assertNotEmpty($link); - $this->assertEquals(2, $link['id']); - $this->assertEquals(3, $link['opposite_id']); - } - - public function testGetLinkById() - { - $link = $this->app->getLinkById(4); - $this->assertNotEmpty($link); - $this->assertEquals(4, $link['id']); - $this->assertEquals(5, $link['opposite_id']); - $this->assertEquals('duplicates', $link['label']); - } - - public function testCreateLink() - { - $link_id = $this->app->createLink(array('label' => 'test')); - $this->assertNotFalse($link_id); - $this->assertInternalType('int', $link_id); - - $link_id = $this->app->createLink(array('label' => 'foo', 'opposite_label' => 'bar')); - $this->assertNotFalse($link_id); - $this->assertInternalType('int', $link_id); - } - - public function testUpdateLink() - { - $link1 = $this->app->getLinkByLabel('bar'); - $this->assertNotEmpty($link1); - - $link2 = $this->app->getLinkByLabel('test'); - $this->assertNotEmpty($link2); - - $this->assertNotFalse($this->app->updateLink($link1['id'], $link2['id'], 'my link')); - - $link = $this->app->getLinkById($link1['id']); - $this->assertNotEmpty($link); - $this->assertEquals($link2['id'], $link['opposite_id']); - $this->assertEquals('my link', $link['label']); - - $this->assertTrue($this->app->removeLink($link1['id'])); - } -} diff --git a/tests/integration/MeProcedureTest.php b/tests/integration/MeProcedureTest.php new file mode 100644 index 00000000..2106419c --- /dev/null +++ b/tests/integration/MeProcedureTest.php @@ -0,0 +1,68 @@ +assertGetMe(); + $this->assertCreateMyPrivateProject(); + $this->assertGetMyProjectsList(); + $this->assertGetMyProjects(); + $this->assertCreateTask(); + $this->assertGetMyDashboard(); + $this->assertGetMyActivityStream(); + } + + public function assertGetMe() + { + $profile = $this->user->getMe(); + $this->assertEquals('user', $profile['username']); + $this->assertEquals('app-user', $profile['role']); + } + + public function assertCreateMyPrivateProject() + { + $this->projectId = $this->user->createMyPrivateProject($this->projectName); + $this->assertNotFalse($this->projectId); + } + + public function assertGetMyProjectsList() + { + $projects = $this->user->getMyProjectsList(); + $this->assertNotEmpty($projects); + $this->assertEquals($this->projectName, $projects[$this->projectId]); + } + + public function assertGetMyProjects() + { + $projects = $this->user->getMyProjects(); + $this->assertNotEmpty($projects); + } + + public function assertCreateTask() + { + $taskId = $this->user->createTask(array('title' => 'My task', 'project_id' => $this->projectId, 'owner_id' => $this->userUserId)); + $this->assertNotFalse($taskId); + } + + public function assertGetMyDashboard() + { + $dashboard = $this->user->getMyDashboard(); + $this->assertNotEmpty($dashboard); + $this->assertArrayHasKey('projects', $dashboard); + $this->assertArrayHasKey('tasks', $dashboard); + $this->assertArrayHasKey('subtasks', $dashboard); + $this->assertNotEmpty($dashboard['projects']); + $this->assertNotEmpty($dashboard['tasks']); + } + + public function assertGetMyActivityStream() + { + $activity = $this->user->getMyActivityStream(); + $this->assertNotEmpty($activity); + } +} diff --git a/tests/integration/MeTest.php b/tests/integration/MeTest.php deleted file mode 100644 index 047ebf85..00000000 --- a/tests/integration/MeTest.php +++ /dev/null @@ -1,73 +0,0 @@ -assertGetMe(); - $this->assertCreateMyPrivateProject(); - $this->assertGetMyProjectsList(); - $this->assertGetMyProjects(); - $this->assertCreateTask(); - $this->assertGetMyDashboard(); - $this->assertGetMyActivityStream(); - } - - public function assertGetMe() - { - $profile = $this->user->getMe(); - $this->assertEquals('user', $profile['username']); - $this->assertEquals('app-user', $profile['role']); - } - - public function assertCreateMyPrivateProject() - { - $this->projectId = $this->user->createMyPrivateProject($this->projectName); - $this->assertNotFalse($this->projectId); - } - - public function assertGetMyProjectsList() - { - $projects = $this->user->getMyProjectsList(); - $this->assertNotEmpty($projects); - $this->assertEquals($this->projectName, $projects[$this->projectId]); - } - - public function assertGetMyProjects() - { - $projects = $this->user->getMyProjects(); - $this->assertNotEmpty($projects); - $this->assertCount(1, $projects); - $this->assertEquals($this->projectName, $projects[0]['name']); - $this->assertNotEmpty($projects[0]['url']['calendar']); - $this->assertNotEmpty($projects[0]['url']['board']); - $this->assertNotEmpty($projects[0]['url']['list']); - } - - public function assertCreateTask() - { - $taskId = $this->user->createTask(array('title' => 'My task', 'project_id' => $this->projectId, 'owner_id' => $this->userUserId)); - $this->assertNotFalse($taskId); - } - - public function assertGetMyDashboard() - { - $dashboard = $this->user->getMyDashboard(); - $this->assertNotEmpty($dashboard); - $this->assertArrayHasKey('projects', $dashboard); - $this->assertArrayHasKey('tasks', $dashboard); - $this->assertArrayHasKey('subtasks', $dashboard); - $this->assertNotEmpty($dashboard['projects']); - $this->assertNotEmpty($dashboard['tasks']); - } - - public function assertGetMyActivityStream() - { - $activity = $this->user->getMyActivityStream(); - $this->assertNotEmpty($activity); - } -} diff --git a/tests/integration/OverdueTaskProcedureTest.php b/tests/integration/OverdueTaskProcedureTest.php new file mode 100644 index 00000000..65f52301 --- /dev/null +++ b/tests/integration/OverdueTaskProcedureTest.php @@ -0,0 +1,43 @@ +assertCreateTeamProject(); + $this->assertCreateOverdueTask(); + $this->assertGetOverdueTasksByProject(); + $this->assertGetOverdueTasks(); + } + + public function assertCreateOverdueTask() + { + $this->assertNotFalse($this->app->createTask(array( + 'title' => 'overdue task', + 'project_id' => $this->projectId, + 'date_due' => date('Y-m-d', strtotime('-2days')), + ))); + } + + public function assertGetOverdueTasksByProject() + { + $tasks = $this->app->getOverdueTasksByProject($this->projectId); + $this->assertNotEmpty($tasks); + $this->assertCount(1, $tasks); + $this->assertEquals('overdue task', $tasks[0]['title']); + $this->assertEquals($this->projectName, $tasks[0]['project_name']); + } + + public function assertGetOverdueTasks() + { + $tasks = $this->app->getOverdueTasks(); + $this->assertNotEmpty($tasks); + $this->assertCount(1, $tasks); + $this->assertEquals('overdue task', $tasks[0]['title']); + $this->assertEquals($this->projectName, $tasks[0]['project_name']); + } +} diff --git a/tests/integration/OverdueTaskTest.php b/tests/integration/OverdueTaskTest.php deleted file mode 100644 index 1dea5030..00000000 --- a/tests/integration/OverdueTaskTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateOverdueTask(); - $this->assertGetOverdueTasksByProject(); - $this->assertGetOverdueTasks(); - } - - public function assertCreateOverdueTask() - { - $this->assertNotFalse($this->app->createTask(array( - 'title' => 'overdue task', - 'project_id' => $this->projectId, - 'date_due' => date('Y-m-d', strtotime('-2days')), - ))); - } - - public function assertGetOverdueTasksByProject() - { - $tasks = $this->app->getOverdueTasksByProject($this->projectId); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals($this->projectName, $tasks[0]['project_name']); - } - - public function assertGetOverdueTasks() - { - $tasks = $this->app->getOverdueTasks(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('overdue task', $tasks[0]['title']); - $this->assertEquals($this->projectName, $tasks[0]['project_name']); - } -} diff --git a/tests/integration/ProcedureAuthorizationTest.php b/tests/integration/ProcedureAuthorizationTest.php new file mode 100644 index 00000000..a63e9d8c --- /dev/null +++ b/tests/integration/ProcedureAuthorizationTest.php @@ -0,0 +1,306 @@ +setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->app->getMe(); + } + + public function testUserCredentialDoNotHaveAccessToAdminProcedures() + { + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->getUser(1); + } + + public function testManagerCredentialDoNotHaveAccessToAdminProcedures() + { + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->getAllProjects(); + } + + public function testUserCredentialDoNotHaveAccessToManagerProcedures() + { + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->createProject('Team project creation are only for app managers'); + } + + public function testAppManagerCanCreateTeamProject() + { + $this->assertNotFalse($this->manager->createProject('Team project created by app manager')); + } + + public function testAdminManagerCanCreateTeamProject() + { + $projectId = $this->admin->createProject('Team project created by admin'); + $this->assertNotFalse($projectId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->assertNotNull($this->manager->getProjectById($projectId)); + } + + public function testProjectManagerCanUpdateHisProject() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Team project can be updated', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + $this->assertEquals('project-manager', $this->app->getProjectUserRole($projectId, $this->managerUserId)); + $this->assertNotNull($this->manager->getProjectById($projectId)); + + $this->assertTrue($this->manager->updateProject($projectId, 'My team project have been updated')); + } + + public function testProjectAuthorizationForbidden() + { + $projectId = $this->manager->createProject('A team project without members'); + $this->assertNotFalse($projectId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->getProjectById($projectId); + } + + public function testProjectAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'A team project with members', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId)); + $this->assertNotNull($this->user->getProjectById($projectId)); + } + + public function testActionAuthorizationForbidden() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $actionId = $this->manager->createAction($projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertNotFalse($actionId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeAction($projectId); + } + + public function testActionAuthorizationForbiddenBecauseNotProjectManager() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $actionId = $this->manager->createAction($projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertNotFalse($actionId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-member')); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeAction($actionId); + } + + public function testActionAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $actionId = $this->manager->createAction($projectId, 'task.move.column', '\Kanboard\Action\TaskCloseColumn', array('column_id' => 1)); + $this->assertNotFalse($actionId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-manager')); + $this->assertTrue($this->user->removeAction($actionId)); + } + + public function testCategoryAuthorizationForbidden() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $categoryId = $this->manager->createCategory($projectId, 'Test'); + $this->assertNotFalse($categoryId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeCategory($categoryId); + } + + public function testCategoryAuthorizationForbiddenBecauseNotProjectManager() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $categoryId = $this->manager->createCategory($projectId, 'Test'); + $this->assertNotFalse($categoryId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-member')); + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeCategory($categoryId); + } + + public function testCategoryAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $categoryId = $this->manager->createCategory($projectId, 'Test'); + $this->assertNotFalse($categoryId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-manager')); + $this->assertTrue($this->user->removeCategory($categoryId)); + } + + public function testColumnAuthorizationForbidden() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $columnId = $this->manager->addColumn($projectId, 'Test'); + $this->assertNotFalse($columnId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeColumn($columnId); + } + + public function testColumnAuthorizationForbiddenBecauseNotProjectManager() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $columnId = $this->manager->addColumn($projectId, 'Test'); + $this->assertNotFalse($columnId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-member')); + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeColumn($columnId); + } + + public function testColumnAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + + $columnId = $this->manager->addColumn($projectId, 'Test'); + $this->assertNotFalse($columnId); + + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-manager')); + $this->assertTrue($this->user->removeColumn($columnId)); + } + + public function testCommentAuthorizationForbidden() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-viewer')); + + $taskId = $this->manager->createTask('My Task', $projectId); + $this->assertNotFalse($taskId); + + $commentId = $this->manager->createComment($taskId, $this->userUserId, 'My comment'); + $this->assertNotFalse($commentId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->updateComment($commentId, 'something else'); + } + + public function testCommentAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-member')); + + $taskId = $this->user->createTask('My Task', $projectId); + $this->assertNotFalse($taskId); + + $commentId = $this->user->createComment($taskId, $this->userUserId, 'My comment'); + $this->assertNotFalse($commentId); + + $this->assertTrue($this->user->updateComment($commentId, 'something else')); + } + + public function testSubtaskAuthorizationForbidden() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-viewer')); + + $taskId = $this->manager->createTask('My Task', $projectId); + $this->assertNotFalse($taskId); + + $subtaskId = $this->manager->createSubtask($taskId, 'My subtask'); + $this->assertNotFalse($subtaskId); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->user->removeSubtask($subtaskId); + } + + public function testSubtaskAuthorizationGranted() + { + $projectId = $this->manager->createProject(array( + 'name' => 'Test Project', + 'owner_id' => $this->managerUserId, + )); + + $this->assertNotFalse($projectId); + $this->assertTrue($this->manager->addProjectUser($projectId, $this->userUserId, 'project-member')); + + $taskId = $this->user->createTask('My Task', $projectId); + $this->assertNotFalse($taskId); + + $subtaskId = $this->manager->createSubtask($taskId, 'My subtask'); + $this->assertNotFalse($subtaskId); + + $this->assertTrue($this->user->removeSubtask($subtaskId)); + } +} diff --git a/tests/integration/ProjectPermissionProcedureTest.php b/tests/integration/ProjectPermissionProcedureTest.php new file mode 100644 index 00000000..74313dc4 --- /dev/null +++ b/tests/integration/ProjectPermissionProcedureTest.php @@ -0,0 +1,89 @@ +assertCreateTeamProject(); + $this->assertCreateGroups(); + $this->assertCreateUser(); + + $this->assertAddProjectUser(); + $this->assertGetProjectUsers(); + $this->assertGetAssignableUsers(); + $this->assertChangeProjectUserRole(); + $this->assertRemoveProjectUser(); + + $this->assertAddProjectGroup(); + $this->assertGetProjectUsers(); + $this->assertGetAssignableUsers(); + $this->assertChangeProjectGroupRole(); + $this->assertRemoveProjectGroup(); + } + + public function assertAddProjectUser() + { + $this->assertTrue($this->app->addProjectUser($this->projectId, $this->userId)); + } + + public function assertGetProjectUsers() + { + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(1, $members); + $this->assertArrayHasKey($this->userId, $members); + $this->assertEquals($this->username, $members[$this->userId]); + } + + public function assertGetAssignableUsers() + { + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(1, $members); + $this->assertArrayHasKey($this->userId, $members); + $this->assertEquals($this->username, $members[$this->userId]); + } + + public function assertChangeProjectUserRole() + { + $this->assertTrue($this->app->changeProjectUserRole($this->projectId, $this->userId, 'project-viewer')); + + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(0, $members); + } + + public function assertRemoveProjectUser() + { + $this->assertTrue($this->app->removeProjectUser($this->projectId, $this->userId)); + + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(0, $members); + } + + public function assertAddProjectGroup() + { + $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); + $this->assertTrue($this->app->addProjectGroup($this->projectId, $this->groupId1)); + } + + public function assertChangeProjectGroupRole() + { + $this->assertTrue($this->app->changeProjectGroupRole($this->projectId, $this->groupId1, 'project-viewer')); + + $members = $this->app->getAssignableUsers($this->projectId); + $this->assertCount(0, $members); + } + + public function assertRemoveProjectGroup() + { + $this->assertTrue($this->app->removeProjectGroup($this->projectId, $this->groupId1)); + + $members = $this->app->getProjectUsers($this->projectId); + $this->assertCount(0, $members); + } +} diff --git a/tests/integration/ProjectPermissionTest.php b/tests/integration/ProjectPermissionTest.php deleted file mode 100644 index 3ceda07d..00000000 --- a/tests/integration/ProjectPermissionTest.php +++ /dev/null @@ -1,89 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateGroups(); - $this->assertCreateUser(); - - $this->assertAddProjectUser(); - $this->assertGetProjectUsers(); - $this->assertGetAssignableUsers(); - $this->assertChangeProjectUserRole(); - $this->assertRemoveProjectUser(); - - $this->assertAddProjectGroup(); - $this->assertGetProjectUsers(); - $this->assertGetAssignableUsers(); - $this->assertChangeProjectGroupRole(); - $this->assertRemoveProjectGroup(); - } - - public function assertAddProjectUser() - { - $this->assertTrue($this->app->addProjectUser($this->projectId, $this->userId)); - } - - public function assertGetProjectUsers() - { - $members = $this->app->getProjectUsers($this->projectId); - $this->assertCount(1, $members); - $this->assertArrayHasKey($this->userId, $members); - $this->assertEquals($this->username, $members[$this->userId]); - } - - public function assertGetAssignableUsers() - { - $members = $this->app->getAssignableUsers($this->projectId); - $this->assertCount(1, $members); - $this->assertArrayHasKey($this->userId, $members); - $this->assertEquals($this->username, $members[$this->userId]); - } - - public function assertChangeProjectUserRole() - { - $this->assertTrue($this->app->changeProjectUserRole($this->projectId, $this->userId, 'project-viewer')); - - $members = $this->app->getAssignableUsers($this->projectId); - $this->assertCount(0, $members); - } - - public function assertRemoveProjectUser() - { - $this->assertTrue($this->app->removeProjectUser($this->projectId, $this->userId)); - - $members = $this->app->getProjectUsers($this->projectId); - $this->assertCount(0, $members); - } - - public function assertAddProjectGroup() - { - $this->assertTrue($this->app->addGroupMember($this->groupId1, $this->userId)); - $this->assertTrue($this->app->addProjectGroup($this->projectId, $this->groupId1)); - } - - public function assertChangeProjectGroupRole() - { - $this->assertTrue($this->app->changeProjectGroupRole($this->projectId, $this->groupId1, 'project-viewer')); - - $members = $this->app->getAssignableUsers($this->projectId); - $this->assertCount(0, $members); - } - - public function assertRemoveProjectGroup() - { - $this->assertTrue($this->app->removeProjectGroup($this->projectId, $this->groupId1)); - - $members = $this->app->getProjectUsers($this->projectId); - $this->assertCount(0, $members); - } -} diff --git a/tests/integration/ProjectProcedureTest.php b/tests/integration/ProjectProcedureTest.php new file mode 100644 index 00000000..1ebd48ae --- /dev/null +++ b/tests/integration/ProjectProcedureTest.php @@ -0,0 +1,89 @@ +assertCreateTeamProject(); + $this->assertGetProjectById(); + $this->assertGetProjectByName(); + $this->assertGetAllProjects(); + $this->assertUpdateProject(); + $this->assertGetProjectActivity(); + $this->assertGetProjectsActivity(); + $this->assertEnableDisableProject(); + $this->assertEnableDisablePublicAccess(); + $this->assertRemoveProject(); + } + + public function assertGetProjectById() + { + $project = $this->app->getProjectById($this->projectId); + $this->assertNotNull($project); + $this->assertEquals($this->projectName, $project['name']); + $this->assertEquals('Description', $project['description']); + } + + public function assertGetProjectByName() + { + $project = $this->app->getProjectByName($this->projectName); + $this->assertNotNull($project); + $this->assertEquals($this->projectId, $project['id']); + $this->assertEquals($this->projectName, $project['name']); + $this->assertEquals('Description', $project['description']); + } + + public function assertGetAllProjects() + { + $projects = $this->app->getAllProjects(); + $this->assertNotEmpty($projects); + } + + public function assertGetProjectActivity() + { + $activities = $this->app->getProjectActivity($this->projectId); + $this->assertInternalType('array', $activities); + $this->assertCount(0, $activities); + } + + public function assertGetProjectsActivity() + { + $activities = $this->app->getProjectActivities(array('project_ids' => array($this->projectId))); + $this->assertInternalType('array', $activities); + $this->assertCount(0, $activities); + } + + public function assertUpdateProject() + { + $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => 'test', 'description' => 'test'))); + + $project = $this->app->getProjectById($this->projectId); + $this->assertNotNull($project); + $this->assertEquals('test', $project['name']); + $this->assertEquals('test', $project['description']); + + $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => $this->projectName))); + } + + public function assertEnableDisableProject() + { + $this->assertTrue($this->app->disableProject($this->projectId)); + $this->assertTrue($this->app->enableProject($this->projectId)); + } + + public function assertEnableDisablePublicAccess() + { + $this->assertTrue($this->app->disableProjectPublicAccess($this->projectId)); + $this->assertTrue($this->app->enableProjectPublicAccess($this->projectId)); + } + + public function assertRemoveProject() + { + $this->assertTrue($this->app->removeProject($this->projectId)); + $this->assertNull($this->app->getProjectById($this->projectId)); + } +} diff --git a/tests/integration/ProjectTest.php b/tests/integration/ProjectTest.php deleted file mode 100644 index 50d4fc53..00000000 --- a/tests/integration/ProjectTest.php +++ /dev/null @@ -1,89 +0,0 @@ -assertCreateTeamProject(); - $this->assertGetProjectById(); - $this->assertGetProjectByName(); - $this->assertGetAllProjects(); - $this->assertUpdateProject(); - $this->assertGetProjectActivity(); - $this->assertGetProjectsActivity(); - $this->assertEnableDisableProject(); - $this->assertEnableDisablePublicAccess(); - $this->assertRemoveProject(); - } - - public function assertGetProjectById() - { - $project = $this->app->getProjectById($this->projectId); - $this->assertNotNull($project); - $this->assertEquals($this->projectName, $project['name']); - $this->assertEquals('Description', $project['description']); - } - - public function assertGetProjectByName() - { - $project = $this->app->getProjectByName($this->projectName); - $this->assertNotNull($project); - $this->assertEquals($this->projectId, $project['id']); - $this->assertEquals($this->projectName, $project['name']); - $this->assertEquals('Description', $project['description']); - } - - public function assertGetAllProjects() - { - $projects = $this->app->getAllProjects(); - $this->assertNotEmpty($projects); - } - - public function assertGetProjectActivity() - { - $activities = $this->app->getProjectActivity($this->projectId); - $this->assertInternalType('array', $activities); - $this->assertCount(0, $activities); - } - - public function assertGetProjectsActivity() - { - $activities = $this->app->getProjectActivities(array('project_ids' => array($this->projectId))); - $this->assertInternalType('array', $activities); - $this->assertCount(0, $activities); - } - - public function assertUpdateProject() - { - $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => 'test', 'description' => 'test'))); - - $project = $this->app->getProjectById($this->projectId); - $this->assertNotNull($project); - $this->assertEquals('test', $project['name']); - $this->assertEquals('test', $project['description']); - - $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => $this->projectName))); - } - - public function assertEnableDisableProject() - { - $this->assertTrue($this->app->disableProject($this->projectId)); - $this->assertTrue($this->app->enableProject($this->projectId)); - } - - public function assertEnableDisablePublicAccess() - { - $this->assertTrue($this->app->disableProjectPublicAccess($this->projectId)); - $this->assertTrue($this->app->enableProjectPublicAccess($this->projectId)); - } - - public function assertRemoveProject() - { - $this->assertTrue($this->app->removeProject($this->projectId)); - $this->assertNull($this->app->getProjectById($this->projectId)); - } -} diff --git a/tests/integration/SubtaskProcedureTest.php b/tests/integration/SubtaskProcedureTest.php new file mode 100644 index 00000000..7ab4ef0b --- /dev/null +++ b/tests/integration/SubtaskProcedureTest.php @@ -0,0 +1,64 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateSubtask(); + $this->assertGetSubtask(); + $this->assertUpdateSubtask(); + $this->assertGetAllSubtasks(); + $this->assertRemoveSubtask(); + } + + public function assertCreateSubtask() + { + $this->subtaskId = $this->app->createSubtask(array( + 'task_id' => $this->taskId, + 'title' => 'subtask #1', + )); + + $this->assertNotFalse($this->subtaskId); + } + + public function assertGetSubtask() + { + $subtask = $this->app->getSubtask($this->subtaskId); + $this->assertEquals($this->taskId, $subtask['task_id']); + $this->assertEquals('subtask #1', $subtask['title']); + } + + public function assertUpdateSubtask() + { + $this->assertTrue($this->app->execute('updateSubtask', array( + 'id' => $this->subtaskId, + 'task_id' => $this->taskId, + 'title' => 'test', + ))); + + $subtask = $this->app->getSubtask($this->subtaskId); + $this->assertEquals('test', $subtask['title']); + } + + public function assertGetAllSubtasks() + { + $subtasks = $this->app->getAllSubtasks($this->taskId); + $this->assertCount(1, $subtasks); + $this->assertEquals('test', $subtasks[0]['title']); + } + + public function assertRemoveSubtask() + { + $this->assertTrue($this->app->removeSubtask($this->subtaskId)); + + $subtasks = $this->app->getAllSubtasks($this->taskId); + $this->assertCount(0, $subtasks); + } +} diff --git a/tests/integration/SubtaskTest.php b/tests/integration/SubtaskTest.php deleted file mode 100644 index 10082e60..00000000 --- a/tests/integration/SubtaskTest.php +++ /dev/null @@ -1,64 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateTask(); - $this->assertCreateSubtask(); - $this->assertGetSubtask(); - $this->assertUpdateSubtask(); - $this->assertGetAllSubtasks(); - $this->assertRemoveSubtask(); - } - - public function assertCreateSubtask() - { - $this->subtaskId = $this->app->createSubtask(array( - 'task_id' => $this->taskId, - 'title' => 'subtask #1', - )); - - $this->assertNotFalse($this->subtaskId); - } - - public function assertGetSubtask() - { - $subtask = $this->app->getSubtask($this->subtaskId); - $this->assertEquals($this->taskId, $subtask['task_id']); - $this->assertEquals('subtask #1', $subtask['title']); - } - - public function assertUpdateSubtask() - { - $this->assertTrue($this->app->execute('updateSubtask', array( - 'id' => $this->subtaskId, - 'task_id' => $this->taskId, - 'title' => 'test', - ))); - - $subtask = $this->app->getSubtask($this->subtaskId); - $this->assertEquals('test', $subtask['title']); - } - - public function assertGetAllSubtasks() - { - $subtasks = $this->app->getAllSubtasks($this->taskId); - $this->assertCount(1, $subtasks); - $this->assertEquals('test', $subtasks[0]['title']); - } - - public function assertRemoveSubtask() - { - $this->assertTrue($this->app->removeSubtask($this->subtaskId)); - - $subtasks = $this->app->getAllSubtasks($this->taskId); - $this->assertCount(0, $subtasks); - } -} diff --git a/tests/integration/SwimlaneProcedureTest.php b/tests/integration/SwimlaneProcedureTest.php new file mode 100644 index 00000000..e64342b4 --- /dev/null +++ b/tests/integration/SwimlaneProcedureTest.php @@ -0,0 +1,93 @@ +assertCreateTeamProject(); + } + + public function assertGetDefaultSwimlane() + { + $swimlane = $this->app->getDefaultSwimlane($this->projectId); + $this->assertNotEmpty($swimlane); + $this->assertEquals('Default swimlane', $swimlane['default_swimlane']); + } + + public function assertAddSwimlane() + { + $this->swimlaneId = $this->app->addSwimlane($this->projectId, 'Swimlane 1'); + $this->assertNotFalse($this->swimlaneId); + $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane 2')); + } + + public function assertGetSwimlane() + { + $swimlane = $this->app->getSwimlane($this->swimlaneId); + $this->assertInternalType('array', $swimlane); + $this->assertEquals('Swimlane 1', $swimlane['name']); + } + + public function assertUpdateSwimlane() + { + $this->assertTrue($this->app->updateSwimlane($this->swimlaneId, 'Another swimlane')); + + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); + $this->assertEquals('Another swimlane', $swimlane['name']); + } + + public function assertDisableSwimlane() + { + $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); + + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); + $this->assertEquals(0, $swimlane['is_active']); + } + + public function assertEnableSwimlane() + { + $this->assertTrue($this->app->enableSwimlane($this->projectId, $this->swimlaneId)); + + $swimlane = $this->app->getSwimlaneById($this->swimlaneId); + $this->assertEquals(1, $swimlane['is_active']); + } + + public function assertGetAllSwimlanes() + { + $swimlanes = $this->app->getAllSwimlanes($this->projectId); + $this->assertCount(2, $swimlanes); + $this->assertEquals('Another swimlane', $swimlanes[0]['name']); + $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); + } + + public function assertGetActiveSwimlane() + { + $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); + + $swimlanes = $this->app->getActiveSwimlanes($this->projectId); + $this->assertCount(2, $swimlanes); + $this->assertEquals('Default swimlane', $swimlanes[0]['name']); + $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); + } + + public function assertRemoveSwimlane() + { + $this->assertTrue($this->app->removeSwimlane($this->projectId, $this->swimlaneId)); + } + + public function assertChangePosition() + { + $swimlaneId1 = $this->app->addSwimlane($this->projectId, 'Swimlane A'); + $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane B')); + + $swimlanes = $this->app->getAllSwimlanes($this->projectId); + $this->assertCount(3, $swimlanes); + + $this->assertTrue($this->app->changeSwimlanePosition($this->projectId, $swimlaneId1, 3)); + } +} diff --git a/tests/integration/SwimlaneTest.php b/tests/integration/SwimlaneTest.php deleted file mode 100644 index 4f703414..00000000 --- a/tests/integration/SwimlaneTest.php +++ /dev/null @@ -1,93 +0,0 @@ -assertCreateTeamProject(); - } - - public function assertGetDefaultSwimlane() - { - $swimlane = $this->app->getDefaultSwimlane($this->projectId); - $this->assertNotEmpty($swimlane); - $this->assertEquals('Default swimlane', $swimlane['default_swimlane']); - } - - public function assertAddSwimlane() - { - $this->swimlaneId = $this->app->addSwimlane($this->projectId, 'Swimlane 1'); - $this->assertNotFalse($this->swimlaneId); - $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane 2')); - } - - public function assertGetSwimlane() - { - $swimlane = $this->app->getSwimlane($this->swimlaneId); - $this->assertInternalType('array', $swimlane); - $this->assertEquals('Swimlane 1', $swimlane['name']); - } - - public function assertUpdateSwimlane() - { - $this->assertTrue($this->app->updateSwimlane($this->swimlaneId, 'Another swimlane')); - - $swimlane = $this->app->getSwimlaneById($this->swimlaneId); - $this->assertEquals('Another swimlane', $swimlane['name']); - } - - public function assertDisableSwimlane() - { - $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); - - $swimlane = $this->app->getSwimlaneById($this->swimlaneId); - $this->assertEquals(0, $swimlane['is_active']); - } - - public function assertEnableSwimlane() - { - $this->assertTrue($this->app->enableSwimlane($this->projectId, $this->swimlaneId)); - - $swimlane = $this->app->getSwimlaneById($this->swimlaneId); - $this->assertEquals(1, $swimlane['is_active']); - } - - public function assertGetAllSwimlanes() - { - $swimlanes = $this->app->getAllSwimlanes($this->projectId); - $this->assertCount(2, $swimlanes); - $this->assertEquals('Another swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); - } - - public function assertGetActiveSwimlane() - { - $this->assertTrue($this->app->disableSwimlane($this->projectId, $this->swimlaneId)); - - $swimlanes = $this->app->getActiveSwimlanes($this->projectId); - $this->assertCount(2, $swimlanes); - $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane 2', $swimlanes[1]['name']); - } - - public function assertRemoveSwimlane() - { - $this->assertTrue($this->app->removeSwimlane($this->projectId, $this->swimlaneId)); - } - - public function assertChangePosition() - { - $swimlaneId1 = $this->app->addSwimlane($this->projectId, 'Swimlane A'); - $this->assertNotFalse($this->app->addSwimlane($this->projectId, 'Swimlane B')); - - $swimlanes = $this->app->getAllSwimlanes($this->projectId); - $this->assertCount(3, $swimlanes); - - $this->assertTrue($this->app->changeSwimlanePosition($this->projectId, $swimlaneId1, 3)); - } -} diff --git a/tests/integration/TaskFileProcedureTest.php b/tests/integration/TaskFileProcedureTest.php new file mode 100644 index 00000000..61155555 --- /dev/null +++ b/tests/integration/TaskFileProcedureTest.php @@ -0,0 +1,67 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateTaskFile(); + $this->assertGetTaskFile(); + $this->assertDownloadTaskFile(); + $this->assertGetAllFiles(); + $this->assertRemoveTaskFile(); + $this->assertRemoveAllTaskFiles(); + } + + public function assertCreateTaskFile() + { + $this->fileId = $this->app->createTaskFile(1, $this->taskId, 'My file', base64_encode('plain text file')); + $this->assertNotFalse($this->fileId); + } + + public function assertGetTaskFile() + { + $file = $this->app->getTaskFile($this->fileId); + $this->assertNotEmpty($file); + $this->assertEquals('My file', $file['name']); + } + + public function assertDownloadTaskFile() + { + $content = $this->app->downloadTaskFile($this->fileId); + $this->assertNotEmpty($content); + $this->assertEquals('plain text file', base64_decode($content)); + } + + public function assertGetAllFiles() + { + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertCount(1, $files); + $this->assertEquals('My file', $files[0]['name']); + } + + public function assertRemoveTaskFile() + { + $this->assertTrue($this->app->removeTaskFile($this->fileId)); + + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertEmpty($files); + } + + public function assertRemoveAllTaskFiles() + { + $this->assertCreateTaskFile(); + $this->assertCreateTaskFile(); + + $this->assertTrue($this->app->removeAllTaskFiles($this->taskId)); + + $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); + $this->assertEmpty($files); + } +} diff --git a/tests/integration/TaskFileTest.php b/tests/integration/TaskFileTest.php deleted file mode 100644 index 7e9e943b..00000000 --- a/tests/integration/TaskFileTest.php +++ /dev/null @@ -1,67 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateTask(); - $this->assertCreateTaskFile(); - $this->assertGetTaskFile(); - $this->assertDownloadTaskFile(); - $this->assertGetAllFiles(); - $this->assertRemoveTaskFile(); - $this->assertRemoveAllTaskFiles(); - } - - public function assertCreateTaskFile() - { - $this->fileId = $this->app->createTaskFile(1, $this->taskId, 'My file', base64_encode('plain text file')); - $this->assertNotFalse($this->fileId); - } - - public function assertGetTaskFile() - { - $file = $this->app->getTaskFile($this->fileId); - $this->assertNotEmpty($file); - $this->assertEquals('My file', $file['name']); - } - - public function assertDownloadTaskFile() - { - $content = $this->app->downloadTaskFile($this->fileId); - $this->assertNotEmpty($content); - $this->assertEquals('plain text file', base64_decode($content)); - } - - public function assertGetAllFiles() - { - $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); - $this->assertCount(1, $files); - $this->assertEquals('My file', $files[0]['name']); - } - - public function assertRemoveTaskFile() - { - $this->assertTrue($this->app->removeTaskFile($this->fileId)); - - $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); - $this->assertEmpty($files); - } - - public function assertRemoveAllTaskFiles() - { - $this->assertCreateTaskFile(); - $this->assertCreateTaskFile(); - - $this->assertTrue($this->app->removeAllTaskFiles($this->taskId)); - - $files = $this->app->getAllTaskFiles(array('task_id' => $this->taskId)); - $this->assertEmpty($files); - } -} diff --git a/tests/integration/TaskLinkProcedureTest.php b/tests/integration/TaskLinkProcedureTest.php new file mode 100644 index 00000000..a25fced5 --- /dev/null +++ b/tests/integration/TaskLinkProcedureTest.php @@ -0,0 +1,68 @@ +assertCreateTeamProject(); + + $this->taskId1 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 1')); + $this->taskId2 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 2')); + + $this->assertNotFalse($this->taskId1); + $this->assertNotFalse($this->taskId2); + + $this->assertCreateTaskLink(); + $this->assertGetTaskLink(); + $this->assertGetAllTaskLinks(); + $this->assertUpdateTaskLink(); + $this->assertRemoveTaskLink(); + } + + public function assertCreateTaskLink() + { + $this->taskLinkId = $this->app->createTaskLink($this->taskId1, $this->taskId2, 1); + $this->assertNotFalse($this->taskLinkId); + } + + public function assertGetTaskLink() + { + $link = $this->app->getTaskLinkById($this->taskLinkId); + $this->assertNotNull($link); + $this->assertEquals($this->taskId1, $link['task_id']); + $this->assertEquals($this->taskId2, $link['opposite_task_id']); + $this->assertEquals(1, $link['link_id']); + } + + public function assertGetAllTaskLinks() + { + $links = $this->app->getAllTaskLinks($this->taskId2); + $this->assertCount(1, $links); + } + + public function assertUpdateTaskLink() + { + $this->assertTrue($this->app->updateTaskLink($this->taskLinkId, $this->taskId1, $this->taskId2, 3)); + + $link = $this->app->getTaskLinkById($this->taskLinkId); + $this->assertNotNull($link); + $this->assertEquals($this->taskId1, $link['task_id']); + $this->assertEquals($this->taskId2, $link['opposite_task_id']); + $this->assertEquals(3, $link['link_id']); + } + + public function assertRemoveTaskLink() + { + $this->assertTrue($this->app->removeTaskLink($this->taskLinkId)); + + $links = $this->app->getAllTaskLinks($this->taskId2); + $this->assertCount(0, $links); + } +} diff --git a/tests/integration/TaskLinkTest.php b/tests/integration/TaskLinkTest.php deleted file mode 100644 index 03bc437b..00000000 --- a/tests/integration/TaskLinkTest.php +++ /dev/null @@ -1,68 +0,0 @@ -assertCreateTeamProject(); - - $this->taskId1 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 1')); - $this->taskId2 = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 2')); - - $this->assertNotFalse($this->taskId1); - $this->assertNotFalse($this->taskId2); - - $this->assertCreateTaskLink(); - $this->assertGetTaskLink(); - $this->assertGetAllTaskLinks(); - $this->assertUpdateTaskLink(); - $this->assertRemoveTaskLink(); - } - - public function assertCreateTaskLink() - { - $this->taskLinkId = $this->app->createTaskLink($this->taskId1, $this->taskId2, 1); - $this->assertNotFalse($this->taskLinkId); - } - - public function assertGetTaskLink() - { - $link = $this->app->getTaskLinkById($this->taskLinkId); - $this->assertNotNull($link); - $this->assertEquals($this->taskId1, $link['task_id']); - $this->assertEquals($this->taskId2, $link['opposite_task_id']); - $this->assertEquals(1, $link['link_id']); - } - - public function assertGetAllTaskLinks() - { - $links = $this->app->getAllTaskLinks($this->taskId2); - $this->assertCount(1, $links); - } - - public function assertUpdateTaskLink() - { - $this->assertTrue($this->app->updateTaskLink($this->taskLinkId, $this->taskId1, $this->taskId2, 3)); - - $link = $this->app->getTaskLinkById($this->taskLinkId); - $this->assertNotNull($link); - $this->assertEquals($this->taskId1, $link['task_id']); - $this->assertEquals($this->taskId2, $link['opposite_task_id']); - $this->assertEquals(3, $link['link_id']); - } - - public function assertRemoveTaskLink() - { - $this->assertTrue($this->app->removeTaskLink($this->taskLinkId)); - - $links = $this->app->getAllTaskLinks($this->taskId2); - $this->assertCount(0, $links); - } -} diff --git a/tests/integration/TaskProcedureTest.php b/tests/integration/TaskProcedureTest.php new file mode 100644 index 00000000..f456ae52 --- /dev/null +++ b/tests/integration/TaskProcedureTest.php @@ -0,0 +1,55 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertUpdateTask(); + $this->assertGetTaskById(); + $this->assertGetTaskByReference(); + $this->assertGetAllTasks(); + $this->assertOpenCloseTask(); + } + + public function assertUpdateTask() + { + $this->assertTrue($this->app->updateTask(array('id' => $this->taskId, 'color_id' => 'red'))); + } + + public function assertGetTaskById() + { + $task = $this->app->getTask($this->taskId); + $this->assertNotNull($task); + $this->assertEquals('red', $task['color_id']); + $this->assertEquals($this->taskTitle, $task['title']); + } + + public function assertGetTaskByReference() + { + $taskId = $this->app->createTask(array('title' => 'task with reference', 'project_id' => $this->projectId, 'reference' => 'test')); + $this->assertNotFalse($taskId); + + $task = $this->app->getTaskByReference($this->projectId, 'test'); + $this->assertNotNull($task); + $this->assertEquals($taskId, $task['id']); + } + + public function assertGetAllTasks() + { + $tasks = $this->app->getAllTasks($this->projectId); + $this->assertInternalType('array', $tasks); + $this->assertNotEmpty($tasks); + } + + public function assertOpenCloseTask() + { + $this->assertTrue($this->app->closeTask($this->taskId)); + $this->assertTrue($this->app->openTask($this->taskId)); + } +} diff --git a/tests/integration/TaskTest.php b/tests/integration/TaskTest.php deleted file mode 100644 index 6f1d9d62..00000000 --- a/tests/integration/TaskTest.php +++ /dev/null @@ -1,55 +0,0 @@ -assertCreateTeamProject(); - $this->assertCreateTask(); - $this->assertUpdateTask(); - $this->assertGetTaskById(); - $this->assertGetTaskByReference(); - $this->assertGetAllTasks(); - $this->assertOpenCloseTask(); - } - - public function assertUpdateTask() - { - $this->assertTrue($this->app->updateTask(array('id' => $this->taskId, 'color_id' => 'red'))); - } - - public function assertGetTaskById() - { - $task = $this->app->getTask($this->taskId); - $this->assertNotNull($task); - $this->assertEquals('red', $task['color_id']); - $this->assertEquals($this->taskTitle, $task['title']); - } - - public function assertGetTaskByReference() - { - $taskId = $this->app->createTask(array('title' => 'task with reference', 'project_id' => $this->projectId, 'reference' => 'test')); - $this->assertNotFalse($taskId); - - $task = $this->app->getTaskByReference($this->projectId, 'test'); - $this->assertNotNull($task); - $this->assertEquals($taskId, $task['id']); - } - - public function assertGetAllTasks() - { - $tasks = $this->app->getAllTasks($this->projectId); - $this->assertInternalType('array', $tasks); - $this->assertNotEmpty($tasks); - } - - public function assertOpenCloseTask() - { - $this->assertTrue($this->app->closeTask($this->taskId)); - $this->assertTrue($this->app->openTask($this->taskId)); - } -} diff --git a/tests/integration/UserProcedureTest.php b/tests/integration/UserProcedureTest.php new file mode 100644 index 00000000..290f87fb --- /dev/null +++ b/tests/integration/UserProcedureTest.php @@ -0,0 +1,63 @@ +assertCreateUser(); + $this->assertGetUserById(); + $this->assertGetUserByName(); + $this->assertGetAllUsers(); + $this->assertEnableDisableUser(); + $this->assertUpdateUser(); + $this->assertRemoveUser(); + } + + public function assertGetUserById() + { + $user = $this->app->getUser($this->userId); + $this->assertNotNull($user); + $this->assertEquals($this->username, $user['username']); + } + + public function assertGetUserByName() + { + $user = $this->app->getUserByName($this->username); + $this->assertNotNull($user); + $this->assertEquals($this->username, $user['username']); + } + + public function assertGetAllUsers() + { + $users = $this->app->getAllUsers(); + $this->assertInternalType('array', $users); + $this->assertNotEmpty($users); + } + + public function assertEnableDisableUser() + { + $this->assertTrue($this->app->disableUser($this->userId)); + $this->assertFalse($this->app->isActiveUser($this->userId)); + $this->assertTrue($this->app->enableUser($this->userId)); + $this->assertTrue($this->app->isActiveUser($this->userId)); + } + + public function assertUpdateUser() + { + $this->assertTrue($this->app->updateUser(array( + 'id' => $this->userId, + 'name' => 'My user', + ))); + + $user = $this->app->getUser($this->userId); + $this->assertNotNull($user); + $this->assertEquals('My user', $user['name']); + } + + public function assertRemoveUser() + { + $this->assertTrue($this->app->removeUser($this->userId)); + } +} diff --git a/tests/integration/UserTest.php b/tests/integration/UserTest.php deleted file mode 100644 index c407c918..00000000 --- a/tests/integration/UserTest.php +++ /dev/null @@ -1,63 +0,0 @@ -assertCreateUser(); - $this->assertGetUserById(); - $this->assertGetUserByName(); - $this->assertGetAllUsers(); - $this->assertEnableDisableUser(); - $this->assertUpdateUser(); - $this->assertRemoveUser(); - } - - public function assertGetUserById() - { - $user = $this->app->getUser($this->userId); - $this->assertNotNull($user); - $this->assertEquals($this->username, $user['username']); - } - - public function assertGetUserByName() - { - $user = $this->app->getUserByName($this->username); - $this->assertNotNull($user); - $this->assertEquals($this->username, $user['username']); - } - - public function assertGetAllUsers() - { - $users = $this->app->getAllUsers(); - $this->assertInternalType('array', $users); - $this->assertNotEmpty($users); - } - - public function assertEnableDisableUser() - { - $this->assertTrue($this->app->disableUser($this->userId)); - $this->assertFalse($this->app->isActiveUser($this->userId)); - $this->assertTrue($this->app->enableUser($this->userId)); - $this->assertTrue($this->app->isActiveUser($this->userId)); - } - - public function assertUpdateUser() - { - $this->assertTrue($this->app->updateUser(array( - 'id' => $this->userId, - 'name' => 'My user', - ))); - - $user = $this->app->getUser($this->userId); - $this->assertNotNull($user); - $this->assertEquals('My user', $user['name']); - } - - public function assertRemoveUser() - { - $this->assertTrue($this->app->removeUser($this->userId)); - } -} diff --git a/tests/units/Model/ActionModelTest.php b/tests/units/Model/ActionModelTest.php new file mode 100644 index 00000000..4e21a999 --- /dev/null +++ b/tests/units/Model/ActionModelTest.php @@ -0,0 +1,527 @@ +container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + } + + public function testRemove() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertNotEmpty($actionModel->getById(1)); + $this->assertTrue($actionModel->remove(1)); + $this->assertEmpty($actionModel->getById(1)); + } + + public function testGetById() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $action = $actionModel->getById(1); + $this->assertNotEmpty($action); + $this->assertEquals(1, $action['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $action['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $action['event_name']); + $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $action['params']); + } + + public function testGetProjectId() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertEquals(1, $actionModel->getProjectId(1)); + $this->assertSame(0, $actionModel->getProjectId(42)); + } + + public function testGetAll() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertEquals(2, $actionModel->create(array( + 'project_id' => 2, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 6, 'color_id' => 'blue'), + ))); + + $actions = $actionModel->getAll(); + $this->assertCount(2, $actions); + + $this->assertEquals(1, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); + + $this->assertEquals(2, $actions[1]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[1]['action_name']); + $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[1]['event_name']); + $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[1]['params']); + } + + public function testGetAllByProject() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertEquals(2, $actionModel->create(array( + 'project_id' => 2, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 6, 'color_id' => 'blue'), + ))); + + $actions = $actionModel->getAllByProject(1); + $this->assertCount(1, $actions); + + $this->assertEquals(1, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); + + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[0]['params']); + } + + public function testGetAllByUser() + { + $projectModel = new ProjectModel($this->container); + $projectUserRoleModel = new ProjectUserRoleModel($this->container); + $userModel = new UserModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + $this->assertEquals(3, $projectModel->create(array('name' => 'test4', 'is_active' => 0))); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user2'))); + + $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_VIEWER)); + $this->assertTrue($projectUserRoleModel->addUser(2, 3, Role::PROJECT_MANAGER)); + $this->assertTrue($projectUserRoleModel->addUser(3, 3, Role::PROJECT_MANAGER)); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertEquals(2, $actionModel->create(array( + 'project_id' => 2, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 6, 'color_id' => 'blue'), + ))); + + $this->assertEquals(3, $actionModel->create(array( + 'project_id' => 3, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 10, 'color_id' => 'green'), + ))); + + $actions = $actionModel->getAllByUser(1); + $this->assertCount(0, $actions); + + $actions = $actionModel->getAllByUser(2); + $this->assertCount(1, $actions); + + $this->assertEquals(1, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); + + $actions = $actionModel->getAllByUser(3); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[0]['params']); + } + + public function testDuplicateWithColumnAndColorParameter() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 5, 'color_id' => 'red'), $actions[0]['params']); + } + + public function testDuplicateWithColumnsParameter() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('src_column_id' => 1, 'dst_column_id' => 2, 'dest_column_id' => 3), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('src_column_id' => 5, 'dst_column_id' => 6, 'dest_column_id' => 7), $actions[0]['params']); + } + + public function testDuplicateWithColumnParameterNotfound() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + $columnModel = new ColumnModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertTrue($columnModel->update(2, 'My unique column')); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 1, 'color_id' => 'red'), + ))); + + $this->assertEquals(2, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', + 'params' => array('column_id' => 2, 'color_id' => 'green'), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 5, 'color_id' => 'red'), $actions[0]['params']); + } + + public function testDuplicateWithProjectParameter() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + $this->assertEquals(3, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CLOSE, + 'action_name' => '\Kanboard\Action\TaskDuplicateAnotherProject', + 'params' => array('column_id' => 1, 'project_id' => 3), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskDuplicateAnotherProject', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CLOSE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 5, 'project_id' => 3), $actions[0]['params']); + } + + public function testDuplicateWithProjectParameterIdenticalToDestination() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CLOSE, + 'action_name' => '\Kanboard\Action\TaskDuplicateAnotherProject', + 'params' => array('column_id' => 1, 'project_id' => 2), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(0, $actions); + } + + public function testDuplicateWithUserParameter() + { + $projectUserRoleModel = new ProjectUserRoleModel($this->container); + $userModel = new UserModel($this->container); + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); + + $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MEMBER)); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', + 'params' => array('column_id' => 1, 'user_id' => 2), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignSpecificUser', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 5, 'user_id' => 2), $actions[0]['params']); + } + + public function testDuplicateWithUserParameterButNotAssignable() + { + $projectUserRoleModel = new ProjectUserRoleModel($this->container); + $userModel = new UserModel($this->container); + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); + + $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_VIEWER)); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', + 'params' => array('column_id' => 1, 'user_id' => 2), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(0, $actions); + } + + public function testDuplicateWithUserParameterButNotAvailable() + { + $projectUserRoleModel = new ProjectUserRoleModel($this->container); + $userModel = new UserModel($this->container); + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); + + $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_MOVE_COLUMN, + 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', + 'params' => array('column_id' => 1, 'owner_id' => 2), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(0, $actions); + } + + public function testDuplicateWithCategoryParameter() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'c1', 'project_id' => 2))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE_UPDATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', + 'params' => array('column_id' => 1, 'category_id' => 1), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(1, $actions); + + $this->assertEquals(2, $actions[0]['project_id']); + $this->assertEquals('\Kanboard\Action\TaskAssignColorCategory', $actions[0]['action_name']); + $this->assertEquals(TaskModel::EVENT_CREATE_UPDATE, $actions[0]['event_name']); + $this->assertEquals(array('column_id' => 5, 'category_id' => 2), $actions[0]['params']); + } + + public function testDuplicateWithCategoryParameterButDifferentName() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'c2', 'project_id' => 2))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE_UPDATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', + 'params' => array('column_id' => 1, 'category_id' => 1), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(0, $actions); + } + + public function testDuplicateWithCategoryParameterButNotFound() + { + $projectModel = new ProjectModel($this->container); + $actionModel = new ActionModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + + $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); + + $this->assertEquals(1, $actionModel->create(array( + 'project_id' => 1, + 'event_name' => TaskModel::EVENT_CREATE_UPDATE, + 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', + 'params' => array('column_id' => 1, 'category_id' => 1), + ))); + + $this->assertTrue($actionModel->duplicate(1, 2)); + + $actions = $actionModel->getAllByProject(2); + $this->assertCount(0, $actions); + } +} diff --git a/tests/units/Model/ActionTest.php b/tests/units/Model/ActionTest.php deleted file mode 100644 index 5db18983..00000000 --- a/tests/units/Model/ActionTest.php +++ /dev/null @@ -1,509 +0,0 @@ -container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - } - - public function testRemove() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertNotEmpty($actionModel->getById(1)); - $this->assertTrue($actionModel->remove(1)); - $this->assertEmpty($actionModel->getById(1)); - } - - public function testGetById() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $action = $actionModel->getById(1); - $this->assertNotEmpty($action); - $this->assertEquals(1, $action['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $action['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $action['event_name']); - $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $action['params']); - } - - public function testGetAll() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertEquals(2, $actionModel->create(array( - 'project_id' => 2, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 6, 'color_id' => 'blue'), - ))); - - $actions = $actionModel->getAll(); - $this->assertCount(2, $actions); - - $this->assertEquals(1, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); - - $this->assertEquals(2, $actions[1]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[1]['action_name']); - $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[1]['event_name']); - $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[1]['params']); - } - - public function testGetAllByProject() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertEquals(2, $actionModel->create(array( - 'project_id' => 2, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 6, 'color_id' => 'blue'), - ))); - - $actions = $actionModel->getAllByProject(1); - $this->assertCount(1, $actions); - - $this->assertEquals(1, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); - - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[0]['params']); - } - - public function testGetAllByUser() - { - $projectModel = new ProjectModel($this->container); - $projectUserRoleModel = new ProjectUserRoleModel($this->container); - $userModel = new UserModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $this->assertEquals(3, $projectModel->create(array('name' => 'test4', 'is_active' => 0))); - - $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); - $this->assertEquals(3, $userModel->create(array('username' => 'user2'))); - - $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_VIEWER)); - $this->assertTrue($projectUserRoleModel->addUser(2, 3, Role::PROJECT_MANAGER)); - $this->assertTrue($projectUserRoleModel->addUser(3, 3, Role::PROJECT_MANAGER)); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertEquals(2, $actionModel->create(array( - 'project_id' => 2, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 6, 'color_id' => 'blue'), - ))); - - $this->assertEquals(3, $actionModel->create(array( - 'project_id' => 3, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 10, 'color_id' => 'green'), - ))); - - $actions = $actionModel->getAllByUser(1); - $this->assertCount(0, $actions); - - $actions = $actionModel->getAllByUser(2); - $this->assertCount(1, $actions); - - $this->assertEquals(1, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 1, 'color_id' => 'red'), $actions[0]['params']); - - $actions = $actionModel->getAllByUser(3); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 6, 'color_id' => 'blue'), $actions[0]['params']); - } - - public function testDuplicateWithColumnAndColorParameter() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 5, 'color_id' => 'red'), $actions[0]['params']); - } - - public function testDuplicateWithColumnsParameter() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('src_column_id' => 1, 'dst_column_id' => 2, 'dest_column_id' => 3), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('src_column_id' => 5, 'dst_column_id' => 6, 'dest_column_id' => 7), $actions[0]['params']); - } - - public function testDuplicateWithColumnParameterNotfound() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - $columnModel = new ColumnModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertTrue($columnModel->update(2, 'My unique column')); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 1, 'color_id' => 'red'), - ))); - - $this->assertEquals(2, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignColorColumn', - 'params' => array('column_id' => 2, 'color_id' => 'green'), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorColumn', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 5, 'color_id' => 'red'), $actions[0]['params']); - } - - public function testDuplicateWithProjectParameter() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $this->assertEquals(3, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CLOSE, - 'action_name' => '\Kanboard\Action\TaskDuplicateAnotherProject', - 'params' => array('column_id' => 1, 'project_id' => 3), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskDuplicateAnotherProject', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CLOSE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 5, 'project_id' => 3), $actions[0]['params']); - } - - public function testDuplicateWithProjectParameterIdenticalToDestination() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CLOSE, - 'action_name' => '\Kanboard\Action\TaskDuplicateAnotherProject', - 'params' => array('column_id' => 1, 'project_id' => 2), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(0, $actions); - } - - public function testDuplicateWithUserParameter() - { - $projectUserRoleModel = new ProjectUserRoleModel($this->container); - $userModel = new UserModel($this->container); - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); - - $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); - $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MEMBER)); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', - 'params' => array('column_id' => 1, 'user_id' => 2), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignSpecificUser', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_MOVE_COLUMN, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 5, 'user_id' => 2), $actions[0]['params']); - } - - public function testDuplicateWithUserParameterButNotAssignable() - { - $projectUserRoleModel = new ProjectUserRoleModel($this->container); - $userModel = new UserModel($this->container); - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); - - $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); - $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_VIEWER)); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', - 'params' => array('column_id' => 1, 'user_id' => 2), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(0, $actions); - } - - public function testDuplicateWithUserParameterButNotAvailable() - { - $projectUserRoleModel = new ProjectUserRoleModel($this->container); - $userModel = new UserModel($this->container); - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); - - $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_MOVE_COLUMN, - 'action_name' => '\Kanboard\Action\TaskAssignSpecificUser', - 'params' => array('column_id' => 1, 'owner_id' => 2), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(0, $actions); - } - - public function testDuplicateWithCategoryParameter() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'c1', 'project_id' => 2))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE_UPDATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', - 'params' => array('column_id' => 1, 'category_id' => 1), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(1, $actions); - - $this->assertEquals(2, $actions[0]['project_id']); - $this->assertEquals('\Kanboard\Action\TaskAssignColorCategory', $actions[0]['action_name']); - $this->assertEquals(TaskModel::EVENT_CREATE_UPDATE, $actions[0]['event_name']); - $this->assertEquals(array('column_id' => 5, 'category_id' => 2), $actions[0]['params']); - } - - public function testDuplicateWithCategoryParameterButDifferentName() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'c2', 'project_id' => 2))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE_UPDATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', - 'params' => array('column_id' => 1, 'category_id' => 1), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(0, $actions); - } - - public function testDuplicateWithCategoryParameterButNotFound() - { - $projectModel = new ProjectModel($this->container); - $actionModel = new ActionModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - - $this->assertEquals(1, $categoryModel->create(array('name' => 'c1', 'project_id' => 1))); - - $this->assertEquals(1, $actionModel->create(array( - 'project_id' => 1, - 'event_name' => TaskModel::EVENT_CREATE_UPDATE, - 'action_name' => '\Kanboard\Action\TaskAssignColorCategory', - 'params' => array('column_id' => 1, 'category_id' => 1), - ))); - - $this->assertTrue($actionModel->duplicate(1, 2)); - - $actions = $actionModel->getAllByProject(2); - $this->assertCount(0, $actions); - } -} diff --git a/tests/units/Model/CategoryModelTest.php b/tests/units/Model/CategoryModelTest.php new file mode 100644 index 00000000..80a20af6 --- /dev/null +++ b/tests/units/Model/CategoryModelTest.php @@ -0,0 +1,229 @@ +container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(2, $task['category_id']); + + $category = $categoryModel->getById(2); + $this->assertEquals(2, $category['id']); + $this->assertEquals('Category #2', $category['name']); + $this->assertEquals(1, $category['project_id']); + } + + public function testExists() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertTrue($categoryModel->exists(1)); + $this->assertFalse($categoryModel->exists(2)); + } + + public function testGetById() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $category = $categoryModel->getById(1); + $this->assertEquals(1, $category['id']); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); + } + + public function testGetNameById() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertEquals('Category #1', $categoryModel->getNameById(1)); + $this->assertEquals('', $categoryModel->getNameById(2)); + } + + public function testGetIdByName() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertSame(1, $categoryModel->getIdByName(1, 'Category #1')); + $this->assertSame(0, $categoryModel->getIdByName(1, 'Category #2')); + } + + public function testGetProjectId() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertEquals(1, $categoryModel->getProjectId(1)); + $this->assertSame(0, $categoryModel->getProjectId(2)); + } + + public function testGetList() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + + $categories = $categoryModel->getList(1, false, false); + $this->assertCount(2, $categories); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + + $categories = $categoryModel->getList(1, true, false); + $this->assertCount(3, $categories); + $this->assertEquals('No category', $categories[0]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + + $categories = $categoryModel->getList(1, false, true); + $this->assertCount(3, $categories); + $this->assertEquals('All categories', $categories[-1]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + + $categories = $categoryModel->getList(1, true, true); + $this->assertCount(4, $categories); + $this->assertEquals('All categories', $categories[-1]); + $this->assertEquals('No category', $categories[0]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + } + + public function testGetAll() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + + $categories = $categoryModel->getAll(1); + $this->assertCount(2, $categories); + + $this->assertEquals('Category #1', $categories[0]['name']); + $this->assertEquals('test', $categories[0]['description']); + $this->assertEquals(1, $categories[0]['project_id']); + $this->assertEquals(1, $categories[0]['id']); + + $this->assertEquals('Category #2', $categories[1]['name']); + $this->assertEquals('', $categories[1]['description']); + $this->assertEquals(1, $categories[1]['project_id']); + $this->assertEquals(2, $categories[1]['id']); + } + + public function testCreateDefaultCategories() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + $configModel = new ConfigModel($this->container); + + $this->assertTrue($configModel->save(array('project_categories' => 'C1, C2, C3'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertTrue($categoryModel->createDefaultCategories(1)); + + $categories = $categoryModel->getAll(1); + $this->assertCount(3, $categories); + $this->assertEquals('C1', $categories[0]['name']); + $this->assertEquals('C2', $categories[1]['name']); + $this->assertEquals('C3', $categories[2]['name']); + } + + public function testUpdate() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertTrue($categoryModel->update(array('id' => 1, 'description' => 'test'))); + + $category = $categoryModel->getById(1); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); + } + + public function testRemove() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(2, $task['category_id']); + + $this->assertTrue($categoryModel->remove(1)); + $this->assertTrue($categoryModel->remove(2)); + + // Make sure tasks assigned with that category are reseted + $task = $taskFinderModel->getById(1); + $this->assertEquals(0, $task['category_id']); + } + + public function testDuplicate() + { + $projectModel = new ProjectModel($this->container); + $categoryModel = new CategoryModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertTrue($categoryModel->duplicate(1, 2)); + + $category = $categoryModel->getById(1); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); + + $category = $categoryModel->getById(2); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(2, $category['project_id']); + $this->assertEquals('test', $category['description']); + } +} diff --git a/tests/units/Model/CategoryTest.php b/tests/units/Model/CategoryTest.php deleted file mode 100644 index 1fdc51f6..00000000 --- a/tests/units/Model/CategoryTest.php +++ /dev/null @@ -1,217 +0,0 @@ -container); - $taskFinderModel = new TaskFinderModel($this->container); - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); - - $task = $taskFinderModel->getById(1); - $this->assertEquals(2, $task['category_id']); - - $category = $categoryModel->getById(2); - $this->assertEquals(2, $category['id']); - $this->assertEquals('Category #2', $category['name']); - $this->assertEquals(1, $category['project_id']); - } - - public function testExists() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertTrue($categoryModel->exists(1)); - $this->assertFalse($categoryModel->exists(2)); - } - - public function testGetById() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - - $category = $categoryModel->getById(1); - $this->assertEquals(1, $category['id']); - $this->assertEquals('Category #1', $category['name']); - $this->assertEquals(1, $category['project_id']); - $this->assertEquals('test', $category['description']); - } - - public function testGetNameById() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - - $this->assertEquals('Category #1', $categoryModel->getNameById(1)); - $this->assertEquals('', $categoryModel->getNameById(2)); - } - - public function testGetIdByName() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - - $this->assertSame(1, $categoryModel->getIdByName(1, 'Category #1')); - $this->assertSame(0, $categoryModel->getIdByName(1, 'Category #2')); - } - - public function testGetList() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); - - $categories = $categoryModel->getList(1, false, false); - $this->assertCount(2, $categories); - $this->assertEquals('Category #1', $categories[1]); - $this->assertEquals('Category #2', $categories[2]); - - $categories = $categoryModel->getList(1, true, false); - $this->assertCount(3, $categories); - $this->assertEquals('No category', $categories[0]); - $this->assertEquals('Category #1', $categories[1]); - $this->assertEquals('Category #2', $categories[2]); - - $categories = $categoryModel->getList(1, false, true); - $this->assertCount(3, $categories); - $this->assertEquals('All categories', $categories[-1]); - $this->assertEquals('Category #1', $categories[1]); - $this->assertEquals('Category #2', $categories[2]); - - $categories = $categoryModel->getList(1, true, true); - $this->assertCount(4, $categories); - $this->assertEquals('All categories', $categories[-1]); - $this->assertEquals('No category', $categories[0]); - $this->assertEquals('Category #1', $categories[1]); - $this->assertEquals('Category #2', $categories[2]); - } - - public function testGetAll() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); - - $categories = $categoryModel->getAll(1); - $this->assertCount(2, $categories); - - $this->assertEquals('Category #1', $categories[0]['name']); - $this->assertEquals('test', $categories[0]['description']); - $this->assertEquals(1, $categories[0]['project_id']); - $this->assertEquals(1, $categories[0]['id']); - - $this->assertEquals('Category #2', $categories[1]['name']); - $this->assertEquals('', $categories[1]['description']); - $this->assertEquals(1, $categories[1]['project_id']); - $this->assertEquals(2, $categories[1]['id']); - } - - public function testCreateDefaultCategories() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - $configModel = new ConfigModel($this->container); - - $this->assertTrue($configModel->save(array('project_categories' => 'C1, C2, C3'))); - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertTrue($categoryModel->createDefaultCategories(1)); - - $categories = $categoryModel->getAll(1); - $this->assertCount(3, $categories); - $this->assertEquals('C1', $categories[0]['name']); - $this->assertEquals('C2', $categories[1]['name']); - $this->assertEquals('C3', $categories[2]['name']); - } - - public function testUpdate() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertTrue($categoryModel->update(array('id' => 1, 'description' => 'test'))); - - $category = $categoryModel->getById(1); - $this->assertEquals('Category #1', $category['name']); - $this->assertEquals(1, $category['project_id']); - $this->assertEquals('test', $category['description']); - } - - public function testRemove() - { - $taskCreationModel = new TaskCreationModel($this->container); - $taskFinderModel = new TaskFinderModel($this->container); - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); - - $task = $taskFinderModel->getById(1); - $this->assertEquals(2, $task['category_id']); - - $this->assertTrue($categoryModel->remove(1)); - $this->assertTrue($categoryModel->remove(2)); - - // Make sure tasks assigned with that category are reseted - $task = $taskFinderModel->getById(1); - $this->assertEquals(0, $task['category_id']); - } - - public function testDuplicate() - { - $projectModel = new ProjectModel($this->container); - $categoryModel = new CategoryModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); - $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); - $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); - - $this->assertTrue($categoryModel->duplicate(1, 2)); - - $category = $categoryModel->getById(1); - $this->assertEquals('Category #1', $category['name']); - $this->assertEquals(1, $category['project_id']); - $this->assertEquals('test', $category['description']); - - $category = $categoryModel->getById(2); - $this->assertEquals('Category #1', $category['name']); - $this->assertEquals(2, $category['project_id']); - $this->assertEquals('test', $category['description']); - } -} diff --git a/tests/units/Model/CommentTest.php b/tests/units/Model/CommentTest.php index 7250ae0b..574b5a87 100644 --- a/tests/units/Model/CommentTest.php +++ b/tests/units/Model/CommentTest.php @@ -10,16 +10,16 @@ class CommentTest extends Base { public function testCreate() { - $c = new CommentModel($this->container); - $tc = new TaskCreationModel($this->container); - $p = new ProjectModel($this->container); + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - $this->assertEquals(1, $c->create(array('task_id' => 1, 'comment' => 'bla bla', 'user_id' => 1))); - $this->assertEquals(2, $c->create(array('task_id' => 1, 'comment' => 'bla bla'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'bla bla', 'user_id' => 1))); + $this->assertEquals(2, $commentModel->create(array('task_id' => 1, 'comment' => 'bla bla'))); - $comment = $c->getById(1); + $comment = $commentModel->getById(1); $this->assertNotEmpty($comment); $this->assertEquals('bla bla', $comment['comment']); $this->assertEquals(1, $comment['task_id']); @@ -27,7 +27,7 @@ class CommentTest extends Base $this->assertEquals('admin', $comment['username']); $this->assertEquals(time(), $comment['date_creation'], '', 3); - $comment = $c->getById(2); + $comment = $commentModel->getById(2); $this->assertNotEmpty($comment); $this->assertEquals('bla bla', $comment['comment']); $this->assertEquals(1, $comment['task_id']); @@ -38,17 +38,17 @@ class CommentTest extends Base public function testGetAll() { - $c = new CommentModel($this->container); - $tc = new TaskCreationModel($this->container); - $p = new ProjectModel($this->container); + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - $this->assertNotFalse($c->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); - $this->assertNotFalse($c->create(array('task_id' => 1, 'comment' => 'c2', 'user_id' => 1))); - $this->assertNotFalse($c->create(array('task_id' => 1, 'comment' => 'c3', 'user_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); + $this->assertEquals(2, $commentModel->create(array('task_id' => 1, 'comment' => 'c2', 'user_id' => 1))); + $this->assertEquals(3, $commentModel->create(array('task_id' => 1, 'comment' => 'c3', 'user_id' => 1))); - $comments = $c->getAll(1); + $comments = $commentModel->getAll(1); $this->assertNotEmpty($comments); $this->assertEquals(3, count($comments)); @@ -56,37 +56,51 @@ class CommentTest extends Base $this->assertEquals(2, $comments[1]['id']); $this->assertEquals(3, $comments[2]['id']); - $this->assertEquals(3, $c->count(1)); + $this->assertEquals(3, $commentModel->count(1)); } public function testUpdate() { - $c = new CommentModel($this->container); - $tc = new TaskCreationModel($this->container); - $p = new ProjectModel($this->container); + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - $this->assertNotFalse($c->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); - $this->assertTrue($c->update(array('id' => 1, 'comment' => 'bla'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); + $this->assertTrue($commentModel->update(array('id' => 1, 'comment' => 'bla'))); - $comment = $c->getById(1); + $comment = $commentModel->getById(1); $this->assertNotEmpty($comment); $this->assertEquals('bla', $comment['comment']); } public function validateRemove() { - $c = new CommentModel($this->container); - $tc = new TaskCreationModel($this->container); - $p = new ProjectModel($this->container); + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - $this->assertTrue($c->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); - $this->assertTrue($c->remove(1)); - $this->assertFalse($c->remove(1)); - $this->assertFalse($c->remove(1111)); + $this->assertTrue($commentModel->remove(1)); + $this->assertFalse($commentModel->remove(1)); + $this->assertFalse($commentModel->remove(1111)); + } + + public function testGetProjectId() + { + $commentModel = new CommentModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); + + $this->assertEquals(1, $commentModel->getProjectId(1)); + $this->assertSame(0, $commentModel->getProjectId(2)); } } diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php new file mode 100644 index 00000000..6451189d --- /dev/null +++ b/tests/units/Model/SubtaskModelTest.php @@ -0,0 +1,400 @@ +assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); + $data = $event->getAll(); + + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('time_estimated', $data); + $this->assertArrayHasKey('time_spent', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('task_id', $data); + $this->assertArrayHasKey('user_id', $data); + $this->assertArrayHasKey('position', $data); + $this->assertNotEmpty($data['task_id']); + $this->assertNotEmpty($data['id']); + } + + public function onSubtaskUpdated($event) + { + $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); + $data = $event->getAll(); + + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('time_estimated', $data); + $this->assertArrayHasKey('time_spent', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('task_id', $data); + $this->assertArrayHasKey('user_id', $data); + $this->assertArrayHasKey('position', $data); + $this->assertArrayHasKey('changes', $data); + $this->assertArrayHasKey('user_id', $data['changes']); + $this->assertArrayHasKey('status', $data['changes']); + + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $data['changes']['status']); + $this->assertEquals(1, $data['changes']['user_id']); + } + + public function onSubtaskDeleted($event) + { + $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); + $data = $event->getAll(); + + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('title', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('time_estimated', $data); + $this->assertArrayHasKey('time_spent', $data); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('task_id', $data); + $this->assertArrayHasKey('user_id', $data); + $this->assertArrayHasKey('position', $data); + $this->assertNotEmpty($data['task_id']); + $this->assertNotEmpty($data['id']); + } + + public function testCreation() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_CREATE, array($this, 'onSubtaskCreated')); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(1, $subtask['id']); + $this->assertEquals(1, $subtask['task_id']); + $this->assertEquals('subtask #1', $subtask['title']); + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); + $this->assertEquals(0, $subtask['time_estimated']); + $this->assertEquals(0, $subtask['time_spent']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['position']); + } + + public function testModification() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_UPDATE, array($this, 'onSubtaskUpdated')); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'user_id' => 1, 'status' => SubtaskModel::STATUS_INPROGRESS))); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(1, $subtask['id']); + $this->assertEquals(1, $subtask['task_id']); + $this->assertEquals('subtask #1', $subtask['title']); + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); + $this->assertEquals(0, $subtask['time_estimated']); + $this->assertEquals(0, $subtask['time_spent']); + $this->assertEquals(1, $subtask['user_id']); + $this->assertEquals(1, $subtask['position']); + } + + public function testRemove() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + + $this->container['dispatcher']->addListener(SubtaskModel::EVENT_DELETE, array($this, 'onSubtaskDeleted')); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + + $this->assertTrue($subtaskModel->remove(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertEmpty($subtask); + } + + public function testToggleStatusWithoutSession() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + } + + public function testToggleStatusWithSession() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); + $this->assertEquals(0, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + // Set the current logged user + $this->container['sessionStorage']->user = array('id' => 1); + + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); + $this->assertEquals(1, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); + $this->assertEquals(1, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtaskModel->toggleStatus(1)); + + $subtask = $subtaskModel->getById(1); + $this->assertNotEmpty($subtask); + $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); + $this->assertEquals(1, $subtask['user_id']); + $this->assertEquals(1, $subtask['task_id']); + } + + public function testCloseAll() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1))); + + $this->assertTrue($subtaskModel->closeAll(1)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertNotEmpty($subtasks); + + foreach ($subtasks as $subtask) { + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); + } + } + + public function testDuplicate() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + // We create a project + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + + // We create 2 tasks + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'test 2', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 0))); + + // We create many subtasks for the first task + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_estimated' => 5, 'time_spent' => 3, 'status' => 1, 'another_subtask' => 'on'))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => '', 'time_spent' => '', 'status' => 2, 'user_id' => 1))); + + // We duplicate our subtasks + $this->assertTrue($subtaskModel->duplicate(1, 2)); + $subtasks = $subtaskModel->getAll(2); + + $this->assertNotFalse($subtasks); + $this->assertNotEmpty($subtasks); + $this->assertEquals(2, count($subtasks)); + + $this->assertEquals('subtask #1', $subtasks[0]['title']); + $this->assertEquals('subtask #2', $subtasks[1]['title']); + + $this->assertEquals(2, $subtasks[0]['task_id']); + $this->assertEquals(2, $subtasks[1]['task_id']); + + $this->assertEquals(5, $subtasks[0]['time_estimated']); + $this->assertEquals(0, $subtasks[1]['time_estimated']); + + $this->assertEquals(0, $subtasks[0]['time_spent']); + $this->assertEquals(0, $subtasks[1]['time_spent']); + + $this->assertEquals(0, $subtasks[0]['status']); + $this->assertEquals(0, $subtasks[1]['status']); + + $this->assertEquals(0, $subtasks[0]['user_id']); + $this->assertEquals(0, $subtasks[1]['user_id']); + + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(2, $subtasks[1]['position']); + } + + public function testChangePosition() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1))); + $this->assertEquals(3, $subtaskModel->create(array('title' => 'subtask #3', 'task_id' => 1))); + + $subtasks = $subtaskModel->getAll(1); + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(1, $subtasks[0]['id']); + $this->assertEquals(2, $subtasks[1]['position']); + $this->assertEquals(2, $subtasks[1]['id']); + $this->assertEquals(3, $subtasks[2]['position']); + $this->assertEquals(3, $subtasks[2]['id']); + + $this->assertTrue($subtaskModel->changePosition(1, 3, 2)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(1, $subtasks[0]['id']); + $this->assertEquals(2, $subtasks[1]['position']); + $this->assertEquals(3, $subtasks[1]['id']); + $this->assertEquals(3, $subtasks[2]['position']); + $this->assertEquals(2, $subtasks[2]['id']); + + $this->assertTrue($subtaskModel->changePosition(1, 2, 1)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(2, $subtasks[0]['id']); + $this->assertEquals(2, $subtasks[1]['position']); + $this->assertEquals(1, $subtasks[1]['id']); + $this->assertEquals(3, $subtasks[2]['position']); + $this->assertEquals(3, $subtasks[2]['id']); + + $this->assertTrue($subtaskModel->changePosition(1, 2, 2)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(1, $subtasks[0]['id']); + $this->assertEquals(2, $subtasks[1]['position']); + $this->assertEquals(2, $subtasks[1]['id']); + $this->assertEquals(3, $subtasks[2]['position']); + $this->assertEquals(3, $subtasks[2]['id']); + + $this->assertTrue($subtaskModel->changePosition(1, 1, 3)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertEquals(1, $subtasks[0]['position']); + $this->assertEquals(2, $subtasks[0]['id']); + $this->assertEquals(2, $subtasks[1]['position']); + $this->assertEquals(3, $subtasks[1]['id']); + $this->assertEquals(3, $subtasks[2]['position']); + $this->assertEquals(1, $subtasks[2]['id']); + + $this->assertFalse($subtaskModel->changePosition(1, 2, 0)); + $this->assertFalse($subtaskModel->changePosition(1, 2, 4)); + } + + public function testConvertToTask() + { + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1, 'user_id' => 1, 'time_spent' => 2, 'time_estimated' => 3))); + $task_id = $subtaskModel->convertToTask(1, 1); + + $this->assertNotFalse($task_id); + $this->assertEmpty($subtaskModel->getById(1)); + + $task = $taskFinderModel->getById($task_id); + $this->assertEquals('subtask #1', $task['title']); + $this->assertEquals(1, $task['project_id']); + $this->assertEquals(1, $task['owner_id']); + $this->assertEquals(2, $task['time_spent']); + $this->assertEquals(3, $task['time_estimated']); + } + + public function testGetProjectId() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); + + $this->assertEquals(1, $subtaskModel->getProjectId(1)); + $this->assertEquals(0, $subtaskModel->getProjectId(2)); + } +} diff --git a/tests/units/Model/SubtaskTest.php b/tests/units/Model/SubtaskTest.php deleted file mode 100644 index b65ee609..00000000 --- a/tests/units/Model/SubtaskTest.php +++ /dev/null @@ -1,388 +0,0 @@ -assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertNotEmpty($data['task_id']); - $this->assertNotEmpty($data['id']); - } - - public function onSubtaskUpdated($event) - { - $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertArrayHasKey('changes', $data); - $this->assertArrayHasKey('user_id', $data['changes']); - $this->assertArrayHasKey('status', $data['changes']); - - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $data['changes']['status']); - $this->assertEquals(1, $data['changes']['user_id']); - } - - public function onSubtaskDeleted($event) - { - $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); - $data = $event->getAll(); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('title', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('time_estimated', $data); - $this->assertArrayHasKey('time_spent', $data); - $this->assertArrayHasKey('status', $data); - $this->assertArrayHasKey('task_id', $data); - $this->assertArrayHasKey('user_id', $data); - $this->assertArrayHasKey('position', $data); - $this->assertNotEmpty($data['task_id']); - $this->assertNotEmpty($data['id']); - } - - public function testCreation() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_CREATE, array($this, 'onSubtaskCreated')); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['id']); - $this->assertEquals(1, $subtask['task_id']); - $this->assertEquals('subtask #1', $subtask['title']); - $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); - $this->assertEquals(0, $subtask['time_estimated']); - $this->assertEquals(0, $subtask['time_spent']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['position']); - } - - public function testModification() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_UPDATE, array($this, 'onSubtaskUpdated')); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertTrue($s->update(array('id' => 1, 'user_id' => 1, 'status' => SubtaskModel::STATUS_INPROGRESS))); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['id']); - $this->assertEquals(1, $subtask['task_id']); - $this->assertEquals('subtask #1', $subtask['title']); - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); - $this->assertEquals(0, $subtask['time_estimated']); - $this->assertEquals(0, $subtask['time_spent']); - $this->assertEquals(1, $subtask['user_id']); - $this->assertEquals(1, $subtask['position']); - } - - public function testRemove() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - - $this->container['dispatcher']->addListener(SubtaskModel::EVENT_DELETE, array($this, 'onSubtaskDeleted')); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - - $this->assertTrue($s->remove(1)); - - $subtask = $s->getById(1); - $this->assertEmpty($subtask); - } - - public function testToggleStatusWithoutSession() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - $this->assertEquals(SubtaskModel::STATUS_DONE, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - $this->assertEquals(SubtaskModel::STATUS_TODO, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - } - - public function testToggleStatusWithSession() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - $us = new UserSession($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); - $this->assertEquals(0, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - // Set the current logged user - $this->container['sessionStorage']->user = array('id' => 1); - - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_INPROGRESS, $subtask['status']); - $this->assertEquals(1, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - $this->assertEquals(SubtaskModel::STATUS_DONE, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); - $this->assertEquals(1, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - - $this->assertEquals(SubtaskModel::STATUS_TODO, $s->toggleStatus(1)); - - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(SubtaskModel::STATUS_TODO, $subtask['status']); - $this->assertEquals(1, $subtask['user_id']); - $this->assertEquals(1, $subtask['task_id']); - } - - public function testCloseAll() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1))); - - $this->assertTrue($s->closeAll(1)); - - $subtasks = $s->getAll(1); - $this->assertNotEmpty($subtasks); - - foreach ($subtasks as $subtask) { - $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); - } - } - - public function testDuplicate() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $p = new ProjectModel($this->container); - - // We create a project - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - - // We create 2 tasks - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'test 2', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 0))); - - // We create many subtasks for the first task - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_estimated' => 5, 'time_spent' => 3, 'status' => 1, 'another_subtask' => 'on'))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => '', 'time_spent' => '', 'status' => 2, 'user_id' => 1))); - - // We duplicate our subtasks - $this->assertTrue($s->duplicate(1, 2)); - $subtasks = $s->getAll(2); - - $this->assertNotFalse($subtasks); - $this->assertNotEmpty($subtasks); - $this->assertEquals(2, count($subtasks)); - - $this->assertEquals('subtask #1', $subtasks[0]['title']); - $this->assertEquals('subtask #2', $subtasks[1]['title']); - - $this->assertEquals(2, $subtasks[0]['task_id']); - $this->assertEquals(2, $subtasks[1]['task_id']); - - $this->assertEquals(5, $subtasks[0]['time_estimated']); - $this->assertEquals(0, $subtasks[1]['time_estimated']); - - $this->assertEquals(0, $subtasks[0]['time_spent']); - $this->assertEquals(0, $subtasks[1]['time_spent']); - - $this->assertEquals(0, $subtasks[0]['status']); - $this->assertEquals(0, $subtasks[1]['status']); - - $this->assertEquals(0, $subtasks[0]['user_id']); - $this->assertEquals(0, $subtasks[1]['user_id']); - - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(2, $subtasks[1]['position']); - } - - public function testChangePosition() - { - $taskCreationModel = new TaskCreationModel($this->container); - $subtaskModel = new SubtaskModel($this->container); - $projectModel = new ProjectModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1))); - $this->assertEquals(3, $subtaskModel->create(array('title' => 'subtask #3', 'task_id' => 1))); - - $subtasks = $subtaskModel->getAll(1); - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(1, $subtasks[0]['id']); - $this->assertEquals(2, $subtasks[1]['position']); - $this->assertEquals(2, $subtasks[1]['id']); - $this->assertEquals(3, $subtasks[2]['position']); - $this->assertEquals(3, $subtasks[2]['id']); - - $this->assertTrue($subtaskModel->changePosition(1, 3, 2)); - - $subtasks = $subtaskModel->getAll(1); - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(1, $subtasks[0]['id']); - $this->assertEquals(2, $subtasks[1]['position']); - $this->assertEquals(3, $subtasks[1]['id']); - $this->assertEquals(3, $subtasks[2]['position']); - $this->assertEquals(2, $subtasks[2]['id']); - - $this->assertTrue($subtaskModel->changePosition(1, 2, 1)); - - $subtasks = $subtaskModel->getAll(1); - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(2, $subtasks[0]['id']); - $this->assertEquals(2, $subtasks[1]['position']); - $this->assertEquals(1, $subtasks[1]['id']); - $this->assertEquals(3, $subtasks[2]['position']); - $this->assertEquals(3, $subtasks[2]['id']); - - $this->assertTrue($subtaskModel->changePosition(1, 2, 2)); - - $subtasks = $subtaskModel->getAll(1); - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(1, $subtasks[0]['id']); - $this->assertEquals(2, $subtasks[1]['position']); - $this->assertEquals(2, $subtasks[1]['id']); - $this->assertEquals(3, $subtasks[2]['position']); - $this->assertEquals(3, $subtasks[2]['id']); - - $this->assertTrue($subtaskModel->changePosition(1, 1, 3)); - - $subtasks = $subtaskModel->getAll(1); - $this->assertEquals(1, $subtasks[0]['position']); - $this->assertEquals(2, $subtasks[0]['id']); - $this->assertEquals(2, $subtasks[1]['position']); - $this->assertEquals(3, $subtasks[1]['id']); - $this->assertEquals(3, $subtasks[2]['position']); - $this->assertEquals(1, $subtasks[2]['id']); - - $this->assertFalse($subtaskModel->changePosition(1, 2, 0)); - $this->assertFalse($subtaskModel->changePosition(1, 2, 4)); - } - - public function testConvertToTask() - { - $taskCreationModel = new TaskCreationModel($this->container); - $taskFinderModel = new TaskFinderModel($this->container); - $subtaskModel = new SubtaskModel($this->container); - $projectModel = new ProjectModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1, 'user_id' => 1, 'time_spent' => 2, 'time_estimated' => 3))); - $task_id = $subtaskModel->convertToTask(1, 1); - - $this->assertNotFalse($task_id); - $this->assertEmpty($subtaskModel->getById(1)); - - $task = $taskFinderModel->getById($task_id); - $this->assertEquals('subtask #1', $task['title']); - $this->assertEquals(1, $task['project_id']); - $this->assertEquals(1, $task['owner_id']); - $this->assertEquals(2, $task['time_spent']); - $this->assertEquals(3, $task['time_estimated']); - } -} diff --git a/tests/units/Model/TaskFileModelTest.php b/tests/units/Model/TaskFileModelTest.php new file mode 100644 index 00000000..de12553f --- /dev/null +++ b/tests/units/Model/TaskFileModelTest.php @@ -0,0 +1,458 @@ +container); + $fileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals('/tmp/foo', $file['path']); + $this->assertEquals(0, $file['is_image']); + $this->assertEquals(1, $file['task_id']); + $this->assertEquals(time(), $file['date'], '', 2); + $this->assertEquals(0, $file['user_id']); + $this->assertEquals(10, $file['size']); + + $this->assertEquals(2, $fileModel->create(1, 'test2.png', '/tmp/foobar', 10)); + + $file = $fileModel->getById(2); + $this->assertEquals('test2.png', $file['name']); + $this->assertEquals('/tmp/foobar', $file['path']); + $this->assertEquals(1, $file['is_image']); + } + + public function testCreationWithFileNameTooLong() + { + $projectModel = new ProjectModel($this->container); + $fileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertNotFalse($fileModel->create(1, 'test', '/tmp/foo', 10)); + $this->assertNotFalse($fileModel->create(1, str_repeat('a', 1000), '/tmp/foo', 10)); + + $files = $fileModel->getAll(1); + $this->assertNotEmpty($files); + $this->assertCount(2, $files); + + $this->assertEquals(str_repeat('a', 255), $files[0]['name']); + $this->assertEquals('test', $files[1]['name']); + } + + public function testCreationWithSessionOpen() + { + $this->container['sessionStorage']->user = array('id' => 1); + + $projectModel = new ProjectModel($this->container); + $fileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals(1, $file['user_id']); + } + + public function testGetAll() + { + $projectModel = new ProjectModel($this->container); + $fileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertEquals(1, $fileModel->create(1, 'B.pdf', '/tmp/foo', 10)); + $this->assertEquals(2, $fileModel->create(1, 'A.png', '/tmp/foo', 10)); + $this->assertEquals(3, $fileModel->create(1, 'D.doc', '/tmp/foo', 10)); + $this->assertEquals(4, $fileModel->create(1, 'C.JPG', '/tmp/foo', 10)); + + $fileModeliles = $fileModel->getAll(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(4, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('B.pdf', $fileModeliles[1]['name']); + $this->assertEquals('C.JPG', $fileModeliles[2]['name']); + $this->assertEquals('D.doc', $fileModeliles[3]['name']); + + $fileModeliles = $fileModel->getAllImages(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('C.JPG', $fileModeliles[1]['name']); + + $fileModeliles = $fileModel->getAllDocuments(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('B.pdf', $fileModeliles[0]['name']); + $this->assertEquals('D.doc', $fileModeliles[1]['name']); + } + + public function testIsImage() + { + $fileModel = new TaskFileModel($this->container); + + $this->assertTrue($fileModel->isImage('test.png')); + $this->assertTrue($fileModel->isImage('test.jpeg')); + $this->assertTrue($fileModel->isImage('test.gif')); + $this->assertTrue($fileModel->isImage('test.jpg')); + $this->assertTrue($fileModel->isImage('test.JPG')); + + $this->assertFalse($fileModel->isImage('test.bmp')); + $this->assertFalse($fileModel->isImage('test')); + $this->assertFalse($fileModel->isImage('test.pdf')); + } + + public function testGetThumbnailPath() + { + $fileModel = new TaskFileModel($this->container); + $this->assertEquals('thumbnails'.DIRECTORY_SEPARATOR.'test', $fileModel->getThumbnailPath('test')); + } + + public function testGeneratePath() + { + $fileModel = new TaskFileModel($this->container); + + $this->assertStringStartsWith('tasks'.DIRECTORY_SEPARATOR.'34'.DIRECTORY_SEPARATOR, $fileModel->generatePath(34, 'test.png')); + $this->assertNotEquals($fileModel->generatePath(34, 'test1.png'), $fileModel->generatePath(34, 'test2.png')); + } + + public function testUploadFiles() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFileModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromFile'); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()); + + $this->container['objectStorage'] + ->expects($this->at(1)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpeEwEWG'), $this->anything()); + + $this->assertTrue($fileModel->uploadFiles(1, $files)); + + $files = $fileModel->getAll(1); + $this->assertCount(2, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('file1.png', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(123, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + + $this->assertEquals(2, $files[1]['id']); + $this->assertEquals('file2.doc', $files[1]['name']); + $this->assertEquals(0, $files[1]['is_image']); + $this->assertEquals(1, $files[1]['task_id']); + $this->assertEquals(0, $files[1]['user_id']); + $this->assertEquals(456, $files[1]['size']); + $this->assertEquals(time(), $files[1]['date'], '', 2); + } + + public function testUploadFilesWithEmptyFiles() + { + $fileModel = new TaskFileModel($this->container); + $this->assertFalse($fileModel->uploadFiles(1, array())); + } + + public function testUploadFilesWithUploadError() + { + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_CANT_WRITE, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel = new TaskFileModel($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFilesWithObjectStorageError() + { + $files = array( + 'name' => array( + 'file1.csv', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()) + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $fileModel = new TaskFileModel($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFileContent() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFileModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadContent(1, 'test.doc', base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('test.doc', $files[0]['name']); + $this->assertEquals(0, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } + + public function testUploadFileContentWithObjectStorageError() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFileModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)) + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $this->assertFalse($fileModel->uploadContent(1, 'test.doc', base64_encode($data))); + } + + public function testUploadScreenshot() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFileModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromData')) + ->getMock(); + + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromData'); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadScreenshot(1, base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertStringStartsWith('Screenshot taken ', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } + + public function testRemove() + { + $fileModel = new TaskFileModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('remove') + ->with('tmp/foo'); + + $this->assertTrue($fileModel->remove(1)); + } + + public function testRemoveWithObjectStorageError() + { + $fileModel = new TaskFileModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('remove') + ->with('tmp/foo') + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $this->assertFalse($fileModel->remove(1)); + } + + public function testRemoveImage() + { + $fileModel = new TaskFileModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'image.gif', 'tmp/image.gif', 10)); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('remove') + ->with('tmp/image.gif'); + + $this->container['objectStorage'] + ->expects($this->at(1)) + ->method('remove') + ->with('thumbnails'.DIRECTORY_SEPARATOR.'tmp/image.gif'); + + $this->assertTrue($fileModel->remove(1)); + } + + public function testRemoveAll() + { + $fileModel = new TaskFileModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + $this->assertEquals(2, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->exactly(2)) + ->method('remove') + ->with('tmp/foo'); + + $this->assertTrue($fileModel->removeAll(1)); + } + + public function testGetProjectId() + { + $projectModel = new ProjectModel($this->container); + $fileModel = new TaskFileModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foobar', 10)); + $this->assertEquals(1, $fileModel->getProjectId(1)); + $this->assertEquals(0, $fileModel->getProjectId(2)); + } +} diff --git a/tests/units/Model/TaskFileTest.php b/tests/units/Model/TaskFileTest.php deleted file mode 100644 index 2faee95c..00000000 --- a/tests/units/Model/TaskFileTest.php +++ /dev/null @@ -1,445 +0,0 @@ -container); - $fileModel = new TaskFileModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); - - $file = $fileModel->getById(1); - $this->assertEquals('test', $file['name']); - $this->assertEquals('/tmp/foo', $file['path']); - $this->assertEquals(0, $file['is_image']); - $this->assertEquals(1, $file['task_id']); - $this->assertEquals(time(), $file['date'], '', 2); - $this->assertEquals(0, $file['user_id']); - $this->assertEquals(10, $file['size']); - - $this->assertEquals(2, $fileModel->create(1, 'test2.png', '/tmp/foobar', 10)); - - $file = $fileModel->getById(2); - $this->assertEquals('test2.png', $file['name']); - $this->assertEquals('/tmp/foobar', $file['path']); - $this->assertEquals(1, $file['is_image']); - } - - public function testCreationWithFileNameTooLong() - { - $projectModel = new ProjectModel($this->container); - $fileModel = new TaskFileModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertNotFalse($fileModel->create(1, 'test', '/tmp/foo', 10)); - $this->assertNotFalse($fileModel->create(1, str_repeat('a', 1000), '/tmp/foo', 10)); - - $files = $fileModel->getAll(1); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - - $this->assertEquals(str_repeat('a', 255), $files[0]['name']); - $this->assertEquals('test', $files[1]['name']); - } - - public function testCreationWithSessionOpen() - { - $this->container['sessionStorage']->user = array('id' => 1); - - $projectModel = new ProjectModel($this->container); - $fileModel = new TaskFileModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); - - $file = $fileModel->getById(1); - $this->assertEquals('test', $file['name']); - $this->assertEquals(1, $file['user_id']); - } - - public function testGetAll() - { - $projectModel = new ProjectModel($this->container); - $fileModel = new TaskFileModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertEquals(1, $fileModel->create(1, 'B.pdf', '/tmp/foo', 10)); - $this->assertEquals(2, $fileModel->create(1, 'A.png', '/tmp/foo', 10)); - $this->assertEquals(3, $fileModel->create(1, 'D.doc', '/tmp/foo', 10)); - $this->assertEquals(4, $fileModel->create(1, 'C.JPG', '/tmp/foo', 10)); - - $fileModeliles = $fileModel->getAll(1); - $this->assertNotEmpty($fileModeliles); - $this->assertCount(4, $fileModeliles); - $this->assertEquals('A.png', $fileModeliles[0]['name']); - $this->assertEquals('B.pdf', $fileModeliles[1]['name']); - $this->assertEquals('C.JPG', $fileModeliles[2]['name']); - $this->assertEquals('D.doc', $fileModeliles[3]['name']); - - $fileModeliles = $fileModel->getAllImages(1); - $this->assertNotEmpty($fileModeliles); - $this->assertCount(2, $fileModeliles); - $this->assertEquals('A.png', $fileModeliles[0]['name']); - $this->assertEquals('C.JPG', $fileModeliles[1]['name']); - - $fileModeliles = $fileModel->getAllDocuments(1); - $this->assertNotEmpty($fileModeliles); - $this->assertCount(2, $fileModeliles); - $this->assertEquals('B.pdf', $fileModeliles[0]['name']); - $this->assertEquals('D.doc', $fileModeliles[1]['name']); - } - - public function testIsImage() - { - $fileModel = new TaskFileModel($this->container); - - $this->assertTrue($fileModel->isImage('test.png')); - $this->assertTrue($fileModel->isImage('test.jpeg')); - $this->assertTrue($fileModel->isImage('test.gif')); - $this->assertTrue($fileModel->isImage('test.jpg')); - $this->assertTrue($fileModel->isImage('test.JPG')); - - $this->assertFalse($fileModel->isImage('test.bmp')); - $this->assertFalse($fileModel->isImage('test')); - $this->assertFalse($fileModel->isImage('test.pdf')); - } - - public function testGetThumbnailPath() - { - $fileModel = new TaskFileModel($this->container); - $this->assertEquals('thumbnails'.DIRECTORY_SEPARATOR.'test', $fileModel->getThumbnailPath('test')); - } - - public function testGeneratePath() - { - $fileModel = new TaskFileModel($this->container); - - $this->assertStringStartsWith('tasks'.DIRECTORY_SEPARATOR.'34'.DIRECTORY_SEPARATOR, $fileModel->generatePath(34, 'test.png')); - $this->assertNotEquals($fileModel->generatePath(34, 'test1.png'), $fileModel->generatePath(34, 'test2.png')); - } - - public function testUploadFiles() - { - $fileModel = $this - ->getMockBuilder('\Kanboard\Model\TaskFileModel') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) - ->getMock(); - - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $files = array( - 'name' => array( - 'file1.png', - 'file2.doc', - ), - 'tmp_name' => array( - '/tmp/phpYzdqkD', - '/tmp/phpeEwEWG', - ), - 'error' => array( - UPLOAD_ERR_OK, - UPLOAD_ERR_OK, - ), - 'size' => array( - 123, - 456, - ), - ); - - $fileModel - ->expects($this->once()) - ->method('generateThumbnailFromFile'); - - $this->container['objectStorage'] - ->expects($this->at(0)) - ->method('moveUploadedFile') - ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()); - - $this->container['objectStorage'] - ->expects($this->at(1)) - ->method('moveUploadedFile') - ->with($this->equalTo('/tmp/phpeEwEWG'), $this->anything()); - - $this->assertTrue($fileModel->uploadFiles(1, $files)); - - $files = $fileModel->getAll(1); - $this->assertCount(2, $files); - - $this->assertEquals(1, $files[0]['id']); - $this->assertEquals('file1.png', $files[0]['name']); - $this->assertEquals(1, $files[0]['is_image']); - $this->assertEquals(1, $files[0]['task_id']); - $this->assertEquals(0, $files[0]['user_id']); - $this->assertEquals(123, $files[0]['size']); - $this->assertEquals(time(), $files[0]['date'], '', 2); - - $this->assertEquals(2, $files[1]['id']); - $this->assertEquals('file2.doc', $files[1]['name']); - $this->assertEquals(0, $files[1]['is_image']); - $this->assertEquals(1, $files[1]['task_id']); - $this->assertEquals(0, $files[1]['user_id']); - $this->assertEquals(456, $files[1]['size']); - $this->assertEquals(time(), $files[1]['date'], '', 2); - } - - public function testUploadFilesWithEmptyFiles() - { - $fileModel = new TaskFileModel($this->container); - $this->assertFalse($fileModel->uploadFiles(1, array())); - } - - public function testUploadFilesWithUploadError() - { - $files = array( - 'name' => array( - 'file1.png', - 'file2.doc', - ), - 'tmp_name' => array( - '', - '/tmp/phpeEwEWG', - ), - 'error' => array( - UPLOAD_ERR_CANT_WRITE, - UPLOAD_ERR_OK, - ), - 'size' => array( - 123, - 456, - ), - ); - - $fileModel = new TaskFileModel($this->container); - $this->assertFalse($fileModel->uploadFiles(1, $files)); - } - - public function testUploadFilesWithObjectStorageError() - { - $files = array( - 'name' => array( - 'file1.csv', - 'file2.doc', - ), - 'tmp_name' => array( - '/tmp/phpYzdqkD', - '/tmp/phpeEwEWG', - ), - 'error' => array( - UPLOAD_ERR_OK, - UPLOAD_ERR_OK, - ), - 'size' => array( - 123, - 456, - ), - ); - - $this->container['objectStorage'] - ->expects($this->at(0)) - ->method('moveUploadedFile') - ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()) - ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); - - $fileModel = new TaskFileModel($this->container); - $this->assertFalse($fileModel->uploadFiles(1, $files)); - } - - public function testUploadFileContent() - { - $fileModel = $this - ->getMockBuilder('\Kanboard\Model\TaskFileModel') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) - ->getMock(); - - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - $data = 'test'; - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('put') - ->with($this->anything(), $this->equalTo($data)); - - $this->assertEquals(1, $fileModel->uploadContent(1, 'test.doc', base64_encode($data))); - - $files = $fileModel->getAll(1); - $this->assertCount(1, $files); - - $this->assertEquals(1, $files[0]['id']); - $this->assertEquals('test.doc', $files[0]['name']); - $this->assertEquals(0, $files[0]['is_image']); - $this->assertEquals(1, $files[0]['task_id']); - $this->assertEquals(0, $files[0]['user_id']); - $this->assertEquals(4, $files[0]['size']); - $this->assertEquals(time(), $files[0]['date'], '', 2); - } - - public function testUploadFileContentWithObjectStorageError() - { - $fileModel = $this - ->getMockBuilder('\Kanboard\Model\TaskFileModel') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) - ->getMock(); - - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - $data = 'test'; - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('put') - ->with($this->anything(), $this->equalTo($data)) - ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); - - $this->assertFalse($fileModel->uploadContent(1, 'test.doc', base64_encode($data))); - } - - public function testUploadScreenshot() - { - $fileModel = $this - ->getMockBuilder('\Kanboard\Model\TaskFileModel') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromData')) - ->getMock(); - - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - $data = 'test'; - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $fileModel - ->expects($this->once()) - ->method('generateThumbnailFromData'); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('put') - ->with($this->anything(), $this->equalTo($data)); - - $this->assertEquals(1, $fileModel->uploadScreenshot(1, base64_encode($data))); - - $files = $fileModel->getAll(1); - $this->assertCount(1, $files); - - $this->assertEquals(1, $files[0]['id']); - $this->assertStringStartsWith('Screenshot taken ', $files[0]['name']); - $this->assertEquals(1, $files[0]['is_image']); - $this->assertEquals(1, $files[0]['task_id']); - $this->assertEquals(0, $files[0]['user_id']); - $this->assertEquals(4, $files[0]['size']); - $this->assertEquals(time(), $files[0]['date'], '', 2); - } - - public function testRemove() - { - $fileModel = new TaskFileModel($this->container); - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('remove') - ->with('tmp/foo'); - - $this->assertTrue($fileModel->remove(1)); - } - - public function testRemoveWithObjectStorageError() - { - $fileModel = new TaskFileModel($this->container); - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('remove') - ->with('tmp/foo') - ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); - - $this->assertFalse($fileModel->remove(1)); - } - - public function testRemoveImage() - { - $fileModel = new TaskFileModel($this->container); - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertEquals(1, $fileModel->create(1, 'image.gif', 'tmp/image.gif', 10)); - - $this->container['objectStorage'] - ->expects($this->at(0)) - ->method('remove') - ->with('tmp/image.gif'); - - $this->container['objectStorage'] - ->expects($this->at(1)) - ->method('remove') - ->with('thumbnails'.DIRECTORY_SEPARATOR.'tmp/image.gif'); - - $this->assertTrue($fileModel->remove(1)); - } - - public function testRemoveAll() - { - $fileModel = new TaskFileModel($this->container); - $projectModel = new ProjectModel($this->container); - $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); - $this->assertEquals(2, $fileModel->create(1, 'test', 'tmp/foo', 10)); - - $this->container['objectStorage'] - ->expects($this->exactly(2)) - ->method('remove') - ->with('tmp/foo'); - - $this->assertTrue($fileModel->removeAll(1)); - } -} diff --git a/tests/units/Model/TaskLinkModelTest.php b/tests/units/Model/TaskLinkModelTest.php new file mode 100644 index 00000000..78590891 --- /dev/null +++ b/tests/units/Model/TaskLinkModelTest.php @@ -0,0 +1,211 @@ +container); + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'C'))); + + $this->assertNotFalse($taskLinkModel->create(1, 2, 9)); + $this->assertNotFalse($taskLinkModel->create(1, 3, 9)); + + $task = $taskFinderModel->getExtendedQuery()->findOne(); + $this->assertNotEmpty($task); + } + + public function testCreateTaskLinkWithNoOpposite() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $links = $taskLinkModel->getAll(1); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('relates to', $links[0]['label']); + $this->assertEquals('B', $links[0]['title']); + $this->assertEquals(2, $links[0]['task_id']); + $this->assertEquals(1, $links[0]['is_active']); + + $links = $taskLinkModel->getAll(2); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('relates to', $links[0]['label']); + $this->assertEquals('A', $links[0]['title']); + $this->assertEquals(1, $links[0]['task_id']); + $this->assertEquals(1, $links[0]['is_active']); + + $task_link = $taskLinkModel->getById(1); + $this->assertNotEmpty($task_link); + $this->assertEquals(1, $task_link['id']); + $this->assertEquals(1, $task_link['task_id']); + $this->assertEquals(2, $task_link['opposite_task_id']); + $this->assertEquals(1, $task_link['link_id']); + + $opposite_task_link = $taskLinkModel->getOppositeTaskLink($task_link); + $this->assertNotEmpty($opposite_task_link); + $this->assertEquals(2, $opposite_task_link['id']); + $this->assertEquals(2, $opposite_task_link['task_id']); + $this->assertEquals(1, $opposite_task_link['opposite_task_id']); + $this->assertEquals(1, $opposite_task_link['link_id']); + } + + public function testCreateTaskLinkWithOpposite() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); + + $links = $taskLinkModel->getAll(1); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('blocks', $links[0]['label']); + $this->assertEquals('B', $links[0]['title']); + $this->assertEquals(2, $links[0]['task_id']); + $this->assertEquals(1, $links[0]['is_active']); + + $links = $taskLinkModel->getAll(2); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('is blocked by', $links[0]['label']); + $this->assertEquals('A', $links[0]['title']); + $this->assertEquals(1, $links[0]['task_id']); + $this->assertEquals(1, $links[0]['is_active']); + + $task_link = $taskLinkModel->getById(1); + $this->assertNotEmpty($task_link); + $this->assertEquals(1, $task_link['id']); + $this->assertEquals(1, $task_link['task_id']); + $this->assertEquals(2, $task_link['opposite_task_id']); + $this->assertEquals(2, $task_link['link_id']); + + $opposite_task_link = $taskLinkModel->getOppositeTaskLink($task_link); + $this->assertNotEmpty($opposite_task_link); + $this->assertEquals(2, $opposite_task_link['id']); + $this->assertEquals(2, $opposite_task_link['task_id']); + $this->assertEquals(1, $opposite_task_link['opposite_task_id']); + $this->assertEquals(3, $opposite_task_link['link_id']); + } + + public function testGroupByLabel() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'C'))); + + $this->assertNotFalse($taskLinkModel->create(1, 2, 2)); + $this->assertNotFalse($taskLinkModel->create(1, 3, 2)); + + $links = $taskLinkModel->getAllGroupedByLabel(1); + $this->assertCount(1, $links); + $this->assertArrayHasKey('blocks', $links); + $this->assertCount(2, $links['blocks']); + $this->assertEquals('test', $links['blocks'][0]['project_name']); + $this->assertEquals('Backlog', $links['blocks'][0]['column_title']); + $this->assertEquals('blocks', $links['blocks'][0]['label']); + } + + public function testUpdate() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 2, 'title' => 'B'))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'C'))); + + $this->assertEquals(1, $taskLinkModel->create(1, 2, 5)); + $this->assertTrue($taskLinkModel->update(1, 1, 3, 11)); + + $links = $taskLinkModel->getAll(1); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('is fixed by', $links[0]['label']); + $this->assertEquals('C', $links[0]['title']); + $this->assertEquals(3, $links[0]['task_id']); + + $links = $taskLinkModel->getAll(2); + $this->assertEmpty($links); + + $links = $taskLinkModel->getAll(3); + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + $this->assertEquals('fixes', $links[0]['label']); + $this->assertEquals('A', $links[0]['title']); + $this->assertEquals(1, $links[0]['task_id']); + } + + public function testRemove() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); + + $links = $taskLinkModel->getAll(1); + $this->assertNotEmpty($links); + $links = $taskLinkModel->getAll(2); + $this->assertNotEmpty($links); + + $this->assertTrue($taskLinkModel->remove($links[0]['id'])); + + $links = $taskLinkModel->getAll(1); + $this->assertEmpty($links); + $links = $taskLinkModel->getAll(2); + $this->assertEmpty($links); + } + + public function testGetProjectId() + { + $taskLinkModel = new TaskLinkModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'A'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'B'))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); + + $this->assertEquals(1, $taskLinkModel->getProjectId(1)); + $this->assertEquals(0, $taskLinkModel->getProjectId(42)); + } +} diff --git a/tests/units/Model/TaskLinkTest.php b/tests/units/Model/TaskLinkTest.php deleted file mode 100644 index bc574731..00000000 --- a/tests/units/Model/TaskLinkTest.php +++ /dev/null @@ -1,196 +0,0 @@ -container); - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'B'))); - $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'C'))); - - $this->assertNotFalse($tl->create(1, 2, 9)); - $this->assertNotFalse($tl->create(1, 3, 9)); - - $task = $tf->getExtendedQuery()->findOne(); - $this->assertNotEmpty($task); - } - - public function testCreateTaskLinkWithNoOpposite() - { - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'B'))); - $this->assertEquals(1, $tl->create(1, 2, 1)); - - $links = $tl->getAll(1); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('relates to', $links[0]['label']); - $this->assertEquals('B', $links[0]['title']); - $this->assertEquals(2, $links[0]['task_id']); - $this->assertEquals(1, $links[0]['is_active']); - - $links = $tl->getAll(2); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('relates to', $links[0]['label']); - $this->assertEquals('A', $links[0]['title']); - $this->assertEquals(1, $links[0]['task_id']); - $this->assertEquals(1, $links[0]['is_active']); - - $task_link = $tl->getById(1); - $this->assertNotEmpty($task_link); - $this->assertEquals(1, $task_link['id']); - $this->assertEquals(1, $task_link['task_id']); - $this->assertEquals(2, $task_link['opposite_task_id']); - $this->assertEquals(1, $task_link['link_id']); - - $opposite_task_link = $tl->getOppositeTaskLink($task_link); - $this->assertNotEmpty($opposite_task_link); - $this->assertEquals(2, $opposite_task_link['id']); - $this->assertEquals(2, $opposite_task_link['task_id']); - $this->assertEquals(1, $opposite_task_link['opposite_task_id']); - $this->assertEquals(1, $opposite_task_link['link_id']); - } - - public function testCreateTaskLinkWithOpposite() - { - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'B'))); - $this->assertEquals(1, $tl->create(1, 2, 2)); - - $links = $tl->getAll(1); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('blocks', $links[0]['label']); - $this->assertEquals('B', $links[0]['title']); - $this->assertEquals(2, $links[0]['task_id']); - $this->assertEquals(1, $links[0]['is_active']); - - $links = $tl->getAll(2); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('is blocked by', $links[0]['label']); - $this->assertEquals('A', $links[0]['title']); - $this->assertEquals(1, $links[0]['task_id']); - $this->assertEquals(1, $links[0]['is_active']); - - $task_link = $tl->getById(1); - $this->assertNotEmpty($task_link); - $this->assertEquals(1, $task_link['id']); - $this->assertEquals(1, $task_link['task_id']); - $this->assertEquals(2, $task_link['opposite_task_id']); - $this->assertEquals(2, $task_link['link_id']); - - $opposite_task_link = $tl->getOppositeTaskLink($task_link); - $this->assertNotEmpty($opposite_task_link); - $this->assertEquals(2, $opposite_task_link['id']); - $this->assertEquals(2, $opposite_task_link['task_id']); - $this->assertEquals(1, $opposite_task_link['opposite_task_id']); - $this->assertEquals(3, $opposite_task_link['link_id']); - } - - public function testGroupByLabel() - { - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'B'))); - $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'C'))); - - $this->assertNotFalse($tl->create(1, 2, 2)); - $this->assertNotFalse($tl->create(1, 3, 2)); - - $links = $tl->getAllGroupedByLabel(1); - $this->assertCount(1, $links); - $this->assertArrayHasKey('blocks', $links); - $this->assertCount(2, $links['blocks']); - $this->assertEquals('test', $links['blocks'][0]['project_name']); - $this->assertEquals('Backlog', $links['blocks'][0]['column_title']); - $this->assertEquals('blocks', $links['blocks'][0]['label']); - } - - public function testUpdate() - { - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(2, $p->create(array('name' => 'test2'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 2, 'title' => 'B'))); - $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'C'))); - - $this->assertEquals(1, $tl->create(1, 2, 5)); - $this->assertTrue($tl->update(1, 1, 3, 11)); - - $links = $tl->getAll(1); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('is fixed by', $links[0]['label']); - $this->assertEquals('C', $links[0]['title']); - $this->assertEquals(3, $links[0]['task_id']); - - $links = $tl->getAll(2); - $this->assertEmpty($links); - - $links = $tl->getAll(3); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - $this->assertEquals('fixes', $links[0]['label']); - $this->assertEquals('A', $links[0]['title']); - $this->assertEquals(1, $links[0]['task_id']); - } - - public function testRemove() - { - $tl = new TaskLinkModel($this->container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'A'))); - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'B'))); - $this->assertEquals(1, $tl->create(1, 2, 2)); - - $links = $tl->getAll(1); - $this->assertNotEmpty($links); - $links = $tl->getAll(2); - $this->assertNotEmpty($links); - - $this->assertTrue($tl->remove($links[0]['id'])); - - $links = $tl->getAll(1); - $this->assertEmpty($links); - $links = $tl->getAll(2); - $this->assertEmpty($links); - } -} -- cgit v1.2.3 From b48c0cecbb1f687641594430260a67938d870cbb Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 11:57:28 -0400 Subject: Added new arguments to project API calls and update composer.json --- ChangeLog | 1 + app/Api/Procedure/BaseProcedure.php | 1 - app/Api/Procedure/ProjectProcedure.php | 6 +- app/Validator/ProjectValidator.php | 8 +- composer.json | 31 +-- composer.lock | 297 +++++++------------------ doc/api-project-procedures.markdown | 2 +- doc/requirements.markdown | 2 +- tests/integration/ProjectProcedureTest.php | 29 +++ tests/units/Validator/ProjectValidatorTest.php | 12 +- 10 files changed, 143 insertions(+), 246 deletions(-) (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 42af8ee3..550a7ada 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,7 @@ New features: Improvements: +* Added argument owner_id and identifier to project API calls * Rewrite integration tests to run with Docker containers * Use the same task form layout everywhere * Remove some tasks dropdown menus that are now available with task edit form diff --git a/app/Api/Procedure/BaseProcedure.php b/app/Api/Procedure/BaseProcedure.php index 0aa43428..e31b3027 100644 --- a/app/Api/Procedure/BaseProcedure.php +++ b/app/Api/Procedure/BaseProcedure.php @@ -2,7 +2,6 @@ namespace Kanboard\Api\Procedure; -use JsonRPC\Exception\AccessDeniedException; use Kanboard\Api\Authorization\ProcedureAuthorization; use Kanboard\Api\Authorization\UserAuthorization; use Kanboard\Core\Base; diff --git a/app/Api/Procedure/ProjectProcedure.php b/app/Api/Procedure/ProjectProcedure.php index 9187f221..fe6b63e2 100644 --- a/app/Api/Procedure/ProjectProcedure.php +++ b/app/Api/Procedure/ProjectProcedure.php @@ -78,17 +78,17 @@ class ProjectProcedure extends BaseProcedure public function createProject($name, $description = null, $owner_id = 0, $identifier = null) { - $values = array( + $values = $this->filterValues(array( 'name' => $name, 'description' => $description, 'identifier' => $identifier, - ); + )); list($valid, ) = $this->projectValidator->validateCreation($values); return $valid ? $this->projectModel->create($values, $owner_id, $this->userSession->isLogged()) : false; } - public function updateProject($project_id, $name, $description = null, $owner_id = null, $identifier = null) + public function updateProject($project_id, $name = null, $description = null, $owner_id = null, $identifier = null) { ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateProject', $project_id); diff --git a/app/Validator/ProjectValidator.php b/app/Validator/ProjectValidator.php index 9ef59111..8c6117a4 100644 --- a/app/Validator/ProjectValidator.php +++ b/app/Validator/ProjectValidator.php @@ -28,7 +28,7 @@ class ProjectValidator extends BaseValidator new Validators\Integer('priority_start', t('This value must be an integer')), new Validators\Integer('priority_end', t('This value must be an integer')), new Validators\Integer('is_active', t('This value must be an integer')), - new Validators\Required('name', t('The project name is required')), + new Validators\NotEmpty('name', t('This field cannot be empty')), new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50), new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10), @@ -47,11 +47,15 @@ class ProjectValidator extends BaseValidator */ public function validateCreation(array $values) { + $rules = array( + new Validators\Required('name', t('The project name is required')), + ); + if (! empty($values['identifier'])) { $values['identifier'] = strtoupper($values['identifier']); } - $v = new Validator($values, $this->commonValidationRules()); + $v = new Validator($values, array_merge($this->commonValidationRules(), $rules)); return array( $v->execute(), diff --git a/composer.json b/composer.json index bcac020e..d82f3f0c 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "discard-changes": true }, "require" : { - "php" : ">=5.3.3", + "php" : ">=5.3.9", "ext-gd" : "*", "ext-mbstring" : "*", "ext-hash" : "*", @@ -23,21 +23,21 @@ "ext-ctype" : "*", "ext-filter" : "*", "ext-session" : "*", - "christian-riesen/otp" : "1.4", - "eluceo/ical": "0.8.0", + "christian-riesen/otp" : "1.4.3", + "eluceo/ical": "0.10.1", "erusev/parsedown" : "1.6.0", "fguillot/json-rpc" : "1.2.1", "fguillot/picodb" : "1.0.12", "fguillot/simpleLogger" : "1.0.1", - "fguillot/simple-validator" : "1.0.0", + "fguillot/simple-validator" : "1.0.1", "fguillot/simple-queue" : "1.0.1", - "paragonie/random_compat": "@stable", - "pimple/pimple" : "~3.0", - "ramsey/array_column": "@stable", - "swiftmailer/swiftmailer" : "~5.4", - "symfony/console" : "~2.7", - "symfony/event-dispatcher" : "~2.7", - "gregwar/captcha": "1.*" + "paragonie/random_compat": "2.0.2", + "pimple/pimple" : "3.0.2", + "ramsey/array_column": "1.1.3", + "swiftmailer/swiftmailer" : "5.4.2", + "symfony/console" : "2.8.7", + "symfony/event-dispatcher" : "2.7.14", + "gregwar/captcha": "1.1.1" }, "autoload" : { "classmap" : ["app/"], @@ -50,9 +50,10 @@ ] }, "require-dev" : { - "symfony/yaml" : "2.1", - "symfony/stopwatch" : "~2.6", - "phpunit/phpunit" : "4.8.*", - "phpunit/phpunit-selenium": "^2.0" + "phpdocumentor/reflection-docblock": "2.0.4", + "symfony/yaml": "2.8.7", + "symfony/stopwatch" : "2.6.13", + "phpunit/phpunit" : "4.8.26", + "phpunit/phpunit-selenium": "2.0.2" } } diff --git a/composer.lock b/composer.lock index e6a72582..03c5e523 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "283af0b856598f5bc3d8ee0b226959e5", - "content-hash": "18c0bbff5406ceb8b567d9655de26746", + "hash": "ab5b2c960b3a6d9f93883606269085e0", + "content-hash": "bd5f17c3382d7f85e33a68023927704c", "packages": [ { "name": "christian-riesen/base32", @@ -63,22 +63,25 @@ }, { "name": "christian-riesen/otp", - "version": "1.4", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/ChristianRiesen/otp.git", - "reference": "a209b8bbd975d96d6b5287f8658562061adef1f8" + "reference": "20a539ce6280eb029030f4e7caefd5709a75e1ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ChristianRiesen/otp/zipball/a209b8bbd975d96d6b5287f8658562061adef1f8", - "reference": "a209b8bbd975d96d6b5287f8658562061adef1f8", + "url": "https://api.github.com/repos/ChristianRiesen/otp/zipball/20a539ce6280eb029030f4e7caefd5709a75e1ad", + "reference": "20a539ce6280eb029030f4e7caefd5709a75e1ad", "shasum": "" }, "require": { "christian-riesen/base32": ">=1.0", "php": ">=5.3.0" }, + "suggest": { + "paragonie/random_compat": "Optional polyfill for a more secure random generator for pre PHP7 versions" + }, "type": "library", "autoload": { "psr-0": { @@ -107,20 +110,20 @@ "rfc6238", "totp" ], - "time": "2015-02-12 09:11:49" + "time": "2015-10-08 08:17:59" }, { "name": "eluceo/ical", - "version": "0.8.0", + "version": "0.10.1", "source": { "type": "git", "url": "https://github.com/markuspoerschke/iCal.git", - "reference": "a291711851d1538e2726ffe95862aa5e340ddb9a" + "reference": "2dd99c12c0aa961c541380ab0c113135e14af33e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/a291711851d1538e2726ffe95862aa5e340ddb9a", - "reference": "a291711851d1538e2726ffe95862aa5e340ddb9a", + "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/2dd99c12c0aa961c541380ab0c113135e14af33e", + "reference": "2dd99c12c0aa961c541380ab0c113135e14af33e", "shasum": "" }, "require": { @@ -160,7 +163,7 @@ "ics", "php calendar" ], - "time": "2015-07-12 18:19:30" + "time": "2016-06-09 09:08:55" }, { "name": "erusev/parsedown", @@ -331,16 +334,16 @@ }, { "name": "fguillot/simple-validator", - "version": "1.0.0", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/fguillot/simpleValidator.git", - "reference": "9579993f3dd0f03053b28fec1e7b9990edc3947b" + "reference": "23b0a99c5f11ad74d05f8845feaafbcfd9223eda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/9579993f3dd0f03053b28fec1e7b9990edc3947b", - "reference": "9579993f3dd0f03053b28fec1e7b9990edc3947b", + "url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/23b0a99c5f11ad74d05f8845feaafbcfd9223eda", + "reference": "23b0a99c5f11ad74d05f8845feaafbcfd9223eda", "shasum": "" }, "require": { @@ -363,7 +366,7 @@ ], "description": "Simple validator library", "homepage": "https://github.com/fguillot/simpleValidator", - "time": "2015-08-29 00:44:37" + "time": "2016-06-26 15:09:26" }, { "name": "fguillot/simpleLogger", @@ -742,16 +745,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.7", + "version": "v2.7.14", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "2a6b8713f8bdb582058cfda463527f195b066110" + "reference": "d3e09ed1224503791f31b913d22196f65f9afed5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2a6b8713f8bdb582058cfda463527f195b066110", - "reference": "2a6b8713f8bdb582058cfda463527f195b066110", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d3e09ed1224503791f31b913d22196f65f9afed5", + "reference": "d3e09ed1224503791f31b913d22196f65f9afed5", "shasum": "" }, "require": { @@ -759,10 +762,10 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5|~3.0.0", - "symfony/dependency-injection": "~2.6|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/stopwatch": "~2.3|~3.0.0" + "symfony/config": "~2.0,>=2.0.5", + "symfony/dependency-injection": "~2.6", + "symfony/expression-language": "~2.6", + "symfony/stopwatch": "~2.3" }, "suggest": { "symfony/dependency-injection": "", @@ -771,7 +774,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.7-dev" } }, "autoload": { @@ -798,7 +801,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-06-06 11:11:27" + "time": "2016-06-06 11:03:51" }, { "name": "symfony/polyfill-mbstring", @@ -915,136 +918,39 @@ ], "time": "2015-06-14 21:17:01" }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2015-12-27 11:43:31" - }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.0", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9270140b940ff02e58ec577c237274e92cd40cdd" + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd", - "reference": "9270140b940ff02e58ec577c237274e92cd40cdd", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", - "webmozart/assert": "^1.0" + "php": ">=5.3.3" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-06-10 09:48:41" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0" + "phpunit/phpunit": "~4.0" }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ + "psr-0": { + "phpDocumentor": [ "src/" ] } @@ -1056,10 +962,10 @@ "authors": [ { "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "email": "mike.vanriel@naenius.com" } ], - "time": "2016-06-10 07:14:17" + "time": "2015-02-03 12:10:50" }, { "name": "phpspec/prophecy", @@ -1932,34 +1838,35 @@ }, { "name": "symfony/stopwatch", - "version": "v2.8.7", + "version": "v2.6.13", + "target-dir": "Symfony/Component/Stopwatch", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5e628055488bcc42dbace3af65be435d094e37e4" + "reference": "a0d91f2f4e2c60bd78f13388aa68f9d7cab8c987" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5e628055488bcc42dbace3af65be435d094e37e4", - "reference": "5e628055488bcc42dbace3af65be435d094e37e4", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/a0d91f2f4e2c60bd78f13388aa68f9d7cab8c987", + "reference": "a0d91f2f4e2c60bd78f13388aa68f9d7cab8c987", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.6-dev" } }, "autoload": { - "psr-4": { + "psr-0": { "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1977,115 +1884,65 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2016-06-06 11:11:27" + "time": "2015-07-01 18:23:01" }, { "name": "symfony/yaml", - "version": "v2.1.0", - "target-dir": "Symfony/Component/Yaml", + "version": "v2.8.7", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f18e004fc975707bb4695df1dbbe9b0d8c8b7715" + "reference": "815fabf3f48c7d1df345a69d1ad1a88f59757b34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f18e004fc975707bb4695df1dbbe9b0d8c8b7715", - "reference": "f18e004fc975707bb4695df1dbbe9b0d8c8b7715", + "url": "https://api.github.com/repos/symfony/yaml/zipball/815fabf3f48c7d1df345a69d1ad1a88f59757b34", + "reference": "815fabf3f48c7d1df345a69d1ad1a88f59757b34", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.8-dev" } }, "autoload": { - "psr-0": { - "Symfony\\Component\\Yaml": "" - } + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" - } - ], - "description": "Symfony Yaml Component", - "homepage": "http://symfony.com", - "time": "2012-08-22 13:48:41" - }, - { - "name": "webmozart/assert", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", - "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + }, { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2015-08-24 13:29:44" + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-06-06 11:11:27" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "paragonie/random_compat": 0, - "ramsey/array_column": 0 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.3.3", + "php": ">=5.3.9", "ext-gd": "*", "ext-mbstring": "*", "ext-hash": "*", diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown index 3f8d33c2..d375852c 100644 --- a/doc/api-project-procedures.markdown +++ b/doc/api-project-procedures.markdown @@ -186,7 +186,7 @@ Response example: - Purpose: **Update a project** - Parameters: - **project_id** (integer, required) - - **name** (string, required) + - **name** (string, optional) - **description** (string, optional) - **owner_id** (integer, optional) - **identifier** (string, optional) diff --git a/doc/requirements.markdown b/doc/requirements.markdown index 9943465a..f6c9b309 100644 --- a/doc/requirements.markdown +++ b/doc/requirements.markdown @@ -51,7 +51,7 @@ Kanboard is pre-configured to work with Apache (URL rewriting). | PHP Version | |----------------| -| PHP >= 5.3.3 | +| PHP >= 5.3.9 | | PHP 5.4 | | PHP 5.5 | | PHP 5.6 | diff --git a/tests/integration/ProjectProcedureTest.php b/tests/integration/ProjectProcedureTest.php index 1ebd48ae..a4b65241 100644 --- a/tests/integration/ProjectProcedureTest.php +++ b/tests/integration/ProjectProcedureTest.php @@ -13,6 +13,8 @@ class ProjectProcedureTest extends BaseProcedureTest $this->assertGetProjectByName(); $this->assertGetAllProjects(); $this->assertUpdateProject(); + $this->assertUpdateProjectIdentifier(); + $this->assertCreateProjectWithIdentifier(); $this->assertGetProjectActivity(); $this->assertGetProjectsActivity(); $this->assertEnableDisableProject(); @@ -69,6 +71,33 @@ class ProjectProcedureTest extends BaseProcedureTest $this->assertTrue($this->app->updateProject(array('project_id' => $this->projectId, 'name' => $this->projectName))); } + public function assertUpdateProjectIdentifier() + { + $this->assertTrue($this->app->updateProject(array( + 'project_id' => $this->projectId, + 'identifier' => 'MYPROJECT', + ))); + + $project = $this->app->getProjectById($this->projectId); + $this->assertNotNull($project); + $this->assertEquals($this->projectName, $project['name']); + $this->assertEquals('MYPROJECT', $project['identifier']); + } + + public function assertCreateProjectWithIdentifier() + { + $projectId = $this->app->createProject(array( + 'name' => 'My project with an identifier', + 'identifier' => 'MYPROJECTWITHIDENTIFIER', + )); + + $this->assertNotFalse($projectId); + + $project = $this->app->getProjectById($projectId); + $this->assertEquals('My project with an identifier', $project['name']); + $this->assertEquals('MYPROJECTWITHIDENTIFIER', $project['identifier']); + } + public function assertEnableDisableProject() { $this->assertTrue($this->app->disableProject($this->projectId)); diff --git a/tests/units/Validator/ProjectValidatorTest.php b/tests/units/Validator/ProjectValidatorTest.php index 07de6c25..e1e2f077 100644 --- a/tests/units/Validator/ProjectValidatorTest.php +++ b/tests/units/Validator/ProjectValidatorTest.php @@ -55,13 +55,19 @@ class ProjectValidatorTest extends Base $r = $validator->validateModification(array('id' => 1, 'name' => 'test', 'identifier' => 'TEST1')); $this->assertTrue($r[0]); - $r = $validator->validateModification(array('id' => 1, 'name' => 'test', 'identifier' => 'test3')); + $r = $validator->validateModification(array('id' => 1, 'identifier' => 'test3')); $this->assertTrue($r[0]); - $r = $validator->validateModification(array('id' => 1, 'name' => 'test', 'identifier' => '')); + $r = $validator->validateModification(array('id' => 1, 'identifier' => '')); $this->assertTrue($r[0]); - $r = $validator->validateModification(array('id' => 1, 'name' => 'test', 'identifier' => 'TEST2')); + $r = $validator->validateModification(array('id' => 1, 'identifier' => 'TEST2')); + $this->assertFalse($r[0]); + + $r = $validator->validateModification(array('id' => 1, 'name' => '')); + $this->assertFalse($r[0]); + + $r = $validator->validateModification(array('id' => 1, 'name' => null)); $this->assertFalse($r[0]); } } -- cgit v1.2.3 From b82589e75f58cb36280c3e872ed23d4cfc64128b Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 12:37:51 -0400 Subject: Update API documentation --- doc/api-project-procedures.markdown | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) (limited to 'doc') diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown index d375852c..09000e68 100644 --- a/doc/api-project-procedures.markdown +++ b/doc/api-project-procedures.markdown @@ -133,6 +133,55 @@ Response example: } ``` +## getProjectByIdentifier + +- Purpose: **Get project information** +- Parameters: + - **identifier** (string, required) +- Result on success: **project properties** +- Result on failure: **null** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getProjectByIdentifier", + "id": 1620253806, + "params": { + "identifier": "TEST" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1620253806, + "result": { + "id": "1", + "name": "Test", + "is_active": "1", + "token": "", + "last_modified": "1436119135", + "is_public": "0", + "is_private": "0", + "is_everybody_allowed": "0", + "default_swimlane": "Default swimlane", + "show_default_swimlane": "1", + "description": "test", + "identifier": "TEST", + "url": { + "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1", + "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1", + "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1" + } + } +} +``` + ## getAllProjects - Purpose: **Get all available projects** -- cgit v1.2.3 From 3d34681610854474cb9dbdd93886dbcf0e208a99 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 14:33:53 -0400 Subject: Added new API calls for external task links --- ChangeLog | 1 + app/Api/Procedure/TaskExternalLinkProcedure.php | 106 ++++++++++ app/Core/ExternalLink/ExternalLinkManager.php | 24 +++ app/ServiceProvider/ApiProvider.php | 2 + app/ServiceProvider/AuthenticationProvider.php | 1 + doc/api-external-task-link-procedures.markdown | 221 +++++++++++++++++++++ doc/api-internal-task-link-procedures.markdown | 187 +++++++++++++++++ doc/api-json-rpc.markdown | 2 + doc/api-link-procedures.markdown | 185 ----------------- .../integration/TaskExternalLinkProcedureTest.php | 98 +++++++++ 10 files changed, 642 insertions(+), 185 deletions(-) create mode 100644 app/Api/Procedure/TaskExternalLinkProcedure.php create mode 100644 doc/api-external-task-link-procedures.markdown create mode 100644 doc/api-internal-task-link-procedures.markdown create mode 100644 tests/integration/TaskExternalLinkProcedureTest.php (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 883cc6cf..85966ed8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,7 @@ New features: * Added application and project roles validation for API procedure calls * Added new API call: "getProjectByIdentifier" +* Added new API calls for external task links Improvements: 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 @@ +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/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 @@ -152,6 +152,30 @@ class ExternalLinkManager extends Base return $this; } + /** + * 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 * diff --git a/app/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php index f88d9b4f..194bee5b 100644 --- a/app/ServiceProvider/ApiProvider.php +++ b/app/ServiceProvider/ApiProvider.php @@ -9,6 +9,7 @@ use Kanboard\Api\Procedure\BoardProcedure; use Kanboard\Api\Procedure\CategoryProcedure; use Kanboard\Api\Procedure\ColumnProcedure; use Kanboard\Api\Procedure\CommentProcedure; +use Kanboard\Api\Procedure\TaskExternalLinkProcedure; use Kanboard\Api\Procedure\TaskFileProcedure; use Kanboard\Api\Procedure\GroupProcedure; use Kanboard\Api\Procedure\GroupMemberProcedure; @@ -65,6 +66,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..34b81b9d 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -204,6 +204,7 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('SwimlaneProcedure', '*', Role::PROJECT_MANAGER); $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/doc/api-external-task-link-procedures.markdown b/doc/api-external-task-link-procedures.markdown new file mode 100644 index 00000000..2858be86 --- /dev/null +++ b/doc/api-external-task-link-procedures.markdown @@ -0,0 +1,221 @@ +Internal Task Links API Procedures +================================== + +## getExternalTaskLinkTypes + +- Purpose: **Get all registered external link providers** +- Parameters: **none** +- Result on success: **dict** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"getExternalTaskLinkTypes","id":477370568} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": { + "auto": "Auto", + "attachment": "Attachment", + "file": "Local File", + "weblink": "Web Link" + }, + "id": 477370568 +} +``` + +## getExternalTaskLinkProviderDependencies + +- Purpose: **Get available dependencies for a given provider** +- Parameters: + - **providerName** (string, required) +- Result on success: **dict** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"getExternalTaskLinkProviderDependencies","id":124790226,"params":["weblink"]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": { + "related": "Related" + }, + "id": 124790226 +} +``` + +## createExternalTaskLink + +- Purpose: **Create a new external link** +- Parameters: + - **task_id** (integer, required) + - **url** (string, required) + - **dependency** (string, required) + - **type** (string, optional) + - **title** (string, optional) +- Result on success: **link_id** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"createExternalTaskLink","id":924217495,"params":[9,"http:\/\/localhost\/document.pdf","related","attachment"]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": 1, + "id": 924217495 +} +``` + +## updateExternalTaskLink + +- Purpose: **Update external task link** +- Parameters: + - **task_id** (integer, required) + - **link_id** (integer, required) + - **title** (string, required) + - **url** (string, required) + - **dependency** (string, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc":"2.0", + "method":"updateExternalTaskLink", + "id":1123562620, + "params": { + "task_id":9, + "link_id":1, + "title":"New title" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": true, + "id": 1123562620 +} +``` + +## getExternalTaskLinkById + +- Purpose: **Get an external task link** +- Parameters: + - **task_id** (integer, required) + - **link_id** (integer, required) +- Result on success: **dict** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"getExternalTaskLinkById","id":2107066744,"params":[9,1]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": { + "id": "1", + "link_type": "attachment", + "dependency": "related", + "title": "document.pdf", + "url": "http:\/\/localhost\/document.pdf", + "date_creation": "1466965256", + "date_modification": "1466965256", + "task_id": "9", + "creator_id": "0" + }, + "id": 2107066744 +} +``` + +## getAllExternalTaskLinks + +- Purpose: **Get all external links attached to a task** +- Parameters: + - **task_id** (integer, required) +- Result on success: **list of external links** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"getAllExternalTaskLinks","id":2069307223,"params":[9]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": [ + { + "id": "1", + "link_type": "attachment", + "dependency": "related", + "title": "New title", + "url": "http:\/\/localhost\/document.pdf", + "date_creation": "1466965256", + "date_modification": "1466965256", + "task_id": "9", + "creator_id": "0", + "creator_name": null, + "creator_username": null, + "dependency_label": "Related", + "type": "Attachment" + } + ], + "id": 2069307223 +} +``` + +## removeExternalTaskLink + +- Purpose: **Remove an external link** +- Parameters: + - **task_id** (integer, required) + - **link_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"removeExternalTaskLink","id":552055660,"params":[9,1]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": true, + "id": 552055660 +} +``` diff --git a/doc/api-internal-task-link-procedures.markdown b/doc/api-internal-task-link-procedures.markdown new file mode 100644 index 00000000..859228de --- /dev/null +++ b/doc/api-internal-task-link-procedures.markdown @@ -0,0 +1,187 @@ +Internal Task Links API Procedures +================================== + +## createTaskLink + +- Purpose: **Create a link between two tasks** +- Parameters: + - **task_id** (integer, required) + - **opposite_task_id** (integer, required) + - **link_id** (integer, required) +- Result on success: **task_link_id** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createTaskLink", + "id": 509742912, + "params": [ + 2, + 3, + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 509742912, + "result": 1 +} +``` + +## updateTaskLink + +- Purpose: **Update task link** +- Parameters: + - **task_link_id** (integer, required) + - **task_id** (integer, required) + - **opposite_task_id** (integer, required) + - **link_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateTaskLink", + "id": 669037109, + "params": [ + 1, + 2, + 4, + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 669037109, + "result": true +} +``` + +## getTaskLinkById + +- Purpose: **Get a task link** +- Parameters: + - **task_link_id** (integer, required) +- Result on success: **task link properties** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getTaskLinkById", + "id": 809885202, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 809885202, + "result": { + "id": "1", + "link_id": "1", + "task_id": "2", + "opposite_task_id": "3" + } +} +``` + +## getAllTaskLinks + +- Purpose: **Get all links related to a task** +- Parameters: + - **task_id** (integer, required) +- Result on success: **list of task link** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllTaskLinks", + "id": 810848359, + "params": [ + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 810848359, + "result": [ + { + "id": "1", + "task_id": "3", + "label": "relates to", + "title": "B", + "is_active": "1", + "project_id": "1", + "task_time_spent": "0", + "task_time_estimated": "0", + "task_assignee_id": "0", + "task_assignee_username": null, + "task_assignee_name": null, + "column_title": "Backlog" + } + ] +} +``` + +## removeTaskLink + +- Purpose: **Remove a link between two tasks** +- Parameters: + - **task_link_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeTaskLink", + "id": 473028226, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 473028226, + "result": true +} +``` diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown index 0f922a7c..8e783e71 100644 --- a/doc/api-json-rpc.markdown +++ b/doc/api-json-rpc.markdown @@ -60,6 +60,8 @@ Usage - [Subtasks](api-subtask-procedures.markdown) - [Files](api-file-procedures.markdown) - [Links](api-link-procedures.markdown) +- [Internal Task Links](api-internal-task-link-procedures.markdown) +- [External Task Links](api-external-task-link-procedures.markdown) - [Comments](api-comment-procedures.markdown) - [Users](api-user-procedures.markdown) - [Groups](api-group-procedures.markdown) diff --git a/doc/api-link-procedures.markdown b/doc/api-link-procedures.markdown index 6113316f..44e78a2a 100644 --- a/doc/api-link-procedures.markdown +++ b/doc/api-link-procedures.markdown @@ -283,188 +283,3 @@ Response example: "result": true } ``` - -## createTaskLink - -- Purpose: **Create a link between two tasks** -- Parameters: - - **task_id** (integer, required) - - **opposite_task_id** (integer, required) - - **link_id** (integer, required) -- Result on success: **task_link_id** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "createTaskLink", - "id": 509742912, - "params": [ - 2, - 3, - 1 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 509742912, - "result": 1 -} -``` - -## updateTaskLink - -- Purpose: **Update task link** -- Parameters: - - **task_link_id** (integer, required) - - **task_id** (integer, required) - - **opposite_task_id** (integer, required) - - **link_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "updateTaskLink", - "id": 669037109, - "params": [ - 1, - 2, - 4, - 2 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 669037109, - "result": true -} -``` - -## getTaskLinkById - -- Purpose: **Get a task link** -- Parameters: - - **task_link_id** (integer, required) -- Result on success: **task link properties** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getTaskLinkById", - "id": 809885202, - "params": [ - 1 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 809885202, - "result": { - "id": "1", - "link_id": "1", - "task_id": "2", - "opposite_task_id": "3" - } -} -``` - -## getAllTaskLinks - -- Purpose: **Get all links related to a task** -- Parameters: - - **task_id** (integer, required) -- Result on success: **list of task link** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getAllTaskLinks", - "id": 810848359, - "params": [ - 2 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 810848359, - "result": [ - { - "id": "1", - "task_id": "3", - "label": "relates to", - "title": "B", - "is_active": "1", - "project_id": "1", - "task_time_spent": "0", - "task_time_estimated": "0", - "task_assignee_id": "0", - "task_assignee_username": null, - "task_assignee_name": null, - "column_title": "Backlog" - } - ] -} -``` - -## removeTaskLink - -- Purpose: **Remove a link between two tasks** -- Parameters: - - **task_link_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "removeTaskLink", - "id": 473028226, - "params": [ - 1 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 473028226, - "result": true -} -``` diff --git a/tests/integration/TaskExternalLinkProcedureTest.php b/tests/integration/TaskExternalLinkProcedureTest.php new file mode 100644 index 00000000..47ff53ad --- /dev/null +++ b/tests/integration/TaskExternalLinkProcedureTest.php @@ -0,0 +1,98 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertGetExternalTaskLinkTypes(); + $this->assertGetExternalTaskLinkProviderDependencies(); + $this->assertGetExternalTaskLinkProviderDependenciesWithProviderNotFound(); + $this->assertCreateExternalTaskLink(); + $this->assertUpdateExternalTaskLink(); + $this->assertGetAllExternalTaskLinks(); + $this->assertRemoveExternalTaskLink(); + } + + public function assertGetExternalTaskLinkTypes() + { + $expected = array( + 'auto' => 'Auto', + 'attachment' => 'Attachment', + 'file' => 'Local File', + 'weblink' => 'Web Link', + ); + + $types = $this->app->getExternalTaskLinkTypes(); + $this->assertEquals($expected, $types); + } + + public function assertGetExternalTaskLinkProviderDependencies() + { + $expected = array( + 'related' => 'Related', + ); + + $dependencies = $this->app->getExternalTaskLinkProviderDependencies('weblink'); + + $this->assertEquals($expected, $dependencies); + } + + public function assertGetExternalTaskLinkProviderDependenciesWithProviderNotFound() + { + $this->assertFalse($this->app->getExternalTaskLinkProviderDependencies('foobar')); + } + + public function assertCreateExternalTaskLink() + { + $url = 'http://localhost/document.pdf'; + $this->linkId = $this->app->createExternalTaskLink($this->taskId, $url, 'related', 'attachment'); + $this->assertNotFalse($this->linkId); + + $link = $this->app->getExternalTaskLinkById($this->taskId, $this->linkId); + $this->assertEquals($this->linkId, $link['id']); + $this->assertEquals($this->taskId, $link['task_id']); + $this->assertEquals('document.pdf', $link['title']); + $this->assertEquals($url, $link['url']); + $this->assertEquals('related', $link['dependency']); + $this->assertEquals(0, $link['creator_id']); + } + + public function assertUpdateExternalTaskLink() + { + $this->assertTrue($this->app->updateExternalTaskLink(array( + 'task_id' => $this->taskId, + 'link_id' => $this->linkId, + 'title' => 'New title', + ))); + + $link = $this->app->getExternalTaskLinkById($this->taskId, $this->linkId); + $this->assertEquals($this->linkId, $link['id']); + $this->assertEquals($this->taskId, $link['task_id']); + $this->assertEquals('New title', $link['title']); + $this->assertEquals('related', $link['dependency']); + $this->assertEquals(0, $link['creator_id']); + } + + public function assertGetAllExternalTaskLinks() + { + $links = $this->app->getAllExternalTaskLinks($this->taskId); + $this->assertCount(1, $links); + $this->assertEquals($this->linkId, $links[0]['id']); + $this->assertEquals($this->taskId, $links[0]['task_id']); + $this->assertEquals('New title', $links[0]['title']); + $this->assertEquals('related', $links[0]['dependency']); + $this->assertEquals(0, $links[0]['creator_id']); + } + + public function assertRemoveExternalTaskLink() + { + $this->assertTrue($this->app->removeExternalTaskLink($this->taskId, $this->linkId)); + } +} -- cgit v1.2.3 From f62112983635a281108575bb69bb90df6bed68b7 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 15:17:38 -0400 Subject: Added new API calls for project attachements --- ChangeLog | 2 +- app/Api/Procedure/ProjectFileProcedure.php | 68 +++++++ app/Api/Procedure/SubtaskTimeTrackingProcedure.php | 6 +- app/Api/Procedure/TaskFileProcedure.php | 4 +- app/ServiceProvider/ApiProvider.php | 2 + app/ServiceProvider/AuthenticationProvider.php | 1 + doc/api-file-procedures.markdown | 217 -------------------- doc/api-json-rpc.markdown | 3 +- doc/api-project-file-procedures.markdown | 221 +++++++++++++++++++++ doc/api-task-file-procedures.markdown | 217 ++++++++++++++++++++ tests/docker/entrypoint.sh | 2 +- tests/integration/ProjectFileProcedureTest.php | 66 ++++++ tests/integration/TaskFileProcedureTest.php | 2 +- 13 files changed, 585 insertions(+), 226 deletions(-) create mode 100644 app/Api/Procedure/ProjectFileProcedure.php delete mode 100644 doc/api-file-procedures.markdown create mode 100644 doc/api-project-file-procedures.markdown create mode 100644 doc/api-task-file-procedures.markdown create mode 100644 tests/integration/ProjectFileProcedureTest.php (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 85966ed8..7a56139c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,7 +5,7 @@ New features: * Added application and project roles validation for API procedure calls * Added new API call: "getProjectByIdentifier" -* Added new API calls for external task links +* Added new API calls for external task links, project attachments Improvements: 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 @@ +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..b6d1102a 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 @@ -25,13 +25,13 @@ class SubtaskTimeTrackingProcedure extends BaseProcedure return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); } - public function logSubtaskEndTime($subtask_id,$user_id) + public function logSubtaskEndTime($subtask_id, $user_id) { SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskEndTime', $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/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/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php index 194bee5b..5cf6231c 100644 --- a/app/ServiceProvider/ApiProvider.php +++ b/app/ServiceProvider/ApiProvider.php @@ -9,6 +9,7 @@ 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; @@ -58,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)) diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index 34b81b9d..978bc05b 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -202,6 +202,7 @@ 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); diff --git a/doc/api-file-procedures.markdown b/doc/api-file-procedures.markdown deleted file mode 100644 index 930be733..00000000 --- a/doc/api-file-procedures.markdown +++ /dev/null @@ -1,217 +0,0 @@ -API File Procedures -=================== - -## createTaskFile - -- Purpose: **Create and upload a new task attachment** -- Parameters: - - **project_id** (integer, required) - - **task_id** (integer, required) - - **filename** (integer, required) - - **blob** File content encoded in base64 (string, required) -- Result on success: **file_id** -- Result on failure: **false** -- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "createTaskFile", - "id": 94500810, - "params": [ - 1, - 1, - "My file", - "cGxhaW4gdGV4dCBmaWxl" - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 94500810, - "result": 1 -} -``` - -## getAllTaskFiles - -- Purpose: **Get all files attached to task** -- Parameters: - - **task_id** (integer, required) -- Result on success: **list of files** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getAllTaskFiles", - "id": 1880662820, - "params": { - "task_id": 1 - } -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 1880662820, - "result": [ - { - "id": "1", - "name": "My file", - "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", - "is_image": "0", - "task_id": "1", - "date": "1432509941", - "user_id": "0", - "size": "15", - "username": null, - "user_name": null - } - ] -} -``` - -## getTaskFile - -- Purpose: **Get file information** -- Parameters: - - **file_id** (integer, required) -- Result on success: **file properties** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getTaskFile", - "id": 318676852, - "params": [ - "1" - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 318676852, - "result": { - "id": "1", - "name": "My file", - "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", - "is_image": "0", - "task_id": "1", - "date": "1432509941", - "user_id": "0", - "size": "15" - } -} -``` - -## downloadTaskFile - -- Purpose: **Download file contents (encoded in base64)** -- Parameters: - - **file_id** (integer, required) -- Result on success: **base64 encoded string** -- Result on failure: **empty string** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "downloadTaskFile", - "id": 235943344, - "params": [ - "1" - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 235943344, - "result": "cGxhaW4gdGV4dCBmaWxl" -} -``` - -## removeTaskFile - -- Purpose: **Remove file** -- Parameters: - - **file_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "removeTaskFile", - "id": 447036524, - "params": [ - "1" - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 447036524, - "result": true -} -``` - -## removeAllTaskFiles - -- Purpose: **Remove all files associated to a task** -- Parameters: - - **task_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "removeAllTaskFiles", - "id": 593312993, - "params": { - "task_id": 1 - } -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 593312993, - "result": true -} -``` diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown index 8e783e71..6498b0cc 100644 --- a/doc/api-json-rpc.markdown +++ b/doc/api-json-rpc.markdown @@ -58,7 +58,8 @@ Usage - [Automatic Actions](api-action-procedures.markdown) - [Tasks](api-task-procedures.markdown) - [Subtasks](api-subtask-procedures.markdown) -- [Files](api-file-procedures.markdown) +- [Task Files](api-task-file-procedures.markdown) +- [Project Files](api-project-file-procedures.markdown) - [Links](api-link-procedures.markdown) - [Internal Task Links](api-internal-task-link-procedures.markdown) - [External Task Links](api-external-task-link-procedures.markdown) diff --git a/doc/api-project-file-procedures.markdown b/doc/api-project-file-procedures.markdown new file mode 100644 index 00000000..fdc5da1a --- /dev/null +++ b/doc/api-project-file-procedures.markdown @@ -0,0 +1,221 @@ +Project File API Procedures +=========================== + +## createProjectFile + +- Purpose: **Create and upload a new project attachment** +- Parameters: + - **project_id** (integer, required) + - **filename** (integer, required) + - **blob** File content encoded in base64 (string, required) +- Result on success: **file_id** +- Result on failure: **false** +- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createProjectFile", + "id": 94500810, + "params": [ + 1, + "My file", + "cGxhaW4gdGV4dCBmaWxl" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 94500810, + "result": 1 +} +``` + +## getAllProjectFiles + +- Purpose: **Get all files attached to a project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **list of files** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllProjectFiles", + "id": 1880662820, + "params": { + "project_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1880662820, + "result": [ + { + "id": "1", + "name": "My file", + "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", + "is_image": "0", + "project_id": "1", + "date": "1432509941", + "user_id": "0", + "size": "15", + "username": null, + "user_name": null + } + ] +} +``` + +## getProjectFile + +- Purpose: **Get file information** +- Parameters: + - **project_id** (integer, required) + - **file_id** (integer, required) +- Result on success: **file properties** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getProjectFile", + "id": 318676852, + "params": [ + "42", + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 318676852, + "result": { + "id": "1", + "name": "My file", + "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", + "is_image": "0", + "project_id": "1", + "date": "1432509941", + "user_id": "0", + "size": "15" + } +} +``` + +## downloadProjectFile + +- Purpose: **Download project file contents (encoded in base64)** +- Parameters: + - **project_id** (integer, required) + - **file_id** (integer, required) +- Result on success: **base64 encoded string** +- Result on failure: **empty string** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "downloadProjectFile", + "id": 235943344, + "params": [ + "1", + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 235943344, + "result": "cGxhaW4gdGV4dCBmaWxl" +} +``` + +## removeProjectFile + +- Purpose: **Remove a file associated to a project** +- Parameters: + - **project_id** (integer, required) + - **file_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeProjectFile", + "id": 447036524, + "params": [ + "1", + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 447036524, + "result": true +} +``` + +## removeAllProjectFiles + +- Purpose: **Remove all files associated to a project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeAllProjectFiles", + "id": 593312993, + "params": { + "project_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 593312993, + "result": true +} +``` diff --git a/doc/api-task-file-procedures.markdown b/doc/api-task-file-procedures.markdown new file mode 100644 index 00000000..51840bea --- /dev/null +++ b/doc/api-task-file-procedures.markdown @@ -0,0 +1,217 @@ +Task File API Procedures +======================== + +## createTaskFile + +- Purpose: **Create and upload a new task attachment** +- Parameters: + - **project_id** (integer, required) + - **task_id** (integer, required) + - **filename** (integer, required) + - **blob** File content encoded in base64 (string, required) +- Result on success: **file_id** +- Result on failure: **false** +- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createTaskFile", + "id": 94500810, + "params": [ + 1, + 1, + "My file", + "cGxhaW4gdGV4dCBmaWxl" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 94500810, + "result": 1 +} +``` + +## getAllTaskFiles + +- Purpose: **Get all files attached to task** +- Parameters: + - **task_id** (integer, required) +- Result on success: **list of files** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllTaskFiles", + "id": 1880662820, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1880662820, + "result": [ + { + "id": "1", + "name": "My file", + "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", + "is_image": "0", + "task_id": "1", + "date": "1432509941", + "user_id": "0", + "size": "15", + "username": null, + "user_name": null + } + ] +} +``` + +## getTaskFile + +- Purpose: **Get file information** +- Parameters: + - **file_id** (integer, required) +- Result on success: **file properties** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getTaskFile", + "id": 318676852, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 318676852, + "result": { + "id": "1", + "name": "My file", + "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596", + "is_image": "0", + "task_id": "1", + "date": "1432509941", + "user_id": "0", + "size": "15" + } +} +``` + +## downloadTaskFile + +- Purpose: **Download file contents (encoded in base64)** +- Parameters: + - **file_id** (integer, required) +- Result on success: **base64 encoded string** +- Result on failure: **empty string** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "downloadTaskFile", + "id": 235943344, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 235943344, + "result": "cGxhaW4gdGV4dCBmaWxl" +} +``` + +## removeTaskFile + +- Purpose: **Remove file** +- Parameters: + - **file_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeTaskFile", + "id": 447036524, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 447036524, + "result": true +} +``` + +## removeAllTaskFiles + +- Purpose: **Remove all files associated to a task** +- Parameters: + - **task_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeAllTaskFiles", + "id": 593312993, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 593312993, + "result": true +} +``` diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh index a88c7ed8..5a37ae4e 100755 --- a/tests/docker/entrypoint.sh +++ b/tests/docker/entrypoint.sh @@ -23,7 +23,7 @@ case "$1" in /var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.sqlite.xml ;; "integration-test-postgres") - wait_schema_creation 5 + wait_schema_creation 10 /var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.postgres.xml ;; "integration-test-mysql") diff --git a/tests/integration/ProjectFileProcedureTest.php b/tests/integration/ProjectFileProcedureTest.php new file mode 100644 index 00000000..8ac70d87 --- /dev/null +++ b/tests/integration/ProjectFileProcedureTest.php @@ -0,0 +1,66 @@ +assertCreateTeamProject(); + $this->assertCreateProjectFile(); + $this->assertGetProjectFile(); + $this->assertDownloadProjectFile(); + $this->assertGetAllFiles(); + $this->assertRemoveProjectFile(); + $this->assertRemoveAllProjectFiles(); + } + + public function assertCreateProjectFile() + { + $this->fileId = $this->app->createProjectFile($this->projectId, 'My file.txt', base64_encode('plain text file')); + $this->assertNotFalse($this->fileId); + } + + public function assertGetProjectFile() + { + $file = $this->app->getProjectFile($this->projectId, $this->fileId); + $this->assertNotEmpty($file); + $this->assertEquals('My file.txt', $file['name']); + } + + public function assertDownloadProjectFile() + { + $content = $this->app->downloadProjectFile($this->projectId, $this->fileId); + $this->assertNotEmpty($content); + $this->assertEquals('plain text file', base64_decode($content)); + } + + public function assertGetAllFiles() + { + $files = $this->app->getAllProjectFiles($this->projectId); + $this->assertCount(1, $files); + $this->assertEquals('My file.txt', $files[0]['name']); + } + + public function assertRemoveProjectFile() + { + $this->assertTrue($this->app->removeProjectFile($this->projectId, $this->fileId)); + + $files = $this->app->getAllProjectFiles($this->projectId); + $this->assertEmpty($files); + } + + public function assertRemoveAllProjectFiles() + { + $this->assertCreateProjectFile(); + $this->assertCreateProjectFile(); + + $this->assertTrue($this->app->removeAllProjectFiles($this->projectId)); + + $files = $this->app->getAllProjectFiles($this->projectId); + $this->assertEmpty($files); + } +} diff --git a/tests/integration/TaskFileProcedureTest.php b/tests/integration/TaskFileProcedureTest.php index 61155555..60909ecd 100644 --- a/tests/integration/TaskFileProcedureTest.php +++ b/tests/integration/TaskFileProcedureTest.php @@ -21,7 +21,7 @@ class TaskFileProcedureTest extends BaseProcedureTest public function assertCreateTaskFile() { - $this->fileId = $this->app->createTaskFile(1, $this->taskId, 'My file', base64_encode('plain text file')); + $this->fileId = $this->app->createTaskFile($this->projectId, $this->taskId, 'My file', base64_encode('plain text file')); $this->assertNotFalse($this->fileId); } -- cgit v1.2.3 From 82623f1a212a3a79507ede69586c561efa675224 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 15:47:02 -0400 Subject: Added API calls for subtask time tracking --- ChangeLog | 2 +- app/Api/Procedure/SubtaskTimeTrackingProcedure.php | 8 +- doc/api-json-rpc.markdown | 1 + doc/api-subtask-time-tracking-procedures.markdown | 102 +++++++++++++++++++++ tests/integration/BaseProcedureTest.php | 11 +++ tests/integration/SubtaskProcedureTest.php | 11 --- .../SubtaskTimeTrackingProcedureTest.php | 46 ++++++++++ 7 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 doc/api-subtask-time-tracking-procedures.markdown create mode 100644 tests/integration/SubtaskTimeTrackingProcedureTest.php (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 7a56139c..eccff6a3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,7 +5,7 @@ New features: * Added application and project roles validation for API procedure calls * Added new API call: "getProjectByIdentifier" -* Added new API calls for external task links, project attachments +* Added new API calls for external task links, project attachments and subtask time tracking Improvements: diff --git a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php index b6d1102a..5ceaa08d 100644 --- a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php +++ b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php @@ -19,15 +19,15 @@ 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); } diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown index 6498b0cc..ab1056f0 100644 --- a/doc/api-json-rpc.markdown +++ b/doc/api-json-rpc.markdown @@ -58,6 +58,7 @@ Usage - [Automatic Actions](api-action-procedures.markdown) - [Tasks](api-task-procedures.markdown) - [Subtasks](api-subtask-procedures.markdown) +- [Subtask Time Tracking](api-subtask-time-tracking-procedures.markdown) - [Task Files](api-task-file-procedures.markdown) - [Project Files](api-project-file-procedures.markdown) - [Links](api-link-procedures.markdown) diff --git a/doc/api-subtask-time-tracking-procedures.markdown b/doc/api-subtask-time-tracking-procedures.markdown new file mode 100644 index 00000000..67447623 --- /dev/null +++ b/doc/api-subtask-time-tracking-procedures.markdown @@ -0,0 +1,102 @@ +Subtask Time Tracking API procedures +==================================== + +## hasSubtaskTimer + +- Purpose: **Check if a timer is started for the given subtask and user** +- Parameters: + - **subtask_id** (integer, required) + - **user_id** (integer, optional) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"hasSubtaskTimer","id":1786995697,"params":[2,4]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": true, + "id": 1786995697 +} +``` + +## setSubtaskStartTime + +- Purpose: **Start subtask timer for a user** +- Parameters: + - **subtask_id** (integer, required) + - **user_id** (integer, optional) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"setSubtaskStartTime","id":1168991769,"params":[2,4]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": true, + "id": 1168991769 +} +``` + +## setSubtaskEndTime + +- Purpose: **Stop subtask timer for a user** +- Parameters: + - **subtask_id** (integer, required) + - **user_id** (integer, optional) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"setSubtaskEndTime","id":1026607603,"params":[2,4]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": true, + "id": 1026607603 +} +``` + +## getSubtaskTimeSpent + +- Purpose: **Get time spent on a subtask for a user** +- Parameters: + - **subtask_id** (integer, required) + - **user_id** (integer, optional) +- Result on success: **number of hours** +- Result on failure: **false** + +Request example: + +```json +{"jsonrpc":"2.0","method":"getSubtaskTimeSpent","id":738527378,"params":[2,4]} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "result": 1.5, + "id": 738527378 +} +``` diff --git a/tests/integration/BaseProcedureTest.php b/tests/integration/BaseProcedureTest.php index e3382e82..beb13ff7 100644 --- a/tests/integration/BaseProcedureTest.php +++ b/tests/integration/BaseProcedureTest.php @@ -17,6 +17,7 @@ abstract class BaseProcedureTest extends PHPUnit_Framework_TestCase protected $projectId = 0; protected $taskTitle = 'My task'; protected $taskId = 0; + protected $subtaskId = 0; protected $groupName1 = 'My Group A'; protected $groupName2 = 'My Group B'; @@ -119,4 +120,14 @@ abstract class BaseProcedureTest extends PHPUnit_Framework_TestCase $this->taskId = $this->app->createTask(array('title' => $this->taskTitle, 'project_id' => $this->projectId)); $this->assertNotFalse($this->taskId); } + + public function assertCreateSubtask() + { + $this->subtaskId = $this->app->createSubtask(array( + 'task_id' => $this->taskId, + 'title' => 'subtask #1', + )); + + $this->assertNotFalse($this->subtaskId); + } } diff --git a/tests/integration/SubtaskProcedureTest.php b/tests/integration/SubtaskProcedureTest.php index 7ab4ef0b..b9868e6f 100644 --- a/tests/integration/SubtaskProcedureTest.php +++ b/tests/integration/SubtaskProcedureTest.php @@ -5,7 +5,6 @@ require_once __DIR__.'/BaseProcedureTest.php'; class SubtaskProcedureTest extends BaseProcedureTest { protected $projectName = 'My project to test subtasks'; - private $subtaskId = 0; public function testAll() { @@ -18,16 +17,6 @@ class SubtaskProcedureTest extends BaseProcedureTest $this->assertRemoveSubtask(); } - public function assertCreateSubtask() - { - $this->subtaskId = $this->app->createSubtask(array( - 'task_id' => $this->taskId, - 'title' => 'subtask #1', - )); - - $this->assertNotFalse($this->subtaskId); - } - public function assertGetSubtask() { $subtask = $this->app->getSubtask($this->subtaskId); diff --git a/tests/integration/SubtaskTimeTrackingProcedureTest.php b/tests/integration/SubtaskTimeTrackingProcedureTest.php new file mode 100644 index 00000000..6c45c983 --- /dev/null +++ b/tests/integration/SubtaskTimeTrackingProcedureTest.php @@ -0,0 +1,46 @@ +assertCreateTeamProject(); + $this->assertCreateTask(); + $this->assertCreateSubtask(); + $this->assertHasNoTimer(); + $this->assertStartTimer(); + $this->assertHasTimer(); + $this->assertStopTimer(); + $this->assertHasNoTimer(); + $this->assertGetSubtaskTimeSpent(); + } + + public function assertHasNoTimer() + { + $this->assertFalse($this->app->hasSubtaskTimer($this->subtaskId, $this->userUserId)); + } + + public function assertHasTimer() + { + $this->assertTrue($this->app->hasSubtaskTimer($this->subtaskId, $this->userUserId)); + } + + public function assertStartTimer() + { + $this->assertTrue($this->app->setSubtaskStartTime($this->subtaskId, $this->userUserId)); + } + + public function assertStopTimer() + { + $this->assertTrue($this->app->setSubtaskEndTime($this->subtaskId, $this->userUserId)); + } + + public function assertGetSubtaskTimeSpent() + { + $this->assertEquals(0, $this->app->getSubtaskTimeSpent($this->subtaskId, $this->userUserId)); + } +} -- cgit v1.2.3 From 6ad547288c3978a91d72fddf54d5f3912ac9354f Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 16:45:55 -0400 Subject: Added documentation for tags --- doc/index.markdown | 1 + doc/screenshots/tags-board.png | Bin 0 -> 7405 bytes doc/screenshots/tags-global.png | Bin 0 -> 9617 bytes doc/screenshots/tags-projects.png | Bin 0 -> 13718 bytes doc/screenshots/tags-task.png | Bin 0 -> 4766 bytes doc/tags.markdown | 24 ++++++++++++++++++++++++ 6 files changed, 25 insertions(+) create mode 100644 doc/screenshots/tags-board.png create mode 100644 doc/screenshots/tags-global.png create mode 100644 doc/screenshots/tags-projects.png create mode 100644 doc/screenshots/tags-task.png create mode 100644 doc/tags.markdown (limited to 'doc') diff --git a/doc/index.markdown b/doc/index.markdown index c1e9a506..6d52db27 100644 --- a/doc/index.markdown +++ b/doc/index.markdown @@ -46,6 +46,7 @@ Using Kanboard - [Subtasks](subtasks.markdown) - [Analytics for tasks](analytics-tasks.markdown) - [User mentions](user-mentions.markdown) +- [Tags](tags.markdown) ### Working with users and groups diff --git a/doc/screenshots/tags-board.png b/doc/screenshots/tags-board.png new file mode 100644 index 00000000..1a7f710d Binary files /dev/null and b/doc/screenshots/tags-board.png differ diff --git a/doc/screenshots/tags-global.png b/doc/screenshots/tags-global.png new file mode 100644 index 00000000..f5059ae8 Binary files /dev/null and b/doc/screenshots/tags-global.png differ diff --git a/doc/screenshots/tags-projects.png b/doc/screenshots/tags-projects.png new file mode 100644 index 00000000..2e08ce90 Binary files /dev/null and b/doc/screenshots/tags-projects.png differ diff --git a/doc/screenshots/tags-task.png b/doc/screenshots/tags-task.png new file mode 100644 index 00000000..c9c8b2f7 Binary files /dev/null and b/doc/screenshots/tags-task.png differ diff --git a/doc/tags.markdown b/doc/tags.markdown new file mode 100644 index 00000000..ef2d5b26 --- /dev/null +++ b/doc/tags.markdown @@ -0,0 +1,24 @@ +Tags +==== + +With Kanboard, you can associate one or many tags to a task. +You can define tags globally for all projects or only for a specific project. + +![Tags on the board](screenshots/tags-board.png) + +From the task form, you can enter the desired tags: + +![Tags form](screenshots/tags-task.png) + +The auto-completion form will show up to suggest available tags. + +You can also create tags directly from the task form. +By default, when you create tags from a task form they are associated to the current project: + +![Project Tags](screenshots/tags-projects.png) + +You can also create these tags manually in the project settings. + +To define tags globally for all projects, go to the application settings: + +![Global Tags](screenshots/tags-global.png) -- cgit v1.2.3 From 6bdee8b1cadd991b747a98b2659971b9f2c2de4f Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 16:49:58 -0400 Subject: Update tags documentation (search) --- doc/screenshots/tags-search.png | Bin 0 -> 3904 bytes doc/tags.markdown | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/screenshots/tags-search.png (limited to 'doc') diff --git a/doc/screenshots/tags-search.png b/doc/screenshots/tags-search.png new file mode 100644 index 00000000..eab1ad80 Binary files /dev/null and b/doc/screenshots/tags-search.png differ diff --git a/doc/tags.markdown b/doc/tags.markdown index ef2d5b26..26b3169d 100644 --- a/doc/tags.markdown +++ b/doc/tags.markdown @@ -17,8 +17,12 @@ By default, when you create tags from a task form they are associated to the cur ![Project Tags](screenshots/tags-projects.png) -You can also create these tags manually in the project settings. +All tags can be managed in the project settings. To define tags globally for all projects, go to the application settings: ![Global Tags](screenshots/tags-global.png) + +To search tasks based on tags, just use the attribute "tag": + +![Search Tags](screenshots/tags-search.png) -- cgit v1.2.3 From 7e0e7eba6df951d1795bdd73cd49479ff2cab860 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 26 Jun 2016 16:54:57 -0400 Subject: Fixed some typo in documentation --- doc/api-external-task-link-procedures.markdown | 4 ++-- doc/api-internal-task-link-procedures.markdown | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'doc') diff --git a/doc/api-external-task-link-procedures.markdown b/doc/api-external-task-link-procedures.markdown index 2858be86..85f67b60 100644 --- a/doc/api-external-task-link-procedures.markdown +++ b/doc/api-external-task-link-procedures.markdown @@ -1,5 +1,5 @@ -Internal Task Links API Procedures -================================== +External Task Link API Procedures +================================= ## getExternalTaskLinkTypes diff --git a/doc/api-internal-task-link-procedures.markdown b/doc/api-internal-task-link-procedures.markdown index 859228de..eca0d886 100644 --- a/doc/api-internal-task-link-procedures.markdown +++ b/doc/api-internal-task-link-procedures.markdown @@ -1,5 +1,5 @@ -Internal Task Links API Procedures -================================== +Internal Task Link API Procedures +================================= ## createTaskLink -- cgit v1.2.3 From 3cbc44f75a096e840b23504005878bb9bff7c5be Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Wed, 29 Jun 2016 22:48:38 -0400 Subject: Update IIS documentation --- doc/nice-urls.markdown | 18 ++++++++++------ doc/windows-iis-installation.markdown | 40 +++++++++++++++-------------------- 2 files changed, 29 insertions(+), 29 deletions(-) (limited to 'doc') diff --git a/doc/nice-urls.markdown b/doc/nice-urls.markdown index 9fbb3510..bfea719d 100644 --- a/doc/nice-urls.markdown +++ b/doc/nice-urls.markdown @@ -88,18 +88,25 @@ define('ENABLE_URL_REWRITE', true); Adapt the example above according to your own configuration. IIS configuration example ---------------------------- +------------------------- -Create a web.config in you installation folder: +1. Download and install the Rewrite module for IIS: [Download link](http://www.iis.net/learn/extensions/url-rewrite-module/using-the-url-rewrite-module) +2. Create a web.config in you installation folder: ```xml - + + + + + + + - - + + @@ -109,7 +116,6 @@ Create a web.config in you installation folder: - ``` In your Kanboard `config.php`: diff --git a/doc/windows-iis-installation.markdown b/doc/windows-iis-installation.markdown index bd4607de..26ce178f 100644 --- a/doc/windows-iis-installation.markdown +++ b/doc/windows-iis-installation.markdown @@ -12,7 +12,10 @@ PHP installation - [Microsoft IIS 7.0 and later](http://php.net/manual/en/install.windows.iis7.php) - [PHP for Windows is available here](http://windows.php.net/download/) -Edit the `php.ini`, uncomment these PHP modules: + +### PHP.ini + +You need at least, these extensions in your `php.ini`: ```ini extension=php_gd2.dll @@ -22,7 +25,9 @@ extension=php_openssl.dll extension=php_pdo_sqlite.dll ``` -Set the time zone: +The complete list of required PHP extensions is available on the [requirements page](requirements.markdown) + +Do not forget to set the time zone: ```ini date.timezone = America/Montreal @@ -30,27 +35,21 @@ date.timezone = America/Montreal The list of supported time zones can be found in the [PHP documentation](http://php.net/manual/en/timezones.america.php). -Check if PHP runs correctly: - -Go the IIS document root `C:\inetpub\wwwroot` and create a file `phpinfo.php`: - -```php - -``` - -Open a browser at `http://localhost/phpinfo.php` and you should see the current PHP settings. -If you got an error 500, something is not correctly done in your installation. - Notes: - If you use PHP < 5.4, you have to enable the short tags in your php.ini - Don't forget to enable the required php extensions mentioned above - If you got an error about "the library MSVCP110.dll is missing", you probably need to download the Visual C++ Redistributable for Visual Studio from the Microsoft website. +IIS Modules +----------- + +The Kanboard archive contains a `web.config` file to enable [URL rewriting](nice-urls.markdown). +This configuration require the [Rewrite module for IIS](http://www.iis.net/learn/extensions/url-rewrite-module/using-the-url-rewrite-module). + +If you don't have the rewrite module, you will get an internal server error (500) from IIS. +If you don't want to have Kanboard with nice URLs, you can remove the file `web.config`. + Kanboard installation --------------------- @@ -59,12 +58,7 @@ Kanboard installation - Make sure the directory `data` is writable by the IIS user - Open your web browser to use Kanboard http://localhost/kanboard/ - The default credentials are **admin/admin** - -Tested configurations ---------------------- - -- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16 -- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29 +- [URL rewrite configuration](nice-urls.markdown) Notes ----- -- cgit v1.2.3 From 4b5c3b05271a63cb7b2790ffa9a81453fef5b642 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 2 Jul 2016 22:35:54 -0400 Subject: Use PHP7 for Docker image --- .docker/crontab/cronjob.alpine | 1 - .docker/crontab/cronjob.debian | 1 - .docker/kanboard/config.php | 3 -- .docker/nginx/nginx.conf | 72 ------------------------------------ .docker/php/conf.d/local.ini | 16 -------- .docker/php/php-fpm.conf | 22 ----------- .docker/services.d/.s6-svscan/finish | 2 - .docker/services.d/cron/run | 2 - .docker/services.d/nginx/run | 2 - .docker/services.d/php/run | 2 - .dockerignore | 18 ++++++++- ChangeLog | 1 + Dockerfile | 37 ++++-------------- Makefile | 3 +- app/constants.php | 2 +- doc/docker.markdown | 6 +-- doc/env.markdown | 1 - docker/crontab/cronjob.alpine | 1 + docker/php/env.conf | 2 + docker/services.d/cron/run | 2 + 20 files changed, 36 insertions(+), 160 deletions(-) delete mode 100644 .docker/crontab/cronjob.alpine delete mode 100644 .docker/crontab/cronjob.debian delete mode 100644 .docker/kanboard/config.php delete mode 100644 .docker/nginx/nginx.conf delete mode 100644 .docker/php/conf.d/local.ini delete mode 100644 .docker/php/php-fpm.conf delete mode 100755 .docker/services.d/.s6-svscan/finish delete mode 100755 .docker/services.d/cron/run delete mode 100755 .docker/services.d/nginx/run delete mode 100755 .docker/services.d/php/run create mode 100644 docker/crontab/cronjob.alpine create mode 100644 docker/php/env.conf create mode 100755 docker/services.d/cron/run (limited to 'doc') diff --git a/.docker/crontab/cronjob.alpine b/.docker/crontab/cronjob.alpine deleted file mode 100644 index 91ad044e..00000000 --- a/.docker/crontab/cronjob.alpine +++ /dev/null @@ -1 +0,0 @@ -1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.docker/crontab/cronjob.debian b/.docker/crontab/cronjob.debian deleted file mode 100644 index 40310d4f..00000000 --- a/.docker/crontab/cronjob.debian +++ /dev/null @@ -1 +0,0 @@ -@daily www-data cd /var/www/html/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.docker/kanboard/config.php b/.docker/kanboard/config.php deleted file mode 100644 index fa1c5971..00000000 --- a/.docker/kanboard/config.php +++ /dev/null @@ -1,3 +0,0 @@ - +FROM fguillot/alpine-nginx-php7 -RUN apk update && \ - apk add nginx bash ca-certificates s6 curl \ - php5-fpm php5-json php5-zlib php5-xml php5-dom php5-ctype php5-opcache php5-zip \ - php5-pdo php5-pdo_mysql php5-pdo_sqlite php5-pdo_pgsql php5-ldap \ - php5-gd php5-mcrypt php5-openssl php5-phar && \ - rm -rf /var/cache/apk/* +COPY . /var/www/app +COPY docker/kanboard/config.php /var/www/app/config.php +COPY docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx +COPY docker/services.d/cron /etc/services.d/cron -RUN curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer - -RUN cd /var/www \ - && curl -LO https://github.com/fguillot/kanboard/archive/master.zip \ - && unzip -qq master.zip \ - && rm -f *.zip \ - && mv kanboard-master kanboard \ - && cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install \ - && chown -R nginx:nginx /var/www/kanboard \ - && chown -R nginx:nginx /var/lib/nginx - -COPY .docker/services.d /etc/services.d -COPY .docker/php/conf.d/local.ini /etc/php5/conf.d/ -COPY .docker/php/php-fpm.conf /etc/php5/ -COPY .docker/nginx/nginx.conf /etc/nginx/ -COPY .docker/kanboard/config.php /var/www/kanboard/ -COPY .docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx - -EXPOSE 80 +RUN cd /var/www/app && composer --prefer-dist --no-dev --optimize-autoloader --quiet install +RUN chown -R nginx:nginx /var/www/app/data /var/www/app/plugins VOLUME /var/www/kanboard/data VOLUME /var/www/kanboard/plugins - -ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] -CMD [] diff --git a/Makefile b/Makefile index 47e9d389..4595481d 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,8 @@ archive: @ rm -rf ${BUILD_DIR}/kanboard/*.lock @ rm -rf ${BUILD_DIR}/kanboard/*.json @ rm -rf ${BUILD_DIR}/kanboard/*.js - @ rm -rf ${BUILD_DIR}/kanboard/.docker + @ rm -rf ${BUILD_DIR}/kanboard/.dockerignore + @ rm -rf ${BUILD_DIR}/kanboard/docker @ rm -rf ${BUILD_DIR}/kanboard/nitrous* @ cd ${BUILD_DIR}/kanboard && find ./vendor -name doc -type d -exec rm -rf {} +; @ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +; 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'); diff --git a/doc/docker.markdown b/doc/docker.markdown index 3f13e954..9af5f57a 100644 --- a/doc/docker.markdown +++ b/doc/docker.markdown @@ -3,17 +3,17 @@ How to run Kanboard with Docker? Kanboard can run easily with [Docker](https://www.docker.com). -The image size is approximately **50MB** and contains: +The image size is approximately **70MB** and contains: - [Alpine Linux](http://alpinelinux.org/) - The [process manager S6](http://skarnet.org/software/s6/) - Nginx -- PHP-FPM +- PHP 7 The Kanboard cronjob is also running everyday at midnight. URL rewriting is enabled in the included config file. -When the container is running, the memory utilization is around **20MB**. +When the container is running, the memory utilization is around **30MB**. Use the stable version ---------------------- diff --git a/doc/env.markdown b/doc/env.markdown index 28f14b18..902066d7 100644 --- a/doc/env.markdown +++ b/doc/env.markdown @@ -7,4 +7,3 @@ Environment variables maybe useful when Kanboard is deployed as container (Docke |---------------|---------------------------------------------------------------------------------------------------------------------------------| | DATABASE_URL | `[database type]://[username]:[password]@[host]:[port]/[database name]`, example: `postgres://foo:foo@myserver:5432/kanboard` | | DEBUG | Enable/Disable debug mode: "true" or "false" | -| LOG_DRIVER | Logging driver: stdout, stderr, file or syslog | diff --git a/docker/crontab/cronjob.alpine b/docker/crontab/cronjob.alpine new file mode 100644 index 00000000..d051ff28 --- /dev/null +++ b/docker/crontab/cronjob.alpine @@ -0,0 +1 @@ +1 0 * * * cd /var/www/app && ./kanboard cronjob diff --git a/docker/php/env.conf b/docker/php/env.conf new file mode 100644 index 00000000..bcf2e37c --- /dev/null +++ b/docker/php/env.conf @@ -0,0 +1,2 @@ +env[DATABASE_URL] = $DATABASE_URL +env[DEBUG] = $DEBUG diff --git a/docker/services.d/cron/run b/docker/services.d/cron/run new file mode 100755 index 00000000..da378099 --- /dev/null +++ b/docker/services.d/cron/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +crond -f \ No newline at end of file -- cgit v1.2.3 From e7a15de9e51973c1ed17ccdd01f7d24b5c3530a5 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 2 Jul 2016 22:52:52 -0400 Subject: Docker volume path --- Dockerfile | 4 ++-- doc/docker.markdown | 8 ++++---- docker-compose.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) (limited to 'doc') diff --git a/Dockerfile b/Dockerfile index ebc87238..518f4685 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,5 @@ COPY docker/services.d/cron /etc/services.d/cron RUN cd /var/www/app && composer --prefer-dist --no-dev --optimize-autoloader --quiet install RUN chown -R nginx:nginx /var/www/app/data /var/www/app/plugins -VOLUME /var/www/kanboard/data -VOLUME /var/www/kanboard/plugins +VOLUME /var/www/app/data +VOLUME /var/www/app/plugins diff --git a/doc/docker.markdown b/doc/docker.markdown index 9af5f57a..e55130e5 100644 --- a/doc/docker.markdown +++ b/doc/docker.markdown @@ -64,8 +64,8 @@ Volumes You can attach 2 volumes to your container: -- Data folder: `/var/www/kanboard/data` -- Plugins folder: `/var/www/kanboard/plugins` +- Data folder: `/var/www/app/data` +- Plugins folder: `/var/www/app/plugins` Use the flag `-v` to mount a volume on the host machine like described in [official Docker documentation](https://docs.docker.com/engine/userguide/containers/dockervolumes/). @@ -84,8 +84,8 @@ The list of environment variables is available on [this page](env.markdown). Config files ------------ -- The container already include a custom config file located at `/var/www/kanboard/config.php`. -- You can store your own config file on the data volume: `/var/www/kanboard/data/config.php`. +- The container already include a custom config file located at `/var/www/app/config.php`. +- You can store your own config file on the data volume: `/var/www/app/data/config.php`. References ---------- diff --git a/docker-compose.yml b/docker-compose.yml index aa0a6710..9b618cf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,8 @@ services: ports: - "80:80" volumes: - - kanboard_data:/var/www/kanboard/data - - kanboard_plugins:/var/www/kanboard/plugins + - kanboard_data:/var/www/app/data + - kanboard_plugins:/var/www/app/plugins volumes: kanboard_data: driver: local -- cgit v1.2.3 From 8da8f1d86d323f5843defdd36f2a1b144919a3fc Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 3 Jul 2016 13:18:17 -0400 Subject: Update some documentation --- doc/creating-tasks.markdown | 19 +++++++++++-------- doc/fr_FR/creating-tasks.markdown | 14 +++++++++++--- doc/fr_FR/index.markdown | 1 + doc/fr_FR/removing-projects.markdown | 10 ++++++++++ doc/fr_FR/screenshots/task-creation-board.png | Bin 0 -> 3915 bytes doc/fr_FR/screenshots/task-creation-form.png | Bin 0 -> 42123 bytes doc/index.markdown | 1 + doc/removing-projects.markdown | 10 ++++++++++ doc/screenshots/project-remove.png | Bin 0 -> 4330 bytes doc/screenshots/task-creation-board.png | Bin 0 -> 10242 bytes doc/screenshots/task-creation-form.png | Bin 0 -> 34726 bytes 11 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 doc/fr_FR/removing-projects.markdown create mode 100644 doc/fr_FR/screenshots/task-creation-board.png create mode 100644 doc/fr_FR/screenshots/task-creation-form.png create mode 100644 doc/removing-projects.markdown create mode 100644 doc/screenshots/project-remove.png create mode 100644 doc/screenshots/task-creation-board.png create mode 100644 doc/screenshots/task-creation-form.png (limited to 'doc') diff --git a/doc/creating-tasks.markdown b/doc/creating-tasks.markdown index 2ebdbb9c..99dce713 100644 --- a/doc/creating-tasks.markdown +++ b/doc/creating-tasks.markdown @@ -3,25 +3,28 @@ Creating Tasks From the board, click on the plus sign next to the column name: -![Task creation from the board](https://kanboard.net/screenshots/documentation/task-creation-board.png) +![Task creation from the board](screenshots/task-creation-board.png) Then the task creation form appears: -![Task creation form](https://kanboard.net/screenshots/documentation/task-creation-form.png) - -The only mandatory field is the title. +![Task creation form](screenshots/task-creation-form.png) Field description: - **Title**: The title of your task, which will be displayed on the board. -- **Description**: Allow you to add more information about the task, the content can be written in [Markdown](https://kanboard.net/documentation/syntax-guide). +- **Description**: Description that use the [Markdown](syntax-guide.markdown) format. +- **Tags**: The list of tags associated to tasks. - **Create another task**: Check this box if you want to create a similar task (some fields will be pre-filled). +- **Color**: Choose the color of the card. - **Assignee**: The person that will work on the task. -- **Category**: Only one category can be assigned to a task. +- **Category**: Only one category can be assigned to a task (visible only if the projects have categories). - **Column**: The column where the task will be created, your task will be positioned at the bottom. -- **Color**: Choose the color of the card. +- **Priority**: Task priority, the range can be defined in the project settings, default values are P0 to P3. - **Complexity**: Used in agile project management (Scrum), the complexity or story points is a number that tells the team how hard the story is. Often, people use the Fibonacci series. -- **Original Estimate**: Estimation in hours to complete the tasks. +- **Reference**: External ID for the task, for example it can be ticket number that come from another system +- **Original Estimate**: Estimation in hours to complete the task. +- **Time Spent**: Time spent working on the task. +- **Start Date**: This is a date time field. - **Due Date**: Overdue tasks will have a red due date and upcoming due dates will be black on the board. Several date format are accepted in addition to the date picker. With the preview link, you can see the task description converted from the Markdown syntax. diff --git a/doc/fr_FR/creating-tasks.markdown b/doc/fr_FR/creating-tasks.markdown index 9b7fa274..c3cfed01 100644 --- a/doc/fr_FR/creating-tasks.markdown +++ b/doc/fr_FR/creating-tasks.markdown @@ -3,25 +3,33 @@ Créer des tâches Depuis le tableau, cliquez sur le signe plus + à côté du nom de la colonne : -![Création de tâche à partir du tableau](https://kanboard.net/screenshots/documentation/task-creation-board.png) +![Création de tâche à partir du tableau](screenshots/task-creation-board.png) Le formulaire de création de tâche apparaît : -![Formulaire de création de tâche](https://kanboard.net/screenshots/documentation/task-creation-form.png) +![Formulaire de création de tâche](screenshots/task-creation-form.png) Le seul champ obligatoire est le titre. Description des champs : - **Titre** : le titre de votre tâche, tel qu'il sera affiché sur le tableau. -- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](https://kanboard.net/documentation/syntax-guide). +- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](syntax-guide.markdown). +- **Libellés**: Liste de libellés associés à la tâche. - **Créer une autre tâche** : cochez cette case si vous souhaitez créer une tâche similaire (les champs seront pré-remplis). - **Assigné** : la personne qui va travailler sur la tâche. - **Catégorie** : une seule catégorie peut être assignée à une tâche. - **Colonne** : la colonne dans laquelle la tâche sera créée. La tâche sera positionnée en bas de cette colonne. - **Couleur** : Choisissez la couleur de la carte. - **Complexité** : utilisée dans la gestion de projet agile (Scrum), la complexité des points d'étape est un nombre qui montre à l'équipe le degré de difficulté de l'avancement du projet. Les utilisateurs se servent souvent des suites de Fibonacci. +- **Référence** : Identifiant externe, par exemple cela peut-être un numéro de ticket qui vient d'un système externe. - **Estimation originale** : estimation du nombre d'heures nécessaire pour terminer les tâches. - **Date d'échéance** : les tâches dont la date d'échéance est dépassée auront une date d'échéance en rouge et les dates suivantes seront en noir dans le tableau. Plusieurs formats de date sont acceptés, outre le sélecteur de date. Avec le lien d'aperçu (« Prévisualiser »), vous pouvez voir la description de la tâche convertie depuis la syntaxe Markdown. + +Vous créer une tâche de plusieurs manières : + +- Avec l'icône avec le signe plus sur le board +- Avec le raccourci clavier "n" +- Depuis le menu déroulant en haut à gauche diff --git a/doc/fr_FR/index.markdown b/doc/fr_FR/index.markdown index f74c3fce..a73c5c23 100644 --- a/doc/fr_FR/index.markdown +++ b/doc/fr_FR/index.markdown @@ -22,6 +22,7 @@ Utiliser Kanboard - [Types de projets](project-types.markdown) - [Créer des projets](creating-projects.markdown) - [Modifier des projets](editing-projects.markdown) +- [Supprimer des projets](removing-projects.markdown) - [Partager des tableaux et des tâches](sharing-projects.markdown) - [Actions automatiques](automatic-actions.markdown) - [Permissions des projets](project-permissions.markdown) diff --git a/doc/fr_FR/removing-projects.markdown b/doc/fr_FR/removing-projects.markdown new file mode 100644 index 00000000..1b64191e --- /dev/null +++ b/doc/fr_FR/removing-projects.markdown @@ -0,0 +1,10 @@ +Supprimer des projets +===================== + +Pour supprimer un projet, vous devez être gestionnaire du projet ou administrateur. + +Aller dans les **Préférences du projet**, depuis le menu à gauche, en bas, choisissez **Supprimer**. + +![Supprimer un Projet](screenshots/project-remove.png) + +Supprimer un projet, supprime également toutes les tâches qui appartiennent à ce projet. diff --git a/doc/fr_FR/screenshots/task-creation-board.png b/doc/fr_FR/screenshots/task-creation-board.png new file mode 100644 index 00000000..18f13b3f Binary files /dev/null and b/doc/fr_FR/screenshots/task-creation-board.png differ diff --git a/doc/fr_FR/screenshots/task-creation-form.png b/doc/fr_FR/screenshots/task-creation-form.png new file mode 100644 index 00000000..5e4b455e Binary files /dev/null and b/doc/fr_FR/screenshots/task-creation-form.png differ diff --git a/doc/index.markdown b/doc/index.markdown index 6d52db27..bc3e8a32 100644 --- a/doc/index.markdown +++ b/doc/index.markdown @@ -22,6 +22,7 @@ Using Kanboard - [Project Types](project-types.markdown) - [Creating projects](creating-projects.markdown) - [Editing projects](editing-projects.markdown) +- [Removing projects](removing-projects.markdown) - [Sharing boards and tasks](sharing-projects.markdown) - [Automatic actions](automatic-actions.markdown) - [Project permissions](project-permissions.markdown) diff --git a/doc/removing-projects.markdown b/doc/removing-projects.markdown new file mode 100644 index 00000000..f9e622cb --- /dev/null +++ b/doc/removing-projects.markdown @@ -0,0 +1,10 @@ +Removing Projects +================= + +To remove a project, you must be manager of the project or administrator. + +Go to the **"Project settings"**, and from the menu on the left, at the bottom, choose **"Remove"**. + +![Removing Projects](screenshots/project-remove.png) + +Removing a project remove all tasks that belongs to this project. diff --git a/doc/screenshots/project-remove.png b/doc/screenshots/project-remove.png new file mode 100644 index 00000000..d1c33cc1 Binary files /dev/null and b/doc/screenshots/project-remove.png differ diff --git a/doc/screenshots/task-creation-board.png b/doc/screenshots/task-creation-board.png new file mode 100644 index 00000000..456dff0b Binary files /dev/null and b/doc/screenshots/task-creation-board.png differ diff --git a/doc/screenshots/task-creation-form.png b/doc/screenshots/task-creation-form.png new file mode 100644 index 00000000..d1e5d24e Binary files /dev/null and b/doc/screenshots/task-creation-form.png differ -- cgit v1.2.3 From df423ae4aff12e8143a52642a3025ee1c0dffcea Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 21 Jul 2016 17:46:17 -0400 Subject: Move repository to Kanboard organization --- Makefile | 2 +- README.md | 8 ++++---- app.json | 2 +- app/Template/config/about.php | 2 +- doc/docker.markdown | 2 +- doc/heroku.markdown | 4 ++-- doc/installation.markdown | 2 +- doc/plugin-authentication.markdown | 2 +- doc/plugin-group-provider.markdown | 2 +- doc/plugins.markdown | 2 +- doc/update.markdown | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) (limited to 'doc') diff --git a/Makefile b/Makefile index 4595481d..a96846a1 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ static: archive: @ echo "Build archive: version=${version}, destination=${dst}" @ rm -rf ${BUILD_DIR}/kanboard ${BUILD_DIR}/kanboard-*.zip - @ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/fguillot/kanboard.git + @ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/kanboard/kanboard.git @ cd ${BUILD_DIR}/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install @ rm -rf ${BUILD_DIR}/kanboard/data/*.sqlite @ rm -rf ${BUILD_DIR}/kanboard/data/*.log diff --git a/README.md b/README.md index 49735475..89dd6e25 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,16 @@ Official website: - Open source and self-hosted - Super simple installation - Translated in many languages -- Distributed under [MIT License](https://github.com/fguillot/kanboard/blob/master/LICENSE) +- Distributed under [MIT License](https://github.com/kanboard/kanboard/blob/master/LICENSE) - The complete [list of features are available on the website](https://kanboard.net/features) -- [Change Log](https://github.com/fguillot/kanboard/blob/master/ChangeLog) -- [Documentation](https://github.com/fguillot/kanboard/blob/master/doc/index.markdown) +- [Change Log](https://github.com/kanboard/kanboard/blob/master/ChangeLog) +- [Documentation](https://github.com/kanboard/kanboard/blob/master/doc/index.markdown) Authors ------- - Main developer: [Frédéric Guillot](https://github.com/fguillot) -- [List of contributors](https://github.com/fguillot/kanboard/blob/master/CONTRIBUTORS.md) +- [List of contributors](https://github.com/kanboard/kanboard/blob/master/CONTRIBUTORS.md) Installation and Upgrade ------------------------ diff --git a/app.json b/app.json index 4d41737b..76d9b8e0 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "Kanboard", "description": "Kanboard is a simple visual task board", - "repository": "https://github.com/fguillot/kanboard", + "repository": "https://github.com/kanboard/kanboard", "logo": "https://kanboard.net/assets/img/icon.svg", "keywords": ["kanboard", "kanban", "php", "agile"], "addons": ["heroku-postgresql:hobby-dev"] diff --git a/app/Template/config/about.php b/app/Template/config/about.php index 8e2d1325..8d5a575d 100644 --- a/app/Template/config/about.php +++ b/app/Template/config/about.php @@ -9,7 +9,7 @@
  • - Frédéric Guillot () + Frédéric Guillot ()
  • diff --git a/doc/docker.markdown b/doc/docker.markdown index e55130e5..5b77da76 100644 --- a/doc/docker.markdown +++ b/doc/docker.markdown @@ -93,4 +93,4 @@ References - [Official Kanboard images](https://registry.hub.docker.com/u/kanboard/kanboard/) - [Docker documentation](https://docs.docker.com/) - [Dockerfile stable version](https://github.com/kanboard/docker) -- [Dockerfile dev version](https://github.com/fguillot/kanboard/blob/master/Dockerfile) +- [Dockerfile dev version](https://github.com/kanboard/kanboard/blob/master/Dockerfile) diff --git a/doc/heroku.markdown b/doc/heroku.markdown index 43b15c72..1891efb0 100644 --- a/doc/heroku.markdown +++ b/doc/heroku.markdown @@ -4,7 +4,7 @@ Deploy Kanboard on Heroku You can try Kanboard for free on [Heroku](https://www.heroku.com/). You can use this one click install button or follow the manual instructions below: -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard) +[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/kanboard/kanboard) Requirements ------------ @@ -17,7 +17,7 @@ Manual instructions ```bash # Get the last development version -git clone https://github.com/fguillot/kanboard.git +git clone https://github.com/kanboard/kanboard.git cd kanboard # Push the code to Heroku (You can also use SSH if git over HTTP doesn't work) diff --git a/doc/installation.markdown b/doc/installation.markdown index 2ebe4d14..4955612f 100644 --- a/doc/installation.markdown +++ b/doc/installation.markdown @@ -28,7 +28,7 @@ From the repository (development version) You must install [composer](https://getcomposer.org/) to use this method. -1. `git clone https://github.com/fguillot/kanboard.git` +1. `git clone https://github.com/kanboard/kanboard.git` 2. `composer install --no-dev` 3. Go to the third step just above diff --git a/doc/plugin-authentication.markdown b/doc/plugin-authentication.markdown index 06fdfd8d..e1ca6f01 100644 --- a/doc/plugin-authentication.markdown +++ b/doc/plugin-authentication.markdown @@ -35,6 +35,6 @@ This object must implement the interface `Kanboard\Core\User\UserProviderInterfa Example of authentication plugins --------------------------------- -- [Authentication providers included in Kanboard](https://github.com/fguillot/kanboard/tree/master/app/Auth) +- [Authentication providers included in Kanboard](https://github.com/kanboard/kanboard/tree/master/app/Auth) - [Reverse-Proxy Authentication with LDAP support](https://github.com/kanboard/plugin-reverse-proxy-ldap) - [SMS Two-Factor Authentication](https://github.com/kanboard/plugin-sms-2fa) diff --git a/doc/plugin-group-provider.markdown b/doc/plugin-group-provider.markdown index 4d73b740..31c61aaf 100644 --- a/doc/plugin-group-provider.markdown +++ b/doc/plugin-group-provider.markdown @@ -52,4 +52,4 @@ $groupManager->register(new MyCustomLdapBackendGroupProvider($this->container)); Examples -------- -- [Group providers included in Kanboard (LDAP and Database)](https://github.com/fguillot/kanboard/tree/master/app/Group) +- [Group providers included in Kanboard (LDAP and Database)](https://github.com/kanboard/kanboard/tree/master/app/Group) diff --git a/doc/plugins.markdown b/doc/plugins.markdown index 475bc249..cff3eb6c 100644 --- a/doc/plugins.markdown +++ b/doc/plugins.markdown @@ -5,7 +5,7 @@ Note: The plugin API is **considered alpha** at the moment. Plugins are useful to extend the core functionalities of Kanboard, adding features, creating themes or changing the default behavior. -Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over time and your plugin must be tested with new versions. Always check the [ChangeLog](https://github.com/fguillot/kanboard/blob/master/ChangeLog) for breaking changes. +Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over time and your plugin must be tested with new versions. Always check the [ChangeLog](https://github.com/kanboard/kanboard/blob/master/ChangeLog) for breaking changes. - [Creating your plugin](plugin-registration.markdown) - [Using plugin hooks](plugin-hooks.markdown) diff --git a/doc/update.markdown b/doc/update.markdown index 44f81ff0..4aa59fff 100644 --- a/doc/update.markdown +++ b/doc/update.markdown @@ -10,7 +10,7 @@ Important things to do before updating - **Always make a backup of your data before upgrading** - Check that your backup is valid -- Always read the [change log](https://github.com/fguillot/kanboard/blob/master/ChangeLog) to check for breaking changes +- Always read the [change log](https://github.com/kanboard/kanboard/blob/master/ChangeLog) to check for breaking changes - Always close all user sessions (flush all sessions on the server) From the archive (stable version) -- cgit v1.2.3 From 3ea084fd31a9f820a0c9e15d240cced8e49d5965 Mon Sep 17 00:00:00 2001 From: Hairetdin Date: Sat, 23 Jul 2016 02:12:09 +0500 Subject: Russian documentation added (#2417) --- doc/ru_RU/2fa.markdown | 37 ++ doc/ru_RU/analytics-tasks.markdown | 37 ++ doc/ru_RU/analytics.markdown | 95 ++++ doc/ru_RU/api-json-rpc.markdown | 78 +++ doc/ru_RU/application-configuration.markdown | 54 +++ doc/ru_RU/assets.markdown | 53 ++ doc/ru_RU/automatic-actions.markdown | 128 +++++ doc/ru_RU/board-collapsed-expanded.markdown | 31 ++ doc/ru_RU/board-configuration.markdown | 39 ++ ...-horizontal-scrolling-and-compact-view.markdown | 19 + doc/ru_RU/board-show-hide-columns.markdown | 25 + doc/ru_RU/bruteforce-protection.markdown | 37 ++ doc/ru_RU/calendar-configuration.markdown | 59 +++ doc/ru_RU/calendar.markdown | 31 ++ doc/ru_RU/centos-installation.markdown | 127 +++++ doc/ru_RU/cli.markdown | 331 +++++++++++++ doc/ru_RU/closing-tasks.markdown | 30 ++ doc/ru_RU/cloudron.markdown | 45 ++ doc/ru_RU/coding-standards.markdown | 64 +++ doc/ru_RU/config.markdown | 523 ++++++++++++++++++++ doc/ru_RU/contributing.markdown | 96 ++++ doc/ru_RU/create-tasks-by-email.markdown | 61 +++ doc/ru_RU/creating-projects.markdown | 62 +++ doc/ru_RU/creating-tasks.markdown | 42 ++ doc/ru_RU/cronjob.markdown | 41 ++ doc/ru_RU/currency-rate.markdown | 43 ++ doc/ru_RU/custom-filters.markdown | 36 ++ doc/ru_RU/debian-installation.markdown | 104 ++++ doc/ru_RU/docker.markdown | 134 ++++++ doc/ru_RU/duplicate-move-tasks.markdown | 79 +++ doc/ru_RU/editing-projects.markdown | 25 + doc/ru_RU/email-configuration.markdown | 156 ++++++ doc/ru_RU/env.markdown | 21 + doc/ru_RU/ext-search.markdown | 235 +++++++++ doc/ru_RU/faq.markdown | 162 +++++++ doc/ru_RU/freebsd-installation.markdown | 187 +++++++ doc/ru_RU/gantt-chart-projects.markdown | 60 +++ doc/ru_RU/gantt-chart-tasks.markdown | 66 +++ doc/ru_RU/genindex.markdown | 15 + doc/ru_RU/groups.markdown | 35 ++ doc/ru_RU/heroku.markdown | 72 +++ doc/ru_RU/ical.markdown | 111 +++++ doc/ru_RU/index.markdown | 248 ++++++++++ doc/ru_RU/installation.markdown | 117 +++++ doc/ru_RU/kanban-vs-todo-and-scrum.markdown | 75 +++ doc/ru_RU/keyboard-shortcuts.markdown | 99 ++++ doc/ru_RU/ldap-authentication.markdown | 327 +++++++++++++ doc/ru_RU/ldap-configuration-examples.markdown | 438 +++++++++++++++++ doc/ru_RU/ldap-group-sync.markdown | 153 ++++++ doc/ru_RU/ldap-parameters.markdown | 49 ++ doc/ru_RU/ldap-profile-picture.markdown | 46 ++ doc/ru_RU/link-labels.markdown | 23 + doc/ru_RU/mysql-configuration.markdown | 128 +++++ doc/ru_RU/nice-urls.markdown | 233 +++++++++ doc/ru_RU/nitrous.markdown | 16 + doc/ru_RU/notifications.markdown | 111 +++++ doc/ru_RU/plugin-directory.markdown | 38 ++ doc/ru_RU/plugins.markdown | 167 +++++++ doc/ru_RU/postgresql-configuration.markdown | 92 ++++ doc/ru_RU/project-configuration.markdown | 105 ++++ doc/ru_RU/project-permissions.markdown | 55 +++ doc/ru_RU/project-types.markdown | 27 ++ doc/ru_RU/project-views.markdown | 154 ++++++ doc/ru_RU/recurring-tasks.markdown | 67 +++ doc/ru_RU/requirements.markdown | 137 ++++++ doc/ru_RU/reverse-proxy-authentication.markdown | 138 ++++++ doc/ru_RU/roles.markdown | 44 ++ doc/ru_RU/rss.markdown | 58 +++ doc/ru_RU/screenshots.markdown | 74 +++ doc/ru_RU/search.markdown | 24 + doc/ru_RU/sharing-projects.markdown | 82 ++++ doc/ru_RU/sqlite-database.markdown | 96 ++++ doc/ru_RU/subtasks.markdown | 111 +++++ doc/ru_RU/suse-installation.markdown | 36 ++ doc/ru_RU/swimlanes.markdown | 81 ++++ doc/ru_RU/syntax-guide.markdown | 246 ++++++++++ doc/ru_RU/task-links.markdown | 93 ++++ doc/ru_RU/tests.markdown | 262 ++++++++++ doc/ru_RU/time-tracking.markdown | 112 +++++ doc/ru_RU/transitions.markdown | 60 +++ doc/ru_RU/translations.markdown | 155 ++++++ doc/ru_RU/ubuntu-installation.markdown | 111 +++++ doc/ru_RU/update.markdown | 57 +++ doc/ru_RU/usage-examples.markdown | 193 ++++++++ doc/ru_RU/user-management.markdown | 89 ++++ doc/ru_RU/user-mentions.markdown | 49 ++ doc/ru_RU/user-types.markdown | 26 + doc/ru_RU/vagrant.markdown | 51 ++ doc/ru_RU/webhooks.markdown | 536 +++++++++++++++++++++ doc/ru_RU/what-is-kanban.markdown | 80 +++ doc/ru_RU/windows-apache-installation.markdown | 253 ++++++++++ doc/ru_RU/windows-iis-installation.markdown | 150 ++++++ doc/web.config | 22 + 93 files changed, 9880 insertions(+) create mode 100644 doc/ru_RU/2fa.markdown create mode 100644 doc/ru_RU/analytics-tasks.markdown create mode 100644 doc/ru_RU/analytics.markdown create mode 100644 doc/ru_RU/api-json-rpc.markdown create mode 100644 doc/ru_RU/application-configuration.markdown create mode 100644 doc/ru_RU/assets.markdown create mode 100644 doc/ru_RU/automatic-actions.markdown create mode 100644 doc/ru_RU/board-collapsed-expanded.markdown create mode 100644 doc/ru_RU/board-configuration.markdown create mode 100644 doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown create mode 100644 doc/ru_RU/board-show-hide-columns.markdown create mode 100644 doc/ru_RU/bruteforce-protection.markdown create mode 100644 doc/ru_RU/calendar-configuration.markdown create mode 100644 doc/ru_RU/calendar.markdown create mode 100644 doc/ru_RU/centos-installation.markdown create mode 100644 doc/ru_RU/cli.markdown create mode 100644 doc/ru_RU/closing-tasks.markdown create mode 100644 doc/ru_RU/cloudron.markdown create mode 100644 doc/ru_RU/coding-standards.markdown create mode 100644 doc/ru_RU/config.markdown create mode 100644 doc/ru_RU/contributing.markdown create mode 100644 doc/ru_RU/create-tasks-by-email.markdown create mode 100644 doc/ru_RU/creating-projects.markdown create mode 100644 doc/ru_RU/creating-tasks.markdown create mode 100644 doc/ru_RU/cronjob.markdown create mode 100644 doc/ru_RU/currency-rate.markdown create mode 100644 doc/ru_RU/custom-filters.markdown create mode 100644 doc/ru_RU/debian-installation.markdown create mode 100644 doc/ru_RU/docker.markdown create mode 100644 doc/ru_RU/duplicate-move-tasks.markdown create mode 100644 doc/ru_RU/editing-projects.markdown create mode 100644 doc/ru_RU/email-configuration.markdown create mode 100644 doc/ru_RU/env.markdown create mode 100644 doc/ru_RU/ext-search.markdown create mode 100644 doc/ru_RU/faq.markdown create mode 100644 doc/ru_RU/freebsd-installation.markdown create mode 100644 doc/ru_RU/gantt-chart-projects.markdown create mode 100644 doc/ru_RU/gantt-chart-tasks.markdown create mode 100644 doc/ru_RU/genindex.markdown create mode 100644 doc/ru_RU/groups.markdown create mode 100644 doc/ru_RU/heroku.markdown create mode 100644 doc/ru_RU/ical.markdown create mode 100644 doc/ru_RU/index.markdown create mode 100644 doc/ru_RU/installation.markdown create mode 100644 doc/ru_RU/kanban-vs-todo-and-scrum.markdown create mode 100644 doc/ru_RU/keyboard-shortcuts.markdown create mode 100644 doc/ru_RU/ldap-authentication.markdown create mode 100644 doc/ru_RU/ldap-configuration-examples.markdown create mode 100644 doc/ru_RU/ldap-group-sync.markdown create mode 100644 doc/ru_RU/ldap-parameters.markdown create mode 100644 doc/ru_RU/ldap-profile-picture.markdown create mode 100644 doc/ru_RU/link-labels.markdown create mode 100644 doc/ru_RU/mysql-configuration.markdown create mode 100644 doc/ru_RU/nice-urls.markdown create mode 100644 doc/ru_RU/nitrous.markdown create mode 100644 doc/ru_RU/notifications.markdown create mode 100644 doc/ru_RU/plugin-directory.markdown create mode 100644 doc/ru_RU/plugins.markdown create mode 100644 doc/ru_RU/postgresql-configuration.markdown create mode 100644 doc/ru_RU/project-configuration.markdown create mode 100644 doc/ru_RU/project-permissions.markdown create mode 100644 doc/ru_RU/project-types.markdown create mode 100644 doc/ru_RU/project-views.markdown create mode 100644 doc/ru_RU/recurring-tasks.markdown create mode 100644 doc/ru_RU/requirements.markdown create mode 100644 doc/ru_RU/reverse-proxy-authentication.markdown create mode 100644 doc/ru_RU/roles.markdown create mode 100644 doc/ru_RU/rss.markdown create mode 100644 doc/ru_RU/screenshots.markdown create mode 100644 doc/ru_RU/search.markdown create mode 100644 doc/ru_RU/sharing-projects.markdown create mode 100644 doc/ru_RU/sqlite-database.markdown create mode 100644 doc/ru_RU/subtasks.markdown create mode 100644 doc/ru_RU/suse-installation.markdown create mode 100644 doc/ru_RU/swimlanes.markdown create mode 100644 doc/ru_RU/syntax-guide.markdown create mode 100644 doc/ru_RU/task-links.markdown create mode 100644 doc/ru_RU/tests.markdown create mode 100644 doc/ru_RU/time-tracking.markdown create mode 100644 doc/ru_RU/transitions.markdown create mode 100644 doc/ru_RU/translations.markdown create mode 100644 doc/ru_RU/ubuntu-installation.markdown create mode 100644 doc/ru_RU/update.markdown create mode 100644 doc/ru_RU/usage-examples.markdown create mode 100644 doc/ru_RU/user-management.markdown create mode 100644 doc/ru_RU/user-mentions.markdown create mode 100644 doc/ru_RU/user-types.markdown create mode 100644 doc/ru_RU/vagrant.markdown create mode 100644 doc/ru_RU/webhooks.markdown create mode 100644 doc/ru_RU/what-is-kanban.markdown create mode 100644 doc/ru_RU/windows-apache-installation.markdown create mode 100644 doc/ru_RU/windows-iis-installation.markdown create mode 100644 doc/web.config (limited to 'doc') diff --git a/doc/ru_RU/2fa.markdown b/doc/ru_RU/2fa.markdown new file mode 100644 index 00000000..0787c720 --- /dev/null +++ b/doc/ru_RU/2fa.markdown @@ -0,0 +1,37 @@ +Двух-уровневая аутентификация +============================= + +Любой пользователь может включить [двух-уровневую аутентификацию](http://en.wikipedia.org/wiki/Two_factor_authentication). После успешного входа, разовый код (6 знаков) запрашивается у пользователя для получения доступа в Канборд. + +Этот код присылается в программу на вашем смартфоне. + +Канборд использует [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) основанный на [RFC 6238](http://tools.ietf.org/html/rfc6238). + +Имеется много программ совместимых со стандартной системой TOTP. Например, вы можете использовать эти приложения, бесплатные и с открытым исходным кодом: + +- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry) +- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS) +- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (Command line utility on Unix/Linux) + +Эти системы могут работать офлайн и вам не нужно иметь мобильную связь. + +Настройка +--------- + +1. Перейдите в пользовательский профиль +2. Слева нажмите **Двухфакторная авторизация** и поставте галочку в чекбоке +3. Секретный ключ сгенерируется для вас + +![2FA](https://kanboard.net/screenshots/documentation/2fa.png) + +Рисунок. Двухуровневая аутентификация. + + +- Вы должны сохранить секретный ключ в вашей TOTP программе. Если вы используете сматрфон, то просто сосканируйте QR код с помощью FreeOTP или Google Authenticator. +- Каждый раз, когда вы будете входить в Канборд, будет запрашиваться новый код +- Не забудьте протестировать ваше устройство, перед тем как закрыть вашу сессию + +Новый секретный ключ генерируется каждый раз при включении/выключении этой возможности. + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/analytics-tasks.markdown b/doc/ru_RU/analytics-tasks.markdown new file mode 100644 index 00000000..176a4616 --- /dev/null +++ b/doc/ru_RU/analytics-tasks.markdown @@ -0,0 +1,37 @@ +Аналитика для задач +=================== + +На странице детального просмотра задачи, в левом боковом меню, для каждой задачи имеется раздел аналитики. + +Затраченное время и время цикла +------------------------------- + +![Lead and cycle time](https://kanboard.net/screenshots/documentation/task-lead-cycle-time.png) + +Рисунок. Затраченное время и время цикла + + +- Затраченное время - время между созданием задачи и датой завершения (закрытие задачи). +- Время цикла - время между началом испольнения задачи и датой завершения. +- Если задача не закрыта, то для расчета используется текущее время вместо даты завершения. +- Если дата начала выполнения задачи не указана, то время цикла не может быть расчитано. + + +**Заметка**: Вы можете настроить автоматическое создание даты начала выполения задачи, когда вы перемещаете задачу в определенную колонку. + + +Время затраченное в каждой колонке +---------------------------------- + +![Time spent into each column](https://kanboard.net/screenshots/documentation/time-into-each-column.png) + +Рисунок. Время затраченное в каждой колонке + + + +- Этот график показывает сколько времени задача находилась в каждой колонке. +- Затраченное время расчитывается до закрытия задачи. + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/analytics.markdown b/doc/ru_RU/analytics.markdown new file mode 100644 index 00000000..2af6de34 --- /dev/null +++ b/doc/ru_RU/analytics.markdown @@ -0,0 +1,95 @@ +Аналитика +========= + +Каждый проект имеет анлитический раздел. В зависимости от того как вы используете Канборд, вы можете видеть подобные отчеты: + +Перераспределение(загрузка) пользователей +----------------------------------------- + +![User repartition](https://kanboard.net/screenshots/documentation/user-repartition.png) + +Перераспределение(загрузка) пользователей + + +Круговая диаграмма, представленная выше, показыает количество открытых задач назначенных определенным пользователям. + + +Распределение задач +------------------- + +![Task distribution](https://kanboard.net/screenshots/documentation/task-distribution.png) + +Рисунок. Распределение задач + + + +На рисунке выше, представлена круговая диаграмма, которая показывает количество открытых задач в определенных колонках. + + + +Накопительная диаграмма +----------------------- + +![Cumulative flow diagram](https://kanboard.net/screenshots/documentation/cfd.png) + +Рисунок. Накопительная диаграмма + + +- Эта диаграмма отображает количество задач выполненных в каждой колонке в определенный промежуток времени. +- Счетчик задач записывается для каждой колонки каждый день. +- Если вы хотите исключить закрытые задачи, измените [глобальные настройки проекта](project-configuration.markdown). + + +Заметка: Для того чтобы увидеть этот график, вам нужно иметь, как минимум, данные за два дня. + + +Диаграмма сгорания +------------------ + +![Burndown chart](https://kanboard.net/screenshots/documentation/burndown-chart.png) + +Рисунок. Диаграмма сгорания + + + +[Диаграмма сгорания](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%B0%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B0_%D1%81%D0%B3%D0%BE%D1%80%D0%B0%D0%BD%D0%B8%D1%8F_%D0%B7%D0%B0%D0%B4%D0%B0%D1%87) доступна для каждого проекта. + + +- Эта диаграмма отображает время затраченное на выполнение работы. +- Канборд использует историю задач для генерации этой диаграммы. +- Сумма историй задач для каждой колонки пересчитывается каждый день. + +Среднее время затраченное в каждой колонке +------------------------------------------ + +![Average time spent into each column](https://kanboard.net/screenshots/documentation/average-time-spent-into-each-column.png) + +Рисунок. Среднее время затраченное в каждой колонке + + +Этот график показывает среднее время затраченное в каждой колонке для последних 1000 задач. + +- Канборд использует для подсчета данных переходы задач между колонками. +- Затраченное время подсчитывается до закрытия задачи. + +Среднее время выполнения и время цикла +-------------------------------------- + +![Average time spent into each column](https://kanboard.net/screenshots/documentation/average-lead-cycle-time.png) + +Рисунок. Среднее время затраченное в каждой колонке + +Эта диаграмма показывает Среднее время выполнения и цикла для последних 1000 задач. +- Время выполнения - время между созданием задачи и датой завершения. +- Время цикла - время между указанной датой начала выполнения задачи и датой завершения. +- Если задача не закрыта, текущая дата будет использована вместо даты завершения. + +Эти данные подсчитываются и записываются каждый день на протяжении жизни проекта. + +Заметка: Не забудьте выполнить [ежедневные cronjob](cronjob.markdown) для того чтобы иметь точную статистику. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/api-json-rpc.markdown b/doc/ru_RU/api-json-rpc.markdown new file mode 100644 index 00000000..257f83ec --- /dev/null +++ b/doc/ru_RU/api-json-rpc.markdown @@ -0,0 +1,78 @@ +Json-RPC API +============ + + +API пользователя и приложения +----------------------------- + + +Имеется два типа доступа к API: + +### API приложения[¶](#application-api "Ссылка на этот заголовок") + +- Доступ к API осуществляется с использованием пользователя “jsonrpc” и ключа, доступного в настройках +- Доступ ко всем процедурам +- Не проверяются права доступа +- Нет пользовательской сессии на сервере +- Этот доступ можно использовать для: утилит миграции/импорта данных, создания задач из других систем и т.д. + +### API пользователя[¶](#user-api "Ссылка на этот заголовок") + +- Доступ к API под пользовательскими учетными данными (имя пользователя и пароль) +- Доступ к ограниченному набору процедур +- Проверка прав доступа к проекту +- На сервере создается пользовательская сессия +- Этот доступ можно использовать для клиентов: мобильных/десктопных приложений, утилит коммандной строки и т.д. + +Безопасность +------------ + +- Всегда используйте протокол HTTPS с действительным сертификатом +- Если вы делаете мобильное приложение, позаботьтесь о безопасном хранении учетных данных пользователя на мобильном устройстве +- После 3 неправильных подключений к пользовательскому api, пользователь может разблокировать свою учетную запись только с использованием формы входа +- Двухуровневая аутентификация пока не доступна через API + + + +Протокол +-------- + + +Канборд использует протокол Json-RPC для взаимодействия с внешними программами. + +JSON-RPC - протокол удаленного вызова процедур в формате JSON. По сути своей, тот же XML-RPC, но использующий формат JSON. + +Мы используем [протокол версии 2](http://www.jsonrpc.org/specification). Вы можете вызывать API используя `POST`{.docutils .literal} HTTP запрос. + +Канборд поддерживает пакетные запросы, поэтому вы можете делать многократные API вызовы в одном HTTP запросе. Это, в частности, удобно для мобильных клиентов с высокой сетевой задержкой. + + +Использование +------------- + +- [Аутентификация](api-authentication.markdown) +- [Примеры](api-examples.markdown) +- [Приложение](api-application-procedures.markdown) +- [Проекты](api-project-procedures.markdown) +- [Права доступа к проекту](api-project-permission-procedures.markdown) +- [Доски](api-board-procedures.markdown) +- [Колонки](api-column-procedures.markdown) +- [Дорожки](api-swimlane-procedures.markdown) +- [Категории](api-category-procedures.markdown) +- [Автоматические дейсвия](api-action-procedures.markdown) +- [Задачи](api-task-procedures.markdown) +- [Подзадачи](api-subtask-procedures.markdown) +- [Файлы](api-file-procedures.markdown) +- [Ссылки](api-link-procedures.markdown) +- [Комментарии](api-comment-procedures.markdown) +- [Пользователи](api-user-procedures.markdown) +- [Группы](api-group-procedures.markdown) +- [Члены группы](api-group-member-procedures.markdown) +- [Специфичные запросы пользователя](api-me-procedures.markdown) + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/application-configuration.markdown b/doc/ru_RU/application-configuration.markdown new file mode 100644 index 00000000..d8b2661e --- /dev/null +++ b/doc/ru_RU/application-configuration.markdown @@ -0,0 +1,54 @@ +Настройки приложения +==================== + +Некоторые параметры для приложения могут быть изменены на странице настроек. Только администратор может сделать эти настройки. +Выберите в правом выпадающем меню **Настройки**, затем в слева выберите **Настройки приложения**. + +![Application settings](https://kanboard.net/screenshots/documentation/application-settings.png) + +Рисунок. Настройки приложения + + +URL приложения[¶](#application-url "Ссылка на этот заголовок") +-------------------------------------------------------------- + +Этот параметр используется для email уведомлений. В тексте сообщения будет содержаться ссылка на задачу в Канборде. + + +Язык[¶](#language "Ссылка на этот заголовок") +--------------------------------------------- + +Язык приложения может быть изменен в любое время. Язык устанавливается для всех пользователей Канборд. + + +Часовой пояс[¶](#time-zone "Ссылка на этот заголовок") +------------------------------------------------------ + +По умолчанию, Канборд использует часовой пояс UTC, но вы можете указать любой часовой пояс. Список содержит все поддерживаемые часовые пояса для вашего веб сервера. + + +Формат даты[¶](#date-format "Ссылка на этот заголовок") +------------------------------------------------------- + +Формать даты, который используется для полей дата. Например, дата завершения задачи. + +Канборд поддерживает 4 разных формата: + +- ДД/ММ/ГГГГ +- ММ/ДД/ГГГГ (по умолчанию) +- ГГГГ/ММ/ДД +- ММ.ДД.ГГГГ + +Формат [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) всегда принимается (YYYY-MM-DD or YYYY\_MM\_DD). + + +Пользовательский стиль CSS[¶](#custom-stylesheet "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + +Вы можете сделать свой стиль CSS для Канборд или улучшить имеющийся стиль. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/assets.markdown b/doc/ru_RU/assets.markdown new file mode 100644 index 00000000..9a0124c5 --- /dev/null +++ b/doc/ru_RU/assets.markdown @@ -0,0 +1,53 @@ +Как создать asset (Javascript и CSS файлы) +========================================== + + +Файлы CSS стилей и Javascript объединены вместе и минимизированы. + +- Оригинальные файлы CSS хранятся в каталоге `assets/css/src/*.css`{.docutils .literal} +- Оригинальные файлы Javascript хранятся в каталоге `assets/js/src/*.js`{.docutils .literal} +- `assets/*/vendor.min.*`{.docutils .literal} - внешние зависимости объединены и минимизированы +- `assets/*/app.min.*`{.docutils .literal} - исходный код приложения объединены и минимизированы + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + +- [NodeJS](https://nodejs.org/) с `npm`{.docutils .literal} + + +Сборка файлов Javascript и CSS[¶](#building-javascript-and-css-files "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------ + + +Канборд использует [Gulp](http://gulpjs.com/) для сборки asset и [Bower](http://bower.io/) для управления зависимостями. Эти утилиты устанавлены в проекте как зависимости NodeJS. + + +### Запустить все[¶](#run-everything "Ссылка на этот заголовок") + + make static + +### Собрать `vendor.min.js`{.docutils .literal} и `vendor.min.css`{.docutils .literal}[¶](#build-vendor-min-js-and-vendor-min-css "Ссылка на этот заголовок") + + gulp vendor + +### Собрать `app.min.js`{.docutils .literal}[¶](#build-app-min-js "Ссылка на этот заголовок") + + gulp js + + +### Собрать `app.min.css`{.docutils .literal}[¶](#build-app-min-css "Ссылка на этот заголовок") + + gulp css + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + +Сборка asset невозможна из архива Kanboard, вы должны клонировать репозиторий. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/automatic-actions.markdown b/doc/ru_RU/automatic-actions.markdown new file mode 100644 index 00000000..1e0631c3 --- /dev/null +++ b/doc/ru_RU/automatic-actions.markdown @@ -0,0 +1,128 @@ +Автоматизация процессов +======================= + + +Для минимизации пользовательских действий, Kanboard поддерживает автоматизацию процессов. + +Каждый автоматизированный процесс представляет следующее: + +- Ожидание наступления события +- Выполняется действие при наступлении этого события +- В результате устанавливается определенный параметр + +Каждый проект может иметь свой набор автоматических процессов. Автоматические процессы доступны в панеле настроек (**Меню** -\> **Настройки**) **Автоматические действия**. + + +Добавление нового действия[¶](#add-a-new-action "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + +Нажмите на ссылку **Добавить новое действие**. + +![Automatique action](screenshots/automatic-action-creation.png) + +Рисунок. Автоматическое действие. + + +- Выберете действие +- Затем, выберете событие +- И в завершении, задайте параметр + + +Список доступных действий[¶](#list-of-available-actions "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + + +- Создать комментарий из внешнего источника +- Добавлять запись при перемещении задачи между колонками +- Автоматически назначать категорию по цвету +- Изменить категорию основываясь на внешнем ярлыке +- Автоматически назначать категории на основе ссылки +- Автоматически назначать цвет по категории +- Назначить цвет, когда задача перемещается в определенную колонку +- Изменение цвета задач при использовании ссылки на определенные задачи +- Назначить определенный цвет пользователю +- Назначить задачу тому кто выполнит действие +- Назначить задачу пользователю, который произвел изменение в колонке +- Назначить задачу определенному пользователю +- Изменить назначенного основываясь на внешнем имени пользователя +- Закрыть задачу +- Закрыть задачу в выбранной колонке +- Создать задачу из внешнего источника +- Создать дубликат задачи в другом проекте +- Отправить задачу по email +- Переместить задачу в другой проект +- Переместить задачу в другую колонку, когда она назначена пользователю +- Переносить задачи в другую колонку при изменении категории +- Переместить задачу в другую колонку, когда назначение снято +- Открыть задачу +- Автоматическое обновление даты начала + + +Примеры[¶](#examples "Ссылка на этот заголовок") +------------------------------------------------ + + +Здесь предствалены примеры использованные в реальной жизни: + +### Когда я перемещаю задачу в колонку “Выполнено”, автоматически закрывать эту задачу[¶](#when-i-move-a-task-to-the-column-done-automatically-close-this-task "Ссылка на этот заголовок") + +- Выберите действия: **Закрыть задачу в выбранной колонке** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = Выполнено** (это колонка в которую будет перемещена задача) + +### Когда я перемещаю задачу в колонку “На утверждение”, назначить эту задачу определенному пользователю.[¶](#when-i-move-a-task-to-the-column-to-be-validated-assign-this-task-to-a-specific-user "Ссылка на этот заголовок") + +- Выберите действие: **Назначить задачу определенному пользователю** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = На утверждение** и **Пользователь = Петр** (Петр - наш тестировщик) + +### Когда я перемещаю задачу в колонку “В работе”, назначить эту задачу определенному пользователю[¶](#when-i-move-a-task-to-the-column-work-in-progress-assign-this-task-to-the-current-user "Ссылка на этот заголовок") + +- Выберите действие: **Назначить задачу пользователю, который произвел изменение в колонке** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = В работе** + + +### Когда задача выполнена, скопировать эту задачу в другой проект[¶](#when-a-task-is-completed-duplicate-this-task-to-another-project "Ссылка на этот заголовок") + +Предположим, мы имеем два проекта “Заказы покупателей” и “Производство”. Когда заказ в проекте “Заказы покупателей” утвержден, копируем этот заказ в проект “Производство”. + +- Выбираем действие: **Создать дубликат задачи в другом проекте** +- Выбираем событие: **Завершение задачи** +- Установите параметр действия: **Колонка = Утвержден** и **Проект = Производство** + + +### Когда задача перемещена в последнюю колонку, переместить эту задачу в другой проект[¶](#when-a-task-is-moved-to-the-last-column-move-the-exact-same-task-to-another-project "Ссылка на этот заголовок") + + +Предположим, мы имеем два проекта “Идеи” и “Разработка”, когда идея утверждена, перемещаем эту задачу в проект “Разработка”. + +- Выберите действие: **Переместить задачу в другой проект** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = Утверждена** и **Проект = Разработка** + +### Я хочу назначать автоматически цвет для пользователя Петр[¶](#i-want-to-assign-automatically-a-color-to-the-user-bob "Ссылка на этот заголовок") + +- Выберите действие: **Назначить определенный цвет пользователю** +- Выберите событие: **Изменен назначенный** +- Установите параметр действия: **Цвет = Зеленый** и **Назначена = Петр** + + +### Я хочу назначить цвет автоматически для определенной категории “Важные запросы”[¶](#i-want-to-assign-a-color-automatically-to-the-defined-category-feature-request "Ссылка на этот заголовок") + +- Выберите действие: **Автоматически назначать цвет по категории** +- Выберите событие: **Создание или изменение задачи** +- Установите параметр действия: **Цвет = Голубой** и **Категория = Важные запросы** + + +### Я хочу устанавливать дату начала автоматически когда задача перемещена в колонку “В работе”[¶](#i-want-to-set-the-start-date-automatically-when-the-task-is-moved-to-the-column-work-in-progress "Ссылка на этот заголовок") + +- Выберите действие: **Автоматическое обновление даты начала** +- Выберите событие: **Переместить задачу в другую колонку** +- Установите параметр действия: **Колонка = В работе** + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-collapsed-expanded.markdown b/doc/ru_RU/board-collapsed-expanded.markdown new file mode 100644 index 00000000..a19981a5 --- /dev/null +++ b/doc/ru_RU/board-collapsed-expanded.markdown @@ -0,0 +1,31 @@ +Компактное и развернутое отображение задач +========================================== + +Задачи на Доске могут быть отображены в компактном или развернутом виде. Переключение между компактным и развернутым видом может быть выполнено с помощью горячей клавиши **“s”** или в раскрывающемся Меню (слева вверху) -\> Развернуть задачи или Свернуть задачи. + + +Компактный вид[¶](#collapsed-mode "Ссылка на этот заголовок") +------------------------------------------------------------- + + +![Tasks collapsed](screenshots/board-collapsed-mode.png) + +Рисунок. Задачи представлены в компактном виде + +- Если для задачи назначен исполнитель, то инициалы исполнителя показываются рядом с номером задачи; +- Если заголовок задачи слишком длинный, вы можете подвести курсор мышки над задачей и полный заголовок задачи отобразится во всплывающем окне. + + + +Развернутый вид[¶](#expanded-mode "Ссылка на этот заголовок") +------------------------------------------------------------- + + +![Tasks expanded](screenshots/board-expanded-mode.png) +Рисунок. Развернутый вид + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-configuration.markdown b/doc/ru_RU/board-configuration.markdown new file mode 100644 index 00000000..fb4fb58d --- /dev/null +++ b/doc/ru_RU/board-configuration.markdown @@ -0,0 +1,39 @@ +Настройка Доски +=============== + + +В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки Доски**. + +![Board settings](https://kanboard.net/screenshots/documentation/board-settings.png) + +Рисунок. Настройка Доски + + +Подстветка задач[¶](#task-highlighting "Ссылка на этот заголовок") +------------------------------------------------------------------ + +Эта опция позволяет подсвечивать задачу, которая была перенесена недавно. + +Установите значение 0 для выключения подсветки. По умолчанию установлено значение 172800 секунд (2 дня) + +Перемещенные задачи будут подсвечиваться в течении двух дней. + + +Период обновления для публичных досок[¶](#refresh-interval-for-public-board "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + +Если вы создаете публичную доску, то страница, по умолчанию, будет обновляться каждые 60 секунд. + + +Период обновления для частных досок[¶](#refresh-interval-for-private-board "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------ + +Когда в вашем браузере открыта Доска, Канборд проверяет обновления изменение каждые 10 секунд. + +Процесс обновления реализован по технологии Ajax. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown b/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown new file mode 100644 index 00000000..9eaa5c9e --- /dev/null +++ b/doc/ru_RU/board-horizontal-scrolling-and-compact-view.markdown @@ -0,0 +1,19 @@ +Горизонтальная прокрутка и компактный вид +========================================= + +Когда ширины экрана не хватает для отображения всех колонок, то внизу появляется горизонтальная прокрутка. + +Однако, можно переключится на компактный вид доски для отображения всех колонок на вашем экране. + + +![Switch to compact mode](screenshots/board-compact-mode.png) + +Рисунок. Переключение на компактное представление. + +Переключится между горизонтальной прокруткой и компактным видом можно с помощью горячей клавиши **“c”** или в левом верхнем раскрывающемся “Меню” -\> “Компактный вид” или “Широкий вид”. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/board-show-hide-columns.markdown b/doc/ru_RU/board-show-hide-columns.markdown new file mode 100644 index 00000000..5c333b5c --- /dev/null +++ b/doc/ru_RU/board-show-hide-columns.markdown @@ -0,0 +1,25 @@ +Показать и скрыть колонки на Доске +================================== + +Вы можете показать и скрыть колонки на Доске очень просто: + +![Hide a column](screenshots/hide-column.png) + +Рисунок. Спрятать колонку. + + +Чтобы скрыть (спрятать) колонку , откройте выпадающее меню колонки. + +![Show a column](screenshots/show-column.png) + +Рисунок.Показать колонку. + + +Для отображения скрытой колонки нажмите “иконку плюс” + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/bruteforce-protection.markdown b/doc/ru_RU/bruteforce-protection.markdown new file mode 100644 index 00000000..25e50880 --- /dev/null +++ b/doc/ru_RU/bruteforce-protection.markdown @@ -0,0 +1,37 @@ +Защита от Brute Force +===================== + +Защита от Brute Force (подбор пароля методом перебора) в Канборде работает на уровне учетной записи пользователя: + +- После 3 неправильных вводов пароля для одного и того же пользователя, на форме входа появляется капча для предотвращения дальнейшего подбора программой-роботом. +- После 6 неудачных вводов пароля, учетная запись пользователя блокируется на 15 минут. + +Эта возможность работает только для метода аутентификации с использованием формы входа на веб странице. + +Однако, **после трех ошибочных аутентификаций через пользовательский API, учетная запись может быть разблокирована с использованием формы входа на веб странице** + +В Канборде нет блокировок по IP адресу, потому что программы-роботы используют множество анонимных прокси. Однако, вы можете использовать внешнюю утилиту, например [fail2ban](http://www.fail2ban.org) , чтобы избежать массового сканирования. + +Настройки защиты от Brute Force могут быть изменены в следующих переменных: + + // Enable captcha after 3 authentication failure + + define('BRUTEFORCE_CAPTCHA', 3); + + + + // Lock the account after 6 authentication failure + + define('BRUTEFORCE_LOCKDOWN', 6); + + + + // Lock account duration in minutes + + define('BRUTEFORCE_LOCKDOWN_DURATION', 15); + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/calendar-configuration.markdown b/doc/ru_RU/calendar-configuration.markdown new file mode 100644 index 00000000..bd6d604e --- /dev/null +++ b/doc/ru_RU/calendar-configuration.markdown @@ -0,0 +1,59 @@ +Настройки календаря +=================== + +В правом верхнем выпадающем меню выберите **Настройки**, затем, слева, выберите **Настройки календаря**. + + +![Calendar settings](https://kanboard.net/screenshots/documentation/calendar-settings.png) + +Рисунок. Настройки календаря + + +В Канборде имеется два вида Календаря: + +- Календарь проекта +- Пользовательский календарь (доступен в левом меню Инфопанели) + + +Календарь проекта[¶](#project-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------ + +Эти календари показывают задачи с указанной датой создания или датой начала и датой завершения. + +### Показать задачи в зависимости от даты создания[¶](#show-tasks-based-on-the-creation-date "Ссылка на этот заголовок") + +- Дата начала в календаре показывает дату создания задачи. +- Конечная дата показывает дату завершения. + + +### Показать задачи в зависимости от даты начала[¶](#show-tasks-based-on-the-start-date "Ссылка на этот заголовок") + +- Дата начала в календаре показывает дату начала задачи. +- Эта дата должна быть установлена вручную. +- Конечная дата показывает дату завершения. +- Если не указать дату начала, то задача не будет отображена в календаре. + + + +Пользовательский календарь[¶](#user-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------------ + +Пользовательский календарь показывает только задачи назначенные пользователю и, опционально, информацию о подзадачах. + + +### Показать подзадачи, основанные на отслеживании времени[¶](#show-sub-tasks-based-on-the-time-tracking "Ссылка на этот заголовок") + +- Показывает подзадачи в календаре из записей таблицы отслеживания времени. +- Пересечения в пользовательской таблице времени также подсчитываются. + + +### Показывать оценку подзадач (прогнозирование будущих работ)[¶](#show-sub-task-estimates-forecast-of-future-work "Ссылка на этот заголовок") + +- Показывает оценку будущих работ для подзадач в статусе “для исполнения” и с указанным значением “оценка”. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/calendar.markdown b/doc/ru_RU/calendar.markdown new file mode 100644 index 00000000..f0658c89 --- /dev/null +++ b/doc/ru_RU/calendar.markdown @@ -0,0 +1,31 @@ +Календарь +========= + + +Календарь может быть представлен в двух видах: + +- Представление в проекте с использование фильтров (доступно на Доске) +- Пользовательское представление (доступно в рабочей панели и в пользовательском разделе) + +В Календаре можно увидеть следующую информацию: + +- Задачи с “датой испольнения”, отображаются наверху. **Дата испольнения может быть изменена перемещением задачи на другой день**. +- Задачи с датой создания или датой начала. **Эти события не могут быть изменены в календаре**. +- Отслеживание времени подзадачи. Все записанные временные диапазоны будут отображены в Календаре. +- Подсчеты, прогнозы затрачиваемого время на подзадачу. + +![Calendar](https://kanboard.net/screenshots/documentation/calendar.png) + +Рисунок. Календарь + + +Настроки Календаря могут быть изменены на странице **Настройки** + +Заметка: Дата исполения не содержит информацию о времени. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/centos-installation.markdown b/doc/ru_RU/centos-installation.markdown new file mode 100644 index 00000000..95808586 --- /dev/null +++ b/doc/ru_RU/centos-installation.markdown @@ -0,0 +1,127 @@ +Инсталяция Канборд на Centos +============================ + + +**Внимание**: Некоторые возможности Канборда требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + +Centos 7[¶](#centos-7 "Ссылка на этот заголовок") +------------------------------------------------- + +Установите PHP и Apache: + + + yum install -y php php-mbstring php-pdo php-gd unzip wget + + +По умолчанию, Centos 7 использует PHP 5.4.16 и Apache 2.4.6. + + + +Перезапустите Apache: + + + + systemctl restart httpd.service + + + +Установите Канборд: + + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R apache:apache kanboard/data + + rm kanboard-latest.zip + + + +Если включен SELinux, убедитесь что пользователь веб сервера Apache имеет права на запись в директорию data: + + + + chcon -R -t httpd_sys_content_rw_t /var/www/html/kanboard/data + + + +Убедитесь, что Канборд может посылать email сообщения и делать внешние сетевые запросы, например с SELinux: + + + + setsebool -P httpd_can_network_connect=1 + + + +Позволяет делать внешние подключения если используется LDAP, SMTP, Web hooks или другая интеграция. + + + +Centos 6.x[¶](#centos-6-x "Ссылка на этот заголовок") +----------------------------------------------------- + + + +Установите PHP и Apache: + + + + yum install -y php php-mbstring php-pdo php-gd unzip wget + + + +По умолчанию, Centos 6.5 использует PHP 5.3.3 и Apache 2.2.15. + + + +Включите короткие теги: + + + +- Отредактируйте файл `/etc/php.ini`{.docutils .literal} + + + +- Измените строку `short_open_tag = On`{.docutils .literal} (вместо `short_open_tag = Off`{.docutils .literal}) + + + +Перезапустите Apache: + + + + service httpd restart + + + +Установите Канборд: + + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R apache:apache kanboard/data + + rm kanboard-latest.zip + + + +Готово. Можете работать с Канборд. Откройте в браузере `http://ваш_сервер/kanboard/`{.docutils .literal}. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cli.markdown b/doc/ru_RU/cli.markdown new file mode 100644 index 00000000..9c7b56a7 --- /dev/null +++ b/doc/ru_RU/cli.markdown @@ -0,0 +1,331 @@ +Интерфейс командной строки +========================== + + + +Канборд обеспечивает простой интерфейс командной строки, которым можно воспользоваться только из Unix терминала. Эта возможность доступна только с локальной машины. + + + +Интерфейс командной строки полезен для выполнения команд вне процессов веб сервера. + + + +Использование[¶](#usage "Ссылка на этот заголовок") +--------------------------------------------------- + + + +- Откройте терминал и перейдите в директорию Канборд (например: `cd /var/www/kanboard`) + + + +- Выполните команду `./kanboard` + + + + + + + + Kanboard version master + + + + Usage: + + command [options] [arguments] + + + + Options: + + -h, --help Display this help message + + -q, --quiet Do not output any message + + -V, --version Display this application version + + --ansi Force ANSI output + + --no-ansi Disable ANSI output + + -n, --no-interaction Do not ask any interactive question + + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + + + + Available commands: + + cronjob Execute daily cronjob + + help Displays help for a command + + list Lists commands + + export + + export:daily-project-column-stats Daily project column stats CSV export (number of tasks per column and per day) + + export:subtasks Subtasks CSV export + + export:tasks Tasks CSV export + + export:transitions Task transitions CSV export + + locale + + locale:compare Compare application translations with the fr_FR locale + + locale:sync Synchronize all translations based on the fr_FR locale + + notification + + notification:overdue-tasks Send notifications for overdue tasks + + plugin + + plugin:install Install a plugin from a remote Zip archive + + plugin:uninstall Remove a plugin + + plugin:upgrade Update all installed plugins + + projects + + projects:daily-stats Calculate daily statistics for all projects + + trigger + + trigger:tasks Trigger scheduler event for all tasks + + user + + user:reset-2fa Remove two-factor authentication for a user + + user:reset-password Change user password + + + +Доступные команды[¶](#available-commands "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +### Экспорт задач в формате CSV[¶](#tasks-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:tasks + + + +Пример: + + + + ./kanboard export:tasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +Данные CSV передаются в `stdout`. + + + +### Экспорт подзадач в формате CSV[¶](#subtasks-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:subtasks + + + +Пример: + + + + ./kanboard export:subtasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Экспорт перемещения задач в формате CSV[¶](#task-transitions-csv-export "Ссылка на этот заголовок") + + + +Применение: + + + + ./kanboard export:transitions + + + +Пример: + + + + ./kanboard export:transitions 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Экспорт ежедневных сведений в формате CSV[¶](#export-daily-summaries-data-in-csv "Ссылка на этот заголовок") + + + +Экспортированные данные будут выведены в стандартный вывод: + + + + ./kanboard export:daily-project-column-stats + + + +Пример: + + + + ./kanboard export:daily-project-column-stats 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv + + + +### Отправка уведомлений для просроченных задач[¶](#send-notifications-for-overdue-tasks "Ссылка на этот заголовок") + + + +Email сообщения будут отправлены всем пользователям, у которых включено оповещение. + + + + ./kanboard notification:overdue-tasks + + + +Необязательные параметры: + + + +- `--show`: Показывать отправку уведомлений + + + +- `--group`: Группировать все просроченные задачи для одного пользователя (со всех проектов) на один email + + + +- `--manager`: Посылать все просроченные задачи менеджеру (менеджерам) проекта в одном email сообщении + + + +Вы можете просмотреть просроченные задачи с помощью параметра `--show`: + + + +```bash +./kanboard notification:overdue-tasks --show ++-----+---------+------------+------------+--------------+----------+ +| Id | Title | Due date | Project Id | Project name | Assignee | ++-----+---------+------------+------------+--------------+----------+ +| 201 | Test | 2014-10-26 | 1 | Project #0 | admin | +| 202 | My task | 2014-10-28 | 1 | Project #0 | | ++-----+---------+------------+------------+--------------+----------+ +``` + + +### Запуск ежедневной калькуляции статистики[¶](#run-daily-project-stats-calculation "Ссылка на этот заголовок") + + + +Эта команда считает статистику для каждого проекта: + + + + ./kanboard projects:daily-stats + + Run calculation for Project #0 + + Run calculation for Project #1 + + Run calculation for Project #10 + + + +### Триггеры для задач[¶](#trigger-for-tasks) + + + +Эта команда посылает “событие для ежедневных фоновых заданий” для всех открытых задач в каждом проекте. + + + + ./kanboard trigger:tasks + + Trigger task event: project_id=2, nb_tasks=1 + + + +### Сброс пароля пользователя[¶](#reset-user-password "Ссылка на этот заголовок") + + + + ./kanboard user:reset-password my_user + + + +Будет запрошен пароль и подтверждение. Символы не отображаются на экране. + + + +### Удаление двухуровневой аутентификации для пользователя[¶](#remove-two-factor-authentication-for-a-user "Ссылка на этот заголовок") + + + + ./kanboard user:reset-2fa my_user + + + +### Установка плагина[¶](#install-a-plugin "Ссылка на этот заголовок") + + + + ./kanboard plugin:install https://github.com/kanboard/plugin-github-auth/releases/download/v1.0.1/GithubAuth-1.0.1.zip + + + +Заметка: Установленные файлы будут иметь теже права, что и у текущего пользователя + + + +### Удаление плагина[¶](#remove-a-plugin "Ссылка на этот заголовок") + + + + ./kanboard plugin:uninstall Budget + + + +### Обновление всех плагинов[¶](#upgrade-all-plugins "Ссылка на этот заголовок") + + + + ./kanboard plugin:upgrade + + * Updating plugin: Budget Planning + + * Plugin up to date: Github Authentication + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/closing-tasks.markdown b/doc/ru_RU/closing-tasks.markdown new file mode 100644 index 00000000..ae91757d --- /dev/null +++ b/doc/ru_RU/closing-tasks.markdown @@ -0,0 +1,30 @@ +Закрытие задач +============== + +Когда задача закрыта, то она скрывается на Доске. + +Не смотря на это, вы можете в любой момент зайти в список закрытых задач используя запрос **status:closed** в любой форме поиска или просто выбрать фильтр “Закрытые задачи” в выпадающем меню. + +Имеется два пути для закрытия задачи: - На Доске выбрать задачу и выпадающем меню выбрать **Закрыть задачу** + +![Close a task from drop-down menu](https://kanboard.net/screenshots/documentation/menu-close-task.png) + +Рисунок. Закрытие задачи, используя выпадающее меню. + + +или - Используя детальное представление задачи, выбрать **Закрыть задачу** в меню боковой панели (слева) + + +![Close task](https://kanboard.net/screenshots/documentation/closing-tasks.png) + +Рисунок. Закрытие задачи. + + + +**Заметка**: Когда вы закрываете задачу, у всех не выполненных подзадач будет изменен статус на “Выполнено” + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cloudron.markdown b/doc/ru_RU/cloudron.markdown new file mode 100644 index 00000000..2e41d0d0 --- /dev/null +++ b/doc/ru_RU/cloudron.markdown @@ -0,0 +1,45 @@ +Как запустить Канборд на Cloudron +================================= + + +[Cloudron](https://cloudron.io) приватный смартсервер, на котором вы можете установить веб приложения, такие как Канборд. Вы можете установить Канборд в определенном домене, при этом каждой инсталяции создавается резервная копия и поддерживается новая версия Канборда автоматически. + + + +[![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=net.kanboard.cloudronapp) + + + +Учетные записи[¶](#accounts "Ссылка на этот заголовок") +------------------------------------------------------- + + +Приложение плотно интегрируется с системой Управления пользователями Cloudron (через LDAP). Только пользователи Cloudron могут войти в Канборд. Плюс, любой администратор Cloudron становится администратором Канборда автоматически. + + +Установка плагинов[¶](#installing-plugins "Ссылка на этот заголовок") +--------------------------------------------------------------------- + + + +Плагины могут быть установлены и настроены с помощью утилиты [Cloudron CLI](https://cloudron.io/references/cli.html). Для подробной информации смотрите [описание приложения](https://cloudron.io/appstore.html?app=net.kanboard.cloudronapp). + + + +Исходный код приложения[¶](#application-source-code "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Исходный код приложения Cloudron находится [здесь](https://github.com/cloudron-io/kanboard-app). + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/coding-standards.markdown b/doc/ru_RU/coding-standards.markdown new file mode 100644 index 00000000..b6100375 --- /dev/null +++ b/doc/ru_RU/coding-standards.markdown @@ -0,0 +1,64 @@ +Стандарты используемые при написании кода +========================================= + + + +Код PHP[¶](#php-code "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Отступ: 4 пробела + + + +- Перевод строки: Unix =\> `\n`{.docutils .literal} + + + +- Кодировка: UTF-8 + + + +- Используйте только открытые теги ` `\n`{.docutils .literal} + + + +Код CSS[¶](#css-code "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Отступ: 4 пробела + + + +- Перевод строки: Unix =\> `\n`{.docutils .literal} + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/config.markdown b/doc/ru_RU/config.markdown new file mode 100644 index 00000000..b0419966 --- /dev/null +++ b/doc/ru_RU/config.markdown @@ -0,0 +1,523 @@ +Конфигурационный файл +===================== + + + +Вы можете изменить базовые настройки Канборда добавив файл `config.php` в корень проекта или в каталог `data`. Вы, также, можете переименовать файл `config.default.php` в `config.php` и установить желаемые значения. + + +Включение/выключение режима отладки[¶](#enable-disable-debug-mode "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------- + + + + define('DEBUG', true); + + define('LOG_DRIVER', 'file'); // Other drivers are: syslog, stdout, stderr or file + + + +Обработчик логов может быть определен если вы включите режим отладки. Режим отладки фиксирует все SQL запросы и время затрачиваемое на генерацию страниц. + + + +Плагины[¶](#plugins "Ссылка на этот заголовок") +----------------------------------------------- + + + +Каталог плагинов: + + + + define('PLUGINS_DIR', 'data/plugins'); + + + +Включение/выключение установки плагинов через интерфейс пользователя: + + + + define('PLUGIN_INSTALLER', true); // Default is true + + + +Каталог для загружаемых файлов[¶](#folder-for-uploaded-files "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + + define('FILES_DIR', 'data/files'); + + + +Включение/выключение переопределения url адресов[¶](#enable-disable-url-rewrite "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------- + + + + define('ENABLE_URL_REWRITE', false); + + + +Настройка email[¶](#email-configuration "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + + // E-mail address for the "From" header (notifications) + + define('MAIL_FROM', 'notifications@kanboard.local'); + + + + // Mail transport to use: "smtp", "sendmail" or "mail" (PHP mail function) + + define('MAIL_TRANSPORT', 'mail'); + + + + // SMTP configuration to use when the "smtp" transport is chosen + + define('MAIL_SMTP_HOSTNAME', ''); + + define('MAIL_SMTP_PORT', 25); + + define('MAIL_SMTP_USERNAME', ''); + + define('MAIL_SMTP_PASSWORD', ''); + + define('MAIL_SMTP_ENCRYPTION', null); // Valid values are "null", "ssl" or "tls" + + + + // Sendmail command to use when the transport is "sendmail" + + define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); + + + +Настройки базы данных[¶](#database-settings "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + + // Database driver: sqlite, mysql or postgres (sqlite by default) + + define('DB_DRIVER', 'sqlite'); + + + + // Mysql/Postgres username + + define('DB_USERNAME', 'root'); + + + + // Mysql/Postgres password + + define('DB_PASSWORD', ''); + + + + // Mysql/Postgres hostname + + define('DB_HOSTNAME', 'localhost'); + + + + // Mysql/Postgres database name + + define('DB_NAME', 'kanboard'); + + + + // Mysql/Postgres custom port (null = default port) + + define('DB_PORT', null); + + + + // Mysql SSL key + + define('DB_SSL_KEY', null); + + + + // Mysql SSL certificate + + define('DB_SSL_CERT', null); + + + + // Mysql SSL CA + + define('DB_SSL_CA', null); + + + +Настройки LDAP[¶](#ldap-settings "Ссылка на этот заголовок") +------------------------------------------------------------ + + + + // Enable LDAP authentication (false by default) + + define('LDAP_AUTH', false); + + + + // LDAP server hostname + + define('LDAP_SERVER', ''); + + + + // LDAP server port (389 by default) + + define('LDAP_PORT', 389); + + + + // By default, require certificate to be verified for ldaps:// style URL. Set to false to skip the verification + + define('LDAP_SSL_VERIFY', true); + + + + // Enable LDAP START_TLS + + define('LDAP_START_TLS', false); + + + + // By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive) + + // Set to true if you want to preserve the case + + define('LDAP_USERNAME_CASE_SENSITIVE', false); + + + + // LDAP bind type: "anonymous", "user" or "proxy" + + define('LDAP_BIND_TYPE', 'anonymous'); + + + + // LDAP username to use with proxy mode + + // LDAP username pattern to use with user mode + + define('LDAP_USERNAME', null); + + + + // LDAP password to use for proxy mode + + define('LDAP_PASSWORD', null); + + + + // LDAP DN for users + + // Example for ActiveDirectory: CN=Users,DC=kanboard,DC=local + + // Example for OpenLDAP: ou=People,dc=example,dc=com + + define('LDAP_USER_BASE_DN', ''); + + + + // LDAP pattern to use when searching for a user account + + // Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))' + + // Example for OpenLDAP: 'uid=%s' + + define('LDAP_USER_FILTER', ''); + + + + // LDAP attribute for username + + // Example for ActiveDirectory: 'samaccountname' + + // Example for OpenLDAP: 'uid' + + define('LDAP_USER_ATTRIBUTE_USERNAME', 'uid'); + + + + // LDAP attribute for user full name + + // Example for ActiveDirectory: 'displayname' + + // Example for OpenLDAP: 'cn' + + define('LDAP_USER_ATTRIBUTE_FULLNAME', 'cn'); + + + + // LDAP attribute for user email + + define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail'); + + + + // LDAP attribute to find groups in user profile + + define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof'); + + + + // LDAP attribute for user avatar image: thumbnailPhoto or jpegPhoto + + define('LDAP_USER_ATTRIBUTE_PHOTO', ''); + + + + // LDAP attribute for user language, example: 'preferredlanguage' + + // Put an empty string to disable language sync + + define('LDAP_USER_ATTRIBUTE_LANGUAGE', ''); + + + + // Allow automatic LDAP user creation + + define('LDAP_USER_CREATION', true); + + + + // LDAP DN for administrators + + // Example: CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local + + define('LDAP_GROUP_ADMIN_DN', ''); + + + + // LDAP DN for managers + + // Example: CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local + + define('LDAP_GROUP_MANAGER_DN', ''); + + + + // Enable LDAP group provider for project permissions + + // The end-user will be able to browse LDAP groups from the user interface and allow access to specified projects + + define('LDAP_GROUP_PROVIDER', false); + + + + // LDAP Base DN for groups + + define('LDAP_GROUP_BASE_DN', ''); + + + + // LDAP group filter + + // Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*)) + + define('LDAP_GROUP_FILTER', ''); + + + + // LDAP user group filter + + // If this filter is configured, Kanboard will search user groups in LDAP_GROUP_BASE_DN + + // Example for OpenLDAP: (&(objectClass=posixGroup)(memberUid=%s)) + + define('LDAP_GROUP_USER_FILTER', ''); + + + + // LDAP attribute for the group name + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +Настройки аутентификации Reverse-Proxy[¶](#reverse-proxy-authentication-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------ + + + + // Enable/disable the reverse proxy authentication + + define('REVERSE_PROXY_AUTH', false); + + + + // Header name to use for the username + + define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); + + + + // Username of the admin, by default blank + + define('REVERSE_PROXY_DEFAULT_ADMIN', ''); + + + + // Default domain to use for setting the email address + + define('REVERSE_PROXY_DEFAULT_DOMAIN', ''); + + + +Настройки аутентификации RememberMe[¶](#rememberme-authentication-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------ + + + + // Enable/disable remember me authentication + + define('REMEMBER_ME_AUTH', true); + + + +Настройки Secure HTTP headers[¶](#secure-http-headers-settings "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------ + + + + // Enable or disable "Strict-Transport-Security" HTTP header + + define('ENABLE_HSTS', true); + + + + // Enable or disable "X-Frame-Options: DENY" HTTP header + + define('ENABLE_XFRAME', true); + + + +Запись событий[¶](#logging "Ссылка на этот заголовок") +------------------------------------------------------ + + + +По умолчанию, Канборд записывает не все события. Если вы хотите включить запись событий, вы должны установить обработчик логов. + + + + // Available log drivers: syslog, stderr, stdout or file + + define('LOG_DRIVER', ''); + + + + // Log filename if the log driver is "file" + + define('LOG_FILE', __DIR__.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); + + + +Защита от Brute-force[¶](#brute-force-protection "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + + + + // Enable captcha after 3 authentication failure + + define('BRUTEFORCE_CAPTCHA', 3); + + + + // Lock the account after 6 authentication failure + + define('BRUTEFORCE_LOCKDOWN', 6); + + + + // Lock account duration in minute + + define('BRUTEFORCE_LOCKDOWN_DURATION', 15); + + + +Сессии[¶](#session "Ссылка на этот заголовок") +---------------------------------------------- + + + + // Session duration in second (0 = until the browser is closed) + + // See http://php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime + + define('SESSION_DURATION', 0); + + + +Проксирование клиентских HTTP[¶](#http-client-proxy "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Если внешние запросы HTTP необходимо пробрасывать через прокси: + + + + define('HTTP_PROXY_HOSTNAME', ''); + + define('HTTP_PROXY_PORT', '3128'); + + define('HTTP_PROXY_USERNAME', ''); + + define('HTTP_PROXY_PASSWORD', ''); + + + +Другие настройки[¶](#various-settings "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + + // Escape html inside markdown text + + define('MARKDOWN_ESCAPE_HTML', true); + + + + // API alternative authentication header, the default is HTTP Basic Authentication defined in RFC2617 + + define('API_AUTHENTICATION_HEADER', ''); + + + + // Hide login form, useful if all your users use Google/Github/ReverseProxy authentication + + define('HIDE_LOGIN_FORM', false); + + + + // Disabling logout (for external SSO authentication) + + define('DISABLE_LOGOUT', false); + + + + // Override API token stored in the database, useful for automated tests + + define('API_AUTHENTICATION_TOKEN', 'My unique API Token'); + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/contributing.markdown b/doc/ru_RU/contributing.markdown new file mode 100644 index 00000000..54917067 --- /dev/null +++ b/doc/ru_RU/contributing.markdown @@ -0,0 +1,96 @@ +Руководство для участников проекта +================================== + + + +Как я могу помочь проекту?[¶](#how-can-i-help "Ссылка на этот заголовок") +------------------------------------------------------------------------- + + + +Канборд пока не идеален, поэтому есть несколько вариантов помочь проекту: + + + +- Присылать отзывы +- Сообщать об ошибках +- Добавлять или обновлять переводы +- Улучшать документацию +- Писать код +- Рассказать друзьям, что Канборд отличная программа :) + + + +Перед тем как начать большое дело, создайте новое “обсуждение вопроса” (issue) на [https://github.com/fguillot/kanboard/issues](https://github.com/fguillot/kanboard/issues) и объясните ваше предложение. + + + +Я хочу внести предложения по проекту[¶](#i-want-to-give-feedback "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------- + + + +- У вас есть идея по улучшению (пользовательский интерфейс или другие возможности) +- Посмотрите в обсуждениях (issue), может ваша идея уже предложена кем-то +- Откройте новое обсуждение (issue) +- Опишите вашу идею +- Вы можете проголосовать +1 за имеющиеся предложения + + +Я хочу сообщить об ошибке[¶](#i-want-to-report-a-bug "Ссылка на этот заголовок") +-------------------------------------------------------------------------------- + +- Убедитесь, что обсуждение вопроса (issue) ранее не публиковалось +- Откройте новую заявку (ticket) +- Опишите, что именно не работает +- Опишите, как воспроизвести ошибку (последовательность, как вы вышли на данную ошибку) +- Опишите ваше окружение (версию Канборда, какая ОС, веб сервер, версию PHP, база данных и версия, хостинг провайдер) + + +Я хочу перевести Канборд на другой язык[¶](#i-want-to-translate-kanboard "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------- + +Канборд уже переведен на несколько языков. Вы можете улучшить эти переводы. Некоторые переводы еще не завершены. Для того, чтобы сделать перевод, ознакомтесь с [руководством по переводу на другой язык](translations.markdown). + + +Я хочу улучшить документацию[¶](#i-want-to-improve-the-documentation "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------ + +- Вы считаете, что что-то недостаточно хорошо описано, имеются грамматические или орфографические ошибки, что-то еще. +- Документация написана в формате Markdown и хранится в каталоге `docs`{.docutils .literal}. +- Редактируйте файлы и присылайте pull-request. +- Документация на официальном вебсайте синхронизируется с репозиторием. + + +Я хочу внести свой вклад в код[¶](#i-want-to-contribute-to-the-code "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------- + +Pull-requests всегда приветствуются, однако, чтобы они были приняты, вы должны следовать следующим указаниям: + +- **Перед тем как внести большое изменение или переделать дизайн, откройте новую заявку (ticket) для обсуждения.** +- Если вы хотите добавить новую возможность, уважайте филосовию Канборда: **Мы фокусируемся на простоте**, мы не хотим иметь раздутую программу. +- Это же относится и к пользовательскому интерфейсу: **простота и производительность** +- Присылайте только по одному pull-request для новой возможности или исправления ошибки. +- Небольшие pull-request легче просмотреть и быстрее влить в проект. +- Убедитесь, что [модульные тесты выполняются успешно](tests.markdown). +- Уважайте [стандарты кодирования](coding-standards.markdown). +- Пишите код, который могут поддерживать другие, избегайте дублирования, используйте лучше практики PHP. + +В любом случае, если вы не уверены в чем-то - открывайте новую заявку (ticket) + + +Рассказать друзьям, что Канборд отличная программа :)[¶](#tell-your-friends-that-kanboard-is-awesome "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------------- + +Если вы используете Канборд, покажите его и окружающим. Расскажите всем о прелестях бесплатного и опенсурсного программного обеспечения. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/create-tasks-by-email.markdown b/doc/ru_RU/create-tasks-by-email.markdown new file mode 100644 index 00000000..baddc682 --- /dev/null +++ b/doc/ru_RU/create-tasks-by-email.markdown @@ -0,0 +1,61 @@ +Создание задач через email +========================== + + +Вы можете создавать задачи отправляя email (сообщения через электронную почту). Эта возможность доступна при использовании плагинов. + +В настоящий момент, Канборд поддерживает три внешних плагина: + + +- [Mailgun](https://github.com/kanboard/plugin-mailgun) +- [Sendgrid](https://github.com/kanboard/plugin-sendgrid) +- [Postmark](https://github.com/kanboard/plugin-postmark) + +Эти плагины позволяют обрабатывать входящие электронные сообщения (email) без дополнительной настройки SMTP сервера. + +При получении плагином email сообщения, плагин передает это сообщение в веб транслятор Канборда. + + +Обработка входящих email сообщений[¶](#incoming-emails-workflow "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------- + + +1. Вы отправляете email сообщение на определенный адрес, например **something+myproject@inbound.mydomain.tld** +2. Email сообщение перенаправляется на SMTP сервер +3. SMTP провайдер передает в веб сервис Канборда email сообщение в JSON формате или в формате multipart/form-data +4. Канборд обрабатывает полученное email сообщение и создает задачу в указанном проекте + +**Заметка**: Новые задачи автоматически создаются в первой колонке. + + +Формат email сообщения[¶](#email-format "Ссылка на этот заголовок") +------------------------------------------------------------------- + +- Email адрес до знака **@** должен содержать разделитель **плюс**, например **kanboard+project123** +- Строка следующая после знака плюс означает **Идентификатор проекта**, например, проект **Проект 123** может иметь идентификатор проекта **project123**. Идентификатор проекта можно задать в свойствах проекта **Меню** -\> **Настройки** -\> **Изменить проект** -\> **Идентификатор**. **Идентификатор** должен быть из цифр и латинских букв. +- Тема из email сообщения становится названием задачи +- Текст email сообщения становится описанием задачи (в формате Markdown) + +Email сообщения могут быть написаны в текстовом или HTML формате. **Канборд сам переконвертирует формат сообщения в Markdown** + + +Безопастность и требования[¶](#security-and-requirements "Ссылка на этот заголовок") +------------------------------------------------------------------------------------ + +- Веб транслятор Канборд защищен случайным ключом +- Email адрес отправителя должен быть такой же как и у пользователя Канборд +- Проект в Канборде должен иметь уникальный идентификатор +- Отправитель email сообщения должен быть участником проекта + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/creating-projects.markdown b/doc/ru_RU/creating-projects.markdown new file mode 100644 index 00000000..b878a538 --- /dev/null +++ b/doc/ru_RU/creating-projects.markdown @@ -0,0 +1,62 @@ +Создание проектов +================= + + +Kanboard может содержать одновременно несколько проектов. Проекты могут быть следующих типов: + +- Командный проект +- Приватный проект для одного пользователя + +Создание проекта для нескольких пользователей[¶](#creating-projects-for-multiple-users "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------ + +- Только пользователи с ролью администратор и менеджер могут создавать такие проекты +- Можно добавлять к проекту пользователей и группы + +На рабочей панели нажмите ссылку **Новый проект**: + +![Project creation form](screenshots/new-project.png) + +Рисунок. Форма создания проекта. + + +Теперь надо только добавить название для проекта! Легко, не правда ли? + + +Создание приватного проекта[¶](#creating-a-private-project "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + +- Любой пользователь Kanboard может создать приватный проект +- **Нет** возможности добавлять участников к приватному проекту +- Только владелец приватного проекта и администратор могут получить доступ к проекту + + +На рабочей панели нажмите **Новый проект с ограниченным доступом**. + + + +Создание проекта из другого проекта[¶](#creating-projects-from-another-project "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + +При создании нового проекта у вас есть возможность использовать данные другого (ранее созданного) проекта: + +- Разрешения +- Действия +- Дорожки +- Категории +- Задачи + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/creating-tasks.markdown b/doc/ru_RU/creating-tasks.markdown new file mode 100644 index 00000000..ec2922a8 --- /dev/null +++ b/doc/ru_RU/creating-tasks.markdown @@ -0,0 +1,42 @@ +Создание задач +============== + + +На Доске нажмите значок плюс рядом с названием колонки: + + +![Task creation from the board](https://kanboard.net/screenshots/documentation/task-creation-board.png) + +Рисунок. Создание задачи на Доске + + +Далее появится форма создания задачи: + +![Task creation form](https://kanboard.net/screenshots/documentation/task-creation-form.png) + +Рисунок. Форма создания задачи. + + +Только поле **Название** является обязательным полем для заполнения. + + +Описание полей: + +- **Название**: Название вашей задачи, которое будет отображаться на доске. +- **Описание**: Позволяет вам добавить больше информации о задаче, содержимое может содержать синтаксис [Markdown](syntax-guide.markdown). +- **Создать другую задачу**: Отметьте этот чекбокс если вы хотите создать похожую задачу (некоторые поля будут заполнены). +- **Назначена**: Пользователь, которому будет назначена для выполнения эта задача. +- **Категория**: Только одна категория может быть назначена задаче. +- **Колонка**: Колонка в которой задача будет создана, ваша задача будет помещена вниз. +- **Цвет**: Выберите цвет для карточки. +- **Сложность**: используется в быстрых управлениях проектами (Scrum); сложность - это число, которое говорит команде проекта насколько тяжело выполнить задачу. Обычно пользователи используют шкалу Фибоначи. +- **Запланировано часов**: Планирование времени, которое будет затрачено на выполнение задачи. Измеряется в часах. +- **Сделать до**: Просроченные задачи будут иметь дату завершения красного цвета, а предстоящие задачи будут иметь дату завершения черного цвета. + +**-** + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/cronjob.markdown b/doc/ru_RU/cronjob.markdown new file mode 100644 index 00000000..c3bb5f6d --- /dev/null +++ b/doc/ru_RU/cronjob.markdown @@ -0,0 +1,41 @@ +Ежедневные фоновые задачи +========================= + + +Для корректной работы, Канборд должен запускать ежедневные фоновые задачи. На Unix платформах этот процесс выполнятся в `cron`. + +Фоновые задачи необходимы для следующих возможностей: + +- Отчеты и аналитика (подсчет ежедневной статистики для каждого проекта) +- Рассылка оповещений для просроченных задач +- Выполнение автоматических действий подключенных к событиям “Ежедневные фоновые процессы для задач” + + +Настройка на Unix и Linux платформах[¶](#configuration-on-unix-and-linux-platforms "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------- + +Для создания фоновых задач под операционной системой Unix/Linux используются разные решения. Здесь приведен пример для Ubuntu 14.04. Для других систем процедура похожа. + + +Отредактируйте crontab под пользователем вашего веб сервера: + + + sudo crontab -u www-data -e + + +Пример запуска ежедневной фоновой задачи в 8 утра: + + + 0 8 * * * cd /path/to/kanboard && ./kanboard cronjob >/dev/null 2>&1 + + +Примечание: процес выполнения фоновых задач должен иметь права доступа к вашей базе данных в случае если вы используете Sqlite. Обычно, достаточно запускать фоновую задачу под пользователем веб сервера. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/currency-rate.markdown b/doc/ru_RU/currency-rate.markdown new file mode 100644 index 00000000..6d7dbc3e --- /dev/null +++ b/doc/ru_RU/currency-rate.markdown @@ -0,0 +1,43 @@ +Курсы валют +=========== + + +Каждый пользователь может иметь предопределенный ежечасный курс для разных валют. Если вы хотите вручную занести курсы валют, то вы можете указать ставку в соответсвии с курсом. + +Эта опция используются для расчета бюджета проекта. + +![Currency Rate](https://kanboard.net/screenshots/documentation/currency-rate.png) + +Рисунок. Курсы валют + + +Для настроек курса валют выберите, справа вверху в выпадающем меню, **Настройки** -\> затем, слева, **Курсы валют**. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/custom-filters.markdown b/doc/ru_RU/custom-filters.markdown new file mode 100644 index 00000000..60630860 --- /dev/null +++ b/doc/ru_RU/custom-filters.markdown @@ -0,0 +1,36 @@ +Пользовательские фильтры +======================== + +Пользовательские фильтры позволяют вам сохранять любые поисковые запросы. Таким образом, вы можете легко расширить стандартные фильтры и сохранить часто используемые поисковые запросы. + +- Пользовательские фильтры сохраняются в проекте и имеют привязку к создателю. +- Если создатель фильтра является менеджером проекта, то он может предоставить этот фильтр всем участникам проекта. + + +Создание фильтра[¶](#filter-creation "Ссылка на этот заголовок") +---------------------------------------------------------------- + + +Перейдите в **Меню** -\> **Пользовательские фильтры** или **Меню** -\> **Настройки** -\> **Пользовательские фильтры** + +![Custom Filter Creation](https://kanboard.net/screenshots/documentation/custom-filter-creation.png) + +Рисунок. Создание пользовательского фильтра. + + + +Созданый фильтр появится на Доске рядом со стандартными фильтрами + +![Custom Filter Dropdown](https://kanboard.net/screenshots/documentation/custom-filter-dropdown.png) + +Рисунок. Выпадающий список - Пользовательский фильтр. + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/debian-installation.markdown b/doc/ru_RU/debian-installation.markdown new file mode 100644 index 00000000..2c33465e --- /dev/null +++ b/doc/ru_RU/debian-installation.markdown @@ -0,0 +1,104 @@ +Как установить Канборд на Debian? +================================= + +Некоторые возможности Канборда требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + +Debian 8 (Jessie)[¶](#debian-8-jessie "Ссылка на этот заголовок") +----------------------------------------------------------------- + + +Установите Apache и PHP: + + + apt-get update + + apt-get install -y php5 php5-sqlite php5-gd unzip + + service apache2 restart + + + +Установите Канборд: + + + cd /var/www/html + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + +Debian 7 (Wheezy)[¶](#debian-7-wheezy "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + apt-get update + + apt-get install -y php5 php5-sqlite php5-gd unzip + + + +Установите Канборд: + + + + cd /var/www + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + +Debian 6 (Squeeze)[¶](#debian-6-squeeze "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + apt-get update + + apt-get install -y libapache2-mod-php5 php5-sqlite php5-gd unzip + + + +Установите Канборд: + + + + cd /var/www + + wget https://kanboard.net/kanboard-latest.zip + + unzip kanboard-latest.zip + + chown -R www-data:www-data kanboard/data + + rm kanboard-latest.zip + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/docker.markdown b/doc/ru_RU/docker.markdown new file mode 100644 index 00000000..358ade73 --- /dev/null +++ b/doc/ru_RU/docker.markdown @@ -0,0 +1,134 @@ +Как запустить Канборд с Docker? +=============================== + + +Канборд можно легко запустить с [Docker](https://www.docker.com). + + +Размер образа, приблизительно, **50MB** содержит: + +- [Alpine Linux](http://alpinelinux.org/) +- The [process manager S6](http://skarnet.org/software/s6/) +- Nginx +- PHP-FPM + + +Канборд запускает фоновые задачи каждый день в полночь. Переписывание URL (URL rewriting) включено в базовой конфигурации. + +Когда контейнер запущен, использование памяти около **20MB**. + + +Использование стабильной версии[¶](#use-the-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + + +Для получения последней стабильной версии Канборда используйте тег **stable**: + + + + docker pull kanboard/kanboard + + docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:stable + + + +Использование разрабатываемой версии (автоматической сборки)[¶](#use-the-development-version-automated-build "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------------------------------------- + + + +Каждый новый коммит в репозитории вызывает новую сборку в [Docker Hub](https://registry.hub.docker.com/u/kanboard/kanboard/). + + + + docker pull kanboard/kanboard + + docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:latest + + + +Используя **разрабатываемую версию** Канборда с тегом **latest**, вы принимаете на себя все риски нестабильной версии. + + + +Создание своего образа Docker[¶](#build-your-own-docker-image "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------- + +Для сборки своего образа, в репозитории Канборда имеется `Dockerfile`{.docutils .literal}. Склонируйте репозиторий Канборда и выполните следующую команду: + + + + docker build -t youruser/kanboard:master . + + + +или + + + + make docker-image + + + +Для запуска вашего контейнера в фоновом режиме на порту 80: + + + + docker run -d --name kanboard -p 80:80 -t youruser/kanboard:master + + + +Тома[¶](#volumes "Ссылка на этот заголовок") +-------------------------------------------- + + +Вы можете прикрепить 2 тома к вашему контейнеру: + +- Каталог с данными: `/var/www/kanboard/data` +- Каталог с плагинами: `/var/www/kanboard/plugins` + + + +Используйте опцию `-v` для монтирования тома на удаленной машине как описано в [официальной документации Docker](https://docs.docker.com/engine/userguide/containers/dockervolumes/). + + + +Обновление вашего контейнера[¶](#upgrade-your-container "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + +- Загрузите новый образ +- Удалите старый контейнер +- Перезапустите новый контейнер с теми же томами + + +Переменные окружения[¶](#environment-variables "Ссылка на этот заголовок") +-------------------------------------------------------------------------- + + +Список переменных окружения доступен на [этой странице](env.markdown). + + + +Файлы конфигурации[¶](#config-files "Ссылка на этот заголовок") +--------------------------------------------------------------- + +- Контейнер уже содержит конфигурационный файл расположенный в `/var/www/kanboard/config.php`. +- Вы можете сохранить свой конфиг файл в томе с данными: `/var/www/kanboard/data/config.php`. + + + +Ссылки[¶](#references "Ссылка на этот заголовок") +------------------------------------------------- + +- [Официальные образы Канборд](https://registry.hub.docker.com/u/kanboard/kanboard/) +- [Документация Docker](https://docs.docker.com/) +- [Стабильная версия Dockerfile](https://github.com/kanboard/docker) +- [Разрабатываемая версия Dockerfile](https://github.com/fguillot/kanboard/blob/master/Dockerfile) + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/duplicate-move-tasks.markdown b/doc/ru_RU/duplicate-move-tasks.markdown new file mode 100644 index 00000000..48cec06c --- /dev/null +++ b/doc/ru_RU/duplicate-move-tasks.markdown @@ -0,0 +1,79 @@ +Дублирование и перенос задач +============================ + + +Создание копии задачи в том же проекте[¶](#duplicate-a-task-into-the-same-project "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------- + + +Перейдите в детальное представление задачи и выберите в боковой панели (слева) **Клонировать**. + +![Task Duplication](https://kanboard.net/screenshots/documentation/task-duplication.png) + +Рисунок. Создание копии задачи. + + +Новая задача будет создана с теми же свойствами как и у оригинальной задачи. + + +Создание копии задачи в другой проект[¶](#duplicate-a-task-to-another-project "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + + +Перейдите в детальное представление задачи и выберите в боковом меню (слева) **Клонировать в другой проект**. + +![Task Duplication Another Project](https://kanboard.net/screenshots/documentation/task-duplication-another-project.png) + +Рисунок. Создание копии задачи в другой проект. + + +При выборе проекта в выпадающем списке, показываются только те проекты в которых вы являетесь участниками. + +Перед тем как скопировать задачу, Канборд просит вас указать свойства проекта (куда будет копироваться), потому что проекты могуг иметь разные столбцы, дорожки и т.д. + +Вам нужно указать: + +- Дорожку, в которую скопируется задача +- Колонку +- Категорию +- Испольнителя + +Перемещение задачи в другой проект[¶](#move-a-task-to-another-project "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------- + +Перейдите в детальное представление задачи и выберите в боковом меню **Переместить в другой проект** + +Процедура перемещения задачи в другой проект такая же как и при копировании, вы должны указать новые свойства для задачи. + + +Список копируемых полей[¶](#list-of-fields-duplicated "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + +Ниже приведен список полей (свойств), которые будут скопированы: + +- заголовок +- описание +- дата\_исполнение +- цвет\_id +- проект\_id +- колонка\_id +- владелец\_id +- оценка +- категория\_id +- время\_запланировано +- дорожка\_id +- повторение\_статус +- повторение\_триггер +- повторение\_фактор +- повторение\_timeframe +- повторение\_basedate + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/editing-projects.markdown b/doc/ru_RU/editing-projects.markdown new file mode 100644 index 00000000..5ff81f90 --- /dev/null +++ b/doc/ru_RU/editing-projects.markdown @@ -0,0 +1,25 @@ +Редактирование проектов +======================= + + +Проект может быть переименован и выключен в любое время + +Для переименования проекта нажмите на ссылку **“Изменить проект”** (для перехода выберите **Меню** -\> **Настройки**) + + +![Project edition](screenshots/project-edition.png) + +Рисунок. Изменение проекта. + +- Дата начала и дата завершения используются при генерации диаграммы Ганта +- Описание отображается как подсказка на Доске и на странице со списком проектов +- Администраторы и менеджеры проекта могут сделать приватный проект доступным для других пользователей установив галочку в чекбоксе **“Приватный проект”** +- Вы можете сделать публичный проект приватным. + +Внимание: Когда вы делаете приватный проект из публичного, все пользователи ранее присоединенные к проекту будут иметь доступ. Ограничьте список пользователей для вашего приватного проекта. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/email-configuration.markdown b/doc/ru_RU/email-configuration.markdown new file mode 100644 index 00000000..e04aca7b --- /dev/null +++ b/doc/ru_RU/email-configuration.markdown @@ -0,0 +1,156 @@ +Настройка email +=============== + + +Настройки пользователя[¶](#user-settings "Ссылка на этот заголовок") +-------------------------------------------------------------------- + +Для получение уведомлений на email, пользователи Канборда должны иметь: + +- Включенные уведомления, должны быть включены в профиле пользователя +- Должен быть прописан правильный email адрес в профиле пользователя +- Быть участником проекта, который отсылает уведомления + + +Примечание: Пользователь, выполнивший вход в Канборд и выполняющий действие, не будет получать уведомления. Уведомления будут получать только другие участники проекта. + + + +Email шлюзы[¶](#email-transports "Ссылка на этот заголовок") +------------------------------------------------------------ + +В Канборд доступны несколько шлюзов для email: + +- SMTP +- Sendmail +- Встроенная mail функция PHP +- Другие методы могут предоставить внешние плагины: Postmark, Sendgrid and Mailgun + + +Настройки сервера[¶](#server-settings "Ссылка на этот заголовок") +----------------------------------------------------------------- + +По умолчанию, Канборд использует встроенную в PHP функцию для передачи email сообщений. Обычно не требуется дополнительных настроек, если ваш сервер уже может отправлять email сообщения. + +Если вы захотите использовать другие методы: SMTP протокол и Sendmail, то ниже приведены инструкции по настройке. + +### Настройка SMTP[¶](#smtp-configuration "Ссылка на этот заголовок") + +Переименуйте файл `config.default.php`{.docutils .literal} в `config.php`{.docutils .literal} и измените следующие значения: + + + // We choose "smtp" as mail transport + + define('MAIL_TRANSPORT', 'smtp'); + + + + // We define our server settings + + define('MAIL_SMTP_HOSTNAME', 'mail.example.com'); + + define('MAIL_SMTP_PORT', 25); + + + + // Credentials for authentication on the SMTP server (not mandatory) + + define('MAIL_SMTP_USERNAME', 'username'); + + define('MAIL_SMTP_PASSWORD', 'super password'); + + + +Возможно понадобится использовать шифрованное подключение TLS или SSL: + + + define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls" + + +### Настройка Sendmail[¶](#sendmail-configuration "Ссылка на этот заголовок") + +По умолчанию команда отправки сообщений выглядит так `/usr/sbin/sendmail -bs`{.docutils .literal}, но вы можете изменить ее в файле конфигурации. + +Например: + + + + // We choose "sendmail" as mail transport + + define('MAIL_TRANSPORT', 'sendmail'); + + + + // If you need to change the sendmail command, replace the value + + define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); + + + +### Встроенная mail функция PHP[¶](#php-native-mail-function "Ссылка на этот заголовок") + +Это конфигурация по умолчанию: + + + + define('MAIL_TRANSPORT', 'mail'); + + + +### Email адрес отправителя[¶](#the-sender-email-address "Ссылка на этот заголовок") + +По умолчанию, сообщения отправляются с адресом отправителя `notifications@kanboard.local`{.docutils .literal}. На этот адрес нельзя ответить. + +Вы можете настроить этот адрес изменив значение константы `MAIL_FROM`{.docutils .literal} в вашем конфигурационном файле. + + + define('MAIL_FROM', 'kanboard@mydomain.tld'); + + +Это может быть полезным, если ваш SMTP сервер не принимает неправильные адреса. + + +### Как отобразить ссылку на задачу в уведомлении?[¶](#how-to-display-a-link-to-the-task-in-notifications "Ссылка на этот заголовок") + +Чтобы сделать это, вы должны указать URL вашего установленного Канборда в [Настройках приложения](application-configuration.markdown). + +Например: + + + +- [http://demo.kanboard.ru/](http://demo.kanboard.ru/) + + + +- /имясервера/kanboard/ + + + +- [http://kanboard.mydomain.com/](http://kanboard.mydomain.com/) + + + +Не забудьте добавить в конце слеш `/`{.docutils .literal}. + + + +Вы должны сделать это вручную, потому что Канборд не может угадать URL из скрипта командной строки и некоторые конфигурации веб серверов очень специфичны. + + +Решение проблем[¶](#troubleshooting "Ссылка на этот заголовок") +--------------------------------------------------------------- + +Если email сообщения не отправляются и вы уверены, что все настроили правильно: + +- Проверьте папку Спам +- Включите режим отладки и посмотрите отладочный файл `data/debug.log`{.docutils .literal}, вы можете увидеть конкретную ошибку +- Убедитесь, что ваш сервер или ваш хостинг провайдер позволяет вам отсылать email сообщения +- Если вы используете SeLinux, разрешите PHP отсылать email сообщения. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/env.markdown b/doc/ru_RU/env.markdown new file mode 100644 index 00000000..3764e98e --- /dev/null +++ b/doc/ru_RU/env.markdown @@ -0,0 +1,21 @@ +Переменные окружения +==================== + +Переменные окружения могут пригодится когда Канборд развертывается как контейнер (Docker). + + +| Переменная | Описание | +|---------|------------------------------------------------------------------| +| DATABASE\_URL | `[database type]://[username]:[password]@[host]:[port]/[database name]`, например: `postgres://foo:foo@myserver:5432/kanboard` | +| DEBUG | Включение/выключение режима отладки: “true” или “false” | +| LOG\_DRIVER | Logging driver: stdout, stderr, file or syslog | + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ext-search.markdown b/doc/ru_RU/ext-search.markdown new file mode 100644 index 00000000..1d6e7fe1 --- /dev/null +++ b/doc/ru_RU/ext-search.markdown @@ -0,0 +1,235 @@ +Синтаксис расширенного поиска +============================= + + +В Канборде используется простой язык запросов для расширенного поиска. Вы можете искать задачи, комментарии, подзадачи, ссылки, но только активные. + + +Пример запроса[¶](#example-of-query "Ссылка на этот заголовок") +--------------------------------------------------------------- + + + +В этом примере показываются как отобразить задачи назначенные мне с датой окончания завтра и название содержит “my title”: + + + + assigne:me due:tomorrow my title + + + +Глобальный поиск[¶](#global-search "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +### Поиск по id задачи или названию задачи[¶](#search-by-task-id-or-title "Ссылка на этот заголовок") + +- Поиск задачи по id: `#123` +- Поиск по id задачи и названию задачи: `123` +- Поиск по названию задачи: `любые слова и цифры`, но не должны содержать атрибуты поиска + + +### Поиск по статусу[¶](#search-by-status "Ссылка на этот заголовок") + +Атрибут: **status** + +- Запрос на поиск открытых задач: `status:open` +- Запрос на поиск закрытых задач: `status:closed` + + + +### Поиск по испольнителю[¶](#search-by-assignee "Ссылка на этот заголовок") + + +Атрибут: **assignee** + +- Поиск по полному имени испольнителя: `assignee:"Петр Иванов"` +- Поиск исполнителя по имени пользователя: `assignee:pivanov` +- Отбор нескольких испольнителей: `assignee:tsemenov assignee:"Петр Иванов"` +- Поиск задач без исполнителя: `assignee:nobody` +- Поиск задач назначенных мне: `assignee:me` + + +### Поиск по создателю задач[¶](#search-by-task-creator "Ссылка на этот заголовок") + + +Атрибут: **creator** + +- Отбор задач созданных мной: `creator:me` +- Отбор задач которые создал Петр Иванов: `creator:"Петр Иванов"` +- Отбор задач созданных пользователем с id \#1: `creator:1` + + +### Поиск по исполнителю подзадач[¶](#search-by-subtask-assignee "Ссылка на этот заголовок") + +Атрибут: **subtask:assignee** + +- Например: `subtask:assignee:"Петр Иванов"` + + +### Поиск по цвету[¶](#search-by-color "Ссылка на этот заголовок") + +Атрибут: **color** + +- Отбор по цвету с id blue: `color:blue` +- Отбор по названию цвета: `color:"Deep Orange"` + + +### Отбор по “Сделать до”[¶](#search-by-the-due-date "Ссылка на этот заголовок") + + +Атрибут: **due** + +- Поиск задач со сроком испольнения до сегодня: `due:today` +- Поиск задач со сроком исполнения завтра: `due:tomorrow` +- Поиск задач со сроком исполнения вчера: `due:yesterday` +- Поиск задач с конкретной датой исполнения: `due:2016-06-29` + +Дата должна быть в формате ISO 8601: **YYYY-MM-DD**. + +Все строковые форматы поддерживаемые функцией `strtotime()` допустимы. Например, `next Thursday`, `-2 days`{.docutils .literal}, `+2 months`, `tomorrow` и т.д. + + +Операторы сравнения с датой: + +- Старше чем: **due:\>2015-06-29** +- Моложе чем: **due:\<2015-06-29** +- Старше чем или равно: **due:\>=2015-06-29** +- Моложе чем или равно: **due:\<=2015-06-29** + + +### Поиск по дате изменения[¶](#search-by-modification-date "Ссылка на этот заголовок") + +Атрибут: **modified** или **updated** + +Формат даты такой же как и у “Сделать до” + +Отфильтровать недавно измененные задачи: `modified:recently`. + +Этот запрос использует тоже значение что и в настройках Доски - “Время подсвечивания задачи”. + + +### Поиск по дате создания[¶](#search-by-creation-date "Ссылка на этот заголовок") + +Атрибут: **created** + +Работает также как и поиск по дате изменения. + + +### Поиск по описанию[¶](#search-by-description "Ссылка на этот заголовок") + +Атрибут: **description** or **desc** + +Например: `description:"здесь пишем тескт для поиска"` + + +### Поиск по внешним ссылкам[¶](#search-by-external-reference "Ссылка на этот заголовок") + +Например: нужно найти задачу, которая содержит ссылку на id или название другой задачи. + +- `ref:1234` или `reference:TICKET-1234` + + +### Поиск по категории[¶](#search-by-category "Ссылка на этот заголовок") + +Атрибут: **category** + +- Найти задачи с указанной категорией: `category:"Важные запросы"` +- Найти задачи, которые содержать указанные категории: `category:"Ошибки" category:"Изменения"` +- Найти задачи без категорий: `category:none` + + +### Поиск проектов[¶](#search-by-project "Ссылка на этот заголовок") + +Атрибут: **project** + +- Поиск задач по имени проекта: `project:"Какой-то проект"` +- Поиск задач по id проекта: `project:23` +- Поиск задач в нескольких проектах: `project:"Проект A" project:"Проект B"` + + +### Поиск в колонках[¶](#search-by-columns "Ссылка на этот заголовок") + +Атрибут: **column** + +- Поиск задач в указанной колонке: `column:"В работе"` +- Поиск задач в нескольких колонках: `column:"Невыполненные заказы" column:ready` + + +### Поиск в Дорожках[¶](#search-by-swim-lane "Ссылка на этот заголовок") + +Атрибут: **swimlane** + +- Поиск задач в указанной Дорожке: `swimlane:"Версия 42"` +- Поиск задач в базовой Дорожке: `swimlane:default` +- Поиск задач в нескольких Дорожках: `swimlane:"Версия 1.2" swimlane:"Версия 1.3"` + + +### Поиск ссылки на задачу[¶](#search-by-task-link "Ссылка на этот заголовок") + +Атрибут: **link** + +- Поиск задач содержащих ссылку: `link:"это веха задачи "` +- Поиск задач по нескольким ссылкам: `link:"веха задачи " link:"относится к"` + + +### Поиск по комментарию[¶](#search-by-comment "Ссылка на этот заголовок") + +Атрибут: **comment** + +- Найти комментарии, которые содержат указанное название: `comment:"Какое-то название"` + + +Поиск активности задач[¶](#activity-stream-search "Ссылка на этот заголовок") +----------------------------------------------------------------------------- + + + +### Поиск событий по названию задачи[¶](#search-events-by-task-title "Ссылка на этот заголовок") + + + +Атрибут: **title** или без ничего (по умолчанию) + +- Например: `title:"My task"` +- Поиск задачи по id: `#123` + + +### Поиск событий по статусу задачи[¶](#search-events-by-task-status "Ссылка на этот заголовок") + +Атрибут: **status** + + + +### Поиск событий по создателю[¶](#search-by-event-creator "Ссылка на этот заголовок") + +Атрибут: **creator** + + + +### Поиск событий по дате создания[¶](#search-by-event-creation-date "Ссылка на этот заголовок") + +Атрибут: **created** + + + +### Поиск событий по проекту[¶](#search-events-by-project "Ссылка на этот заголовок") + +Атрибут: **project** + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/faq.markdown b/doc/ru_RU/faq.markdown new file mode 100644 index 00000000..0730f2c8 --- /dev/null +++ b/doc/ru_RU/faq.markdown @@ -0,0 +1,162 @@ +Часто задаваемые вопросы +======================== + + +Вы можете порекомендовать веб хостинг провайдера для Канборд?[¶](#can-you-recommend-a-web-hosting-provider-for-kanboard "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------------- + +Работу Канборд поддерживают несколько крупных провайдеров VPS, такие как [Digital Ocean](https://www.digitalocean.com/?refcode=4b541f47aae4), [Linode](https://www.linode.com/?r=4e381ac8a61116f40c60dc7438acc719610d8b11) или [Gandi](https://www.gandi.net/). + +Для получения большей производительности, выбирайте провайдера с быстрыми дисками чтения/записи, потому что Канборд использует по умолчанию Sqlite. Избегайте провайдеров которые используют подключения NFS. + + +У меня выводится пустая страница после установки или обновления Канборд[¶](#i-get-a-blank-page-after-installing-or-upgrading-kanboard "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +- Проверьте, установили ли вы всё на сервер, что было указано в требованиях +- Посмотрите ошибки в PHP и Apache логах +- Проверьте права доступа к файлам +- Если вы используете кеширование OPcode, перезапустите ваш веб сервер или php-fpm + + +У меня выводится ошибка “There is no suitable CSPRNG installed on your system”[¶](#i-have-the-error-there-is-no-suitable-csprng-installed-on-your-system "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +Если вы используете PHP \< 7.0, то вам нужно включить расширение openssl или доступ из приложения к `/dev/urandom`, если имеются ограничения от `open_basedir`. + + +Страница не найдена и URL выглядит криво (&)[¶](#page-not-found-and-the-url-seems-wrong-amp "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------- + +- UTL выглядит как `/?controller=auth&action=login&redirect_query=` вместо `?controller=auth&action=login&redirect_query=` +- Канборд выдает ошибку “Страница не найдена” + + +Эта ошибка исходит из настроек конфигурации вашего PHP, значение `arg_separator.output` отсутствует в базовой настройке. Есть разные пути решения этой проблемы: + +Измените значение прямо в вашем `php.ini`: + + + arg_separator.output = "&" + + +Переделайте значение с помощью `.htaccess`: + + + php_value arg_separator.output "&" + + +Иначе Канборд будет брать значение напрямую из PHP. + + + +Ошибка аутентификации в API и Apache + PHP-FPM[¶](#authentication-failure-with-the-api-and-apache-php-fpm "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------- + +По умолчанию, php-cgi под Apache не передает HTTP Basic user/pass в PHP. Чтобы это окружение заработало, добавьте эти строки в ваш файл `.htaccess`: + + + + RewriteCond %{HTTP:Authorization} ^(.+)$ + + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + + +Проблемы с eAccelerator[¶](#known-issues-with-eaccelerator "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + +Канборд не очень хорошо работает с [eAccelerator](http://eaccelerator.net). Проблема в том, что выдается чистая страница или падает Apache: + + + [Wed Mar 05 21:36:56 2014] [notice] child pid 22630 exit signal Segmentation fault (11) + + +Лучшее решение, чтобы избежать этой проблемы, выключить eAccelerator или прописать в конфиге какие файлы вы хотите кешировать (параметр `eaccelerator.filter`). + + + +Проект [eAccelerator выглядит мертвым и не обновляется с 2012](https://github.com/eaccelerator/eaccelerator/commits/master). Мы рекомендуем перейти на последнюю версию PHP, потому что в него включен [OPcache](http://php.net/manual/en/intro.opcache.php). + + +Почему минимальная рекомендуемая версия PHP 5.3.3?[¶](#why-the-minimum-requirement-is-php-5-3-3 "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------- + +Канборд использует функцию `password_hash()` для шифрования пароля, а эта функция доступна только для PHP \>= 5.5. + +Однако, имеется back-port для [более ранних версий PHP](https://github.com/ircmaxell/password_compat#requirements). Эта библиотека требует минимум PHP 5.3.7 для корректной работы. + +По всей видимости, патчи безопасности back-port имеются в Centos и Debian, поэтому PHP 5.3.3 подходит для работы Канборд. + +Канборд v1.0.10 и v1.0.11 требует минимум PHP 5.3.7, но эти изменения возвращены на PHP 5.3.3 в Канборде \>= v1.0.12 + + + +Как проверить работу Канборда со встроенным веб-сервером PHP?[¶](#how-to-test-kanboard-with-the-php-built-in-web-server "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------------- + +Если вы не хотите устанавливать веб сервер типа Apache, то вы можете протестировать работу Канборда на [встроенном в PHP веб сервере](http://www.php.net/manual/en/features.commandline.webserver.php): + + + unzip kanboard-VERSION.zip + + cd kanboard + + php -S localhost:8000 + + open http://localhost:8000/ + + + +Как установить Канборд на Yunohost?[¶](#how-to-install-kanboard-on-yunohost "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + +[YunoHost](https://yunohost.org/) это серверная операционная система, цель которой предоставить хостинг для всех. + +Отсюда можно [загрузить инсталяционный пакет Kanboard для Yunohost](https://github.com/mbugeia/kanboard_ynh). + + +Где я могу найти список связанных с Канборд проектов?[¶](#where-can-i-find-a-list-of-related-projects "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------- + +- [Kanboard API python client by @freekoder]([https://github.com/freekoder/kanboard-py](https://github.com/freekoder/kanboard-py)) + +- [Kanboard Presenter by David Eberlein](https://github.com/davideberlein/kanboard-presenter) + +- [CSV2Kanboard by @ashbike]([https://github.com/ashbike/csv2kanboard](https://github.com/ashbike/csv2kanboard)) + +- [Kanboard for Yunohost by @mbugeia]([https://github.com/mbugeia/kanboard\_ynh](https://github.com/mbugeia/kanboard_ynh)) + +- [Trello import script by @matueranet]([https://github.com/matueranet/kanboard-import-trello](https://github.com/matueranet/kanboard-import-trello)) + +- [Chrome extension by Timo](https://chrome.google.com/webstore/detail/kanboard-quickmenu/akjbeplnnihghabpgcfmfhfmifjljneh?utm_source=chrome-ntp-icon), [Source code](https://github.com/BlueTeck/kanboard_chrome_extension) + +- [Python client script by @dzudek]([https://gist.github.com/fguillot/84c70d4928eb1e0cb374](https://gist.github.com/fguillot/84c70d4928eb1e0cb374)) + +- [Shell script for SQLite to MySQL/MariaDB migration by @oliviermaridat]([https://github.com/oliviermaridat/kanboard-sqlite2mysql](https://github.com/oliviermaridat/kanboard-sqlite2mysql)) + +- [Git hooks for integration with Kanboard by Gene Pavlovsky](https://github.com/gene-pavlovsky/kanboard-git-hooks) + + + +Имеются ли руководства по Канборду на других языках?[¶](#are-there-some-tutorials-about-kanboard-in-other-languages "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------- + +- [Серия статей про Kanboard на немецком языке](http://demaya.de/wp/2014/07/kanboard-eine-jira-alternative-im-detail-installation/) . +- [Русская документация по Канборд](http://kanboard.ru/doc/). + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/freebsd-installation.markdown b/doc/ru_RU/freebsd-installation.markdown new file mode 100644 index 00000000..b014e354 --- /dev/null +++ b/doc/ru_RU/freebsd-installation.markdown @@ -0,0 +1,187 @@ +Инсталяция на FreeBSD 10 +======================== + + +Инсталяция из пакетов[¶](#install-from-packages "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + $ pkg update + + $ pkg upgrade + + $ pkg install apache24 mod_php56 kanboard + + + +Включите Apache в `/etc/rc.conf`{.docutils .literal}: + + + + $ echo apache24_enable="YES" >> /etc/rc.conf + + + +Установите PHP для Apache: + + + + $ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf + + $ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf + + + +Затем, запустите Apache: + + + + $ service apache24 start + + + +Создайте символическую ссылку на каталог Kanboard в корне Apache: + + + + cd /usr/local/www/apache24/data + + ln -s /usr/local/www/kanboard + + + +Готово. Можете перейти в /вашвебсервер/kanboard и начинать работать! + + + +*Примечание*: Если вы хотите добавить дополнительные возможности, типа интеграции LDAP, то нужно установить соответствующий PHP модуль. Также, вам необходимо настроить соответсвующие права на каталог data. + + + +Установка из портов[¶](#installing-from-ports "Ссылка на этот заголовок") +------------------------------------------------------------------------- + + +Нужно установить 3 основных элемента: + + + +- Apache + +- mod\_php for Apache + +- Kanboard + + + +Загрузите и распакуйте порты: + + + + $ portsnap fetch + + $ portsnap extract + + + +или обновите имеющиеся: + + + + $ portsnap fetch + + $ portsnap update + + + +Дополнительную информацию о дереве портов вы можете посмотреть на [FreeBSD Handbook](https://www.freebsd.org/doc/handbook/ports-using.html). + + + +Установка Apache: + + + + $ cd /usr/ports/www/apache24 + + $ make install clean + + + +Включите Apache в `/etc/rc.conf`{.docutils .literal}: + + + + $ echo apache24_enable="YES" >> /etc/rc.conf + + + +Установите mod\_php для Apache: + + + + $ cd /usr/ports/www/mod_php5 + + $ make install clean + + + +Установите Kanboard из портов: + + + + $ cd /usr/ports/www/kanboard + + $ make install clean + + + +Установите PHP для Apache: + + + + $ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf + + $ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf + + + +Затем, запустите Apache: + + + + $ service apache24 start + + + +Готово. Можете перейти в /вашвебсервер/kanboard и начинать работать! + + + +*Примечание*: Если вы хотите использовать дополнительные возможности, типа интеграции LDAP, то нужно установить PHP модуль из `lang/php5-extensions`{.docutils .literal}. + + + +Установка из архива[¶](#manual-installation "Ссылка на этот заголовок") +----------------------------------------------------------------------- + +Начина с версии 1.0.16 Kanboard имеется в портах FreeBSD, поэтому нет необходимости устанавливать вручную. + + + +Обратите внимание[¶](#please-note "Ссылка на этот заголовок") +------------------------------------------------------------- + +- Порт расположен на хостинге [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Делайте комментарии, ответвления и предлагайте обновления! +- Некоторые возможности Канборд требуют [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/gantt-chart-projects.markdown b/doc/ru_RU/gantt-chart-projects.markdown new file mode 100644 index 00000000..d440a85d --- /dev/null +++ b/doc/ru_RU/gantt-chart-projects.markdown @@ -0,0 +1,60 @@ +Диаграмма Ганта для всех проектов +================================= + + + +Цель диаграммы Ганта для проектов - показать прогресс проектов основанный на дате начала и дате завершения. + + + +- Диаграмма Ганта для проектов доступна из раздела **Управление проектами** + + + +- Только менеджеры проекта и администраторы имеют доступ в этот раздел + + + +- Менеджеры проекта могут видеть только те проекты, в которых они являются участниками + + + +- Приватные проекты не показывают этот график + + + +![Gantt Chart for all projects](https://kanboard.net/screenshots/documentation/gantt-chart-all-projects.png) + +Рисунок. Диаграмма Ганта для всех проектов + + + +- **Дата начала** и **дата завершения** проекта используются для рисования графика + + + +- Горизонтальные полосы (столбики) могут быть расширены (сжаты) и перемещены горизонтально с помощью мыши + + + +- Перемещение по вертикали невозможно + + + +- Полосы (столбики) проекта отображаются черным, когда проект не имеет дату начала и завершения + + + +- Информационная подсказка показывает список менеджеров и участников проекта + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/gantt-chart-tasks.markdown b/doc/ru_RU/gantt-chart-tasks.markdown new file mode 100644 index 00000000..1b8c4a2c --- /dev/null +++ b/doc/ru_RU/gantt-chart-tasks.markdown @@ -0,0 +1,66 @@ +Диаграмма Ганта для задач +========================= + + + +Цель диаграммы Ганта - показать время отведенное на задачу в заданном проекте. + + + +- Диаграмма Ганта доступна в рабочем окружении проекта + + + +- Только менеджеры проектов могут иметь доступ в этот раздел + + + +![Gantt Chart](https://kanboard.net/screenshots/documentation/gantt-chart-project.png) + +Рисунок. Диаграмма Ганта. + + + +- Дата начала и дата завершения задач используется для рисования диаграммы + + + +- Задача может быть расширена и перемещена горизонтально с помощью мыши + + + +- Перемещение по вертикали невозможно + + + +- Полоса (горизонтальный столбик) на диаграмме имеет такой же цвет как и задача + + + +- Каждая полоса отображает статус прогресса в процентах. Проценты подсчитываются с учетом позиции задачи в колонке на Доске. + + + +- Для соответсвия модели Kanban, задачи могут быть отсортированы в соответствии с позициями на доске или по дате начала + + + +- Новые задачи созданные через диаграмму Ганта будут показаны на Доске в первой колонке на первой позиции + + + +- Задачи отображаются черным цветом, если не указана дата начала или дата исполнения + + + +![Task not defined](https://kanboard.net/screenshots/documentation/gantt-chart-not-defined.png) + +Рисунок. Задача без указанных дат начала или завершения + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/genindex.markdown b/doc/ru_RU/genindex.markdown new file mode 100644 index 00000000..ceb48d17 --- /dev/null +++ b/doc/ru_RU/genindex.markdown @@ -0,0 +1,15 @@ +Алфавитный указатель +==================== + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/groups.markdown b/doc/ru_RU/groups.markdown new file mode 100644 index 00000000..5ab043d4 --- /dev/null +++ b/doc/ru_RU/groups.markdown @@ -0,0 +1,35 @@ +Управление группами +=================== + + + +В Канборде каждый пользователь может быть членом одной или нескольких групп. Группа - это что-то вроде команды или организации. + + + +Только администраторы могут создавать новую группу и добавлять туда пользователей. + + + +Настройка групп доступна через **Управление пользователями** (выпадающее меню справа вверху) -\> **Просмотр всех пользователей**. Здесь вы можете создавать новые группы и добавлять пользователей в группы. + + + +![Group Management](screenshots/groups-management.png) + +Рисунок. Управление группами. + + + +Менеджеры проектов могут предоставлять доступ группам к проектам на [странице Разрешения проекта](project-permissions.markdown). + + + +Внешние id в основном используются для предоставления доступа внешним группам. Канборд поддерживает группы из LDAP посредством [автоматической синхронизации групп из LDAP сервера](ldap-group-sync.markdown). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/heroku.markdown b/doc/ru_RU/heroku.markdown new file mode 100644 index 00000000..6e2bd945 --- /dev/null +++ b/doc/ru_RU/heroku.markdown @@ -0,0 +1,72 @@ +Развертывание Канборд на Heroku +=============================== + +Вы можете бесплатно испытать работу Kanboard на [Heroku](https://www.heroku.com/). Вам нужно нажать кнопку **Deploy to Heroku** и следовать руководству приведенному ниже: + +[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard) + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Учетная запись на Heroku. Вы можете зарегистрироваться бесплатно. +- Установленная утилита командной строки Heroku + + + +Руководство по установке[¶](#manual-instructions "Ссылка на этот заголовок") +---------------------------------------------------------------------------- + + + # Get the last development version + + git clone https://github.com/fguillot/kanboard.git + + cd kanboard + + + + # Push the code to Heroku (You can also use SSH if git over HTTP doesn't work) + + heroku create + + git push heroku master + + + + # Start a new dyno with a Postgresql database + + heroku ps:scale web=1 + + heroku addons:add heroku-postgresql:hobby-dev + + + + # Open your browser + + heroku open + + + +Ограничения[¶](#limitations "Ссылка на этот заголовок") +------------------------------------------------------- + +- Хранилище на Heroku эфимерное. Это означает, что файлы, загружаемые через Канборд, будут отсутствовать в системе после перезагрузки. Вы можете установить плагин для хранения файлов в облаке, например [Amazon S3](https://github.com/kanboard/plugin-s3). +- Некоторые возможности Канборда требуют, чтобы вы выполняли [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ical.markdown b/doc/ru_RU/ical.markdown new file mode 100644 index 00000000..77b6340e --- /dev/null +++ b/doc/ru_RU/ical.markdown @@ -0,0 +1,111 @@ +Синхронизация вашего календаря +============================== + + +Канборд поддерживает iCal транслятор для проектов и пользователей. Эта возможность позволяет вам импортировать задачи из Канборд в любую программу календарь (например, Microsoft Outlook, Apple Calendar, Mozilla Thunderbird и Google Calendar). + +Подписки на календарь возможны только на **чтение**, т.е. вы не можете создавать задачи во внешнем календаре. Данные из Календаря экспортируются в стандарте iCal. + +Заметка: Только задачи в промежутке от -2 месяцев до +6 месяцев (прошедшие два месяца и предстоящие 6 месяцев) экспортируются в iCalendar транслятор. + + +Календарь проекта[¶](#project-calendars "Ссылка на этот заголовок") +------------------------------------------------------------------- + +- Каждый проект имеет свой календарь. +- Ссылка на подписку уникальна для каждого проекта. Ссылка становится активной, когда вы включаете общий доступ к вашему проекту: **Меню** -\> **Настройки** -\> **Общий доступ** +- Этот календарь показывает только задачи для выбранного проекта. + + +Календарь пользователя[¶](#user-calendars "Ссылка на этот заголовок") +--------------------------------------------------------------------- + +- Каждый пользователь имеет свой собственный календарь. +- Ссылка на подписку уникальная для каждого пользователя. Ссылка становится активной, когда вы включите общий доступ для пользователя: в правом верхнем выпадающем меню - **Мой профиль** -\> в левом меню - **Общий доступ**. +- Этот календарь показывает задачи назначенные пользователю во всех проектах. + + +Добавление Канборд календаря в календарь Apple[¶](#adding-your-kanboard-calendar-to-apple-calendar "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------ + +- Откройте календарь +- Выберите **Файл** -\> **Новая подписка на календарь** +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + +![Add iCal subscription](https://kanboard.net/screenshots/documentation/apple-calendar-add-subscription.png) + +Рисунок. Добавление подписки на календарь. + + +- Вы можете выбрать синхронизацию календаря с iCloud, чтобы иметь доступ к календарю с любых ваших устройств +- Не забудьте указать частоту синхронизации + + +![Edit iCal subscription](https://kanboard.net/screenshots/documentation/apple-calendar-edit-subscription.png) + +Рисунок. Редактирование подписки на календарь. + + +Добавление вашего календаря из Канборд в Microsoft Outlook[¶](#adding-your-kanboard-calendar-to-microsoft-outlook "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------- + +![Outlook Add Internet Calendar](https://kanboard.net/screenshots/documentation/outlook-add-subscription.png) + +Рисунок. Добавление в Outlook календаря из интернет + +- Откройте Outlook +- Выберите **Открыть календарь** -\> **Из интернета** +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + +![Outlook Edit Internet Calendar](https://kanboard.net/screenshots/documentation/outlook-edit-subscription.png) + +Рисунок. Настройка интернет календаря в Outlook. + + +Добавление вашего календаря из Канборд в Mozilla Thunderbird[¶](#adding-your-kanboard-calendar-to-mozilla-thunderbird "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------------------------------------- + + +- Установите в Thunderbird Дополнение **Lightning** +- Выберите **Файл** -\> **Новый календарь** +- В диалоговом окне, выберите **Из сети** + +![Thunderbird Step 1](https://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step1.png) + +Рисунок. Создание календаря в Thunderbird, шаг 1. + + + +- Выберите формат iCalendar +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + +![Thunderbird Step 2](https://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step2.png) + +Рисунок. Создание календаря в Thunderbird, шаг 2. + +- Выберите цвета и другие настройки и в завершении нажмите **Сохранить**. + + +Добавление вашего календаря Канборд в календарь Google[¶](#adding-your-kanboard-calendar-to-google-calendar "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------- + +- Нажмите иконку “треугольник” рядом с **Другие календари** (слева). +- Вставьте ссылку на календарь из Канборд в поле “Добавить календарь друга” +- Скопируйте в Канборд URL ссылку на iCal транслятор и вставьте + + +![Google Calendar](https://kanboard.net/screenshots/documentation/google-calendar-add-subscription.png) + +Рисунок. Календарь Google. + +Ваш календарь из Канборд будет доступен на планшетах и смартфонах, нужно только сделать синхронизацию. + + +[Справка по настройке календаря Google](https://support.google.com/calendar/?hl=ru#topic=3417969). + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/index.markdown b/doc/ru_RU/index.markdown new file mode 100644 index 00000000..c4a12d52 --- /dev/null +++ b/doc/ru_RU/index.markdown @@ -0,0 +1,248 @@ +Документация +============ + + +Как работать в Kanboard[¶](#using-kanboard "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + +### Введение[¶](#introduction "Ссылка на этот заголовок") + + +- [Что такое Kanban?](what-is-kanban.markdown) + +- [Kanban против Todo списков и Scrum](kanban-vs-todo-and-scrum.markdown) + +- [Где можно использовать Kanboard](usage-examples.markdown) + + +### Использование доски[¶](#using-the-board "Ссылка на этот заголовок") + + +- [Доска, Календарь, Список и Гант представления](project-views.markdown) + +- [Компактное или развернутое отображение задач](board-collapsed-expanded.markdown) + +- [Горизонтальная прокрутка и компактный вид](board-horizontal-scrolling-and-compact-view.markdown) + +- [Отображение и скрытие колонок](board-show-hide-columns.markdown) + + +### Работа с проектами[¶](#working-with-projects "Ссылка на этот заголовок") + +- [Типы проектов](project-types.markdown) + +- [Создание проектов](creating-projects.markdown) + +- [Редактирование проектов](editing-projects.markdown) + +- [Публичные доски и задачи](sharing-projects.markdown) + +- [Автоматизация процессов](automatic-actions.markdown) + +- [Права доступа к проекту](project-permissions.markdown) + +- [Дорожки](swimlanes.markdown) + +- [Календарь](calendar.markdown) + +- [Аналитика](analytics.markdown) + +- [Диаграмма Ганта для задач](gantt-chart-tasks.markdown) + +- [Диаграмма Ганта для проектов](gantt-chart-projects.markdown) + +- [Пользовательские фильтры](custom-filters.markdown) + + + +### Работа с задачами[¶](#working-with-tasks "Ссылка на этот заголовок") + +- [Создание задач](creating-tasks.markdown) + +- [Закрытие задач](closing-tasks.markdown) + +- [Дублирование и перенос задач](duplicate-move-tasks.markdown) + +- [Добавление снимка экрана](screenshots.markdown) + +- [Ссылки на задачу](task-links.markdown) + +- [Перемещения](transitions.markdown) + +- [Отслеживание времени](time-tracking.markdown) + +- [Повторяющиеся задачи](recurring-tasks.markdown) + +- [Создание задач через email](create-tasks-by-email.markdown) + +- [Подзадачи](subtasks.markdown) + +- [Аналитика для задач](analytics-tasks.markdown) + +- [Ссылка на пользователя](user-mentions.markdown) + + + +### Работа с пользователями и группами[¶](#working-with-users-and-groups "Ссылка на этот заголовок") + +- [Роли](roles.markdown) + +- [Типы пользователей](user-types.markdown) + +- [Управление группами](groups.markdown) + +- [Управление пользователями](user-management.markdown) + +- [Уведомления](notifications.markdown) + +- [Двухуровневая аутентификация](2fa.markdown) + + + +### Настройки[¶](#settings "Ссылка на этот заголовок") + +- [Горячие клавиши](keyboard-shortcuts.markdown) + +- [Настройки приложения](application-configuration.markdown) + +- [Настройки проекта](project-configuration.markdown) + +- [Настройка Доски](board-configuration.markdown) + +- [Настройки календаря](calendar-configuration.markdown) + +- [Настройка ссылок](link-labels.markdown) + +- [Курсы валют](currency-rate.markdown) + + +### Встроенные возможности[¶](#integrations "Ссылка на этот заголовок") + +- [iCalendar подписки](ical.markdown) + +- [RSS/Atom подписки](rss.markdown) + +- [Json-RPC API](api-json-rpc.markdown) + +- [Webhooks](webhooks.markdown) + +- [Плагины](plugins.markdown) + + +### Дополнительно[¶](#more "Ссылка на этот заголовок") + +- [Синтаксис расширенного поиска](ext-search.markdown) + +- [Интерфейс командной строки](cli.markdown) + +- [Руководство по синтаксису](syntax-guide.markdown) + +- [Защита от Brute force](bruteforce-protection.markdown) + +- [Часто задаваемые вопросы](faq.markdown) + + + +Технические детали[¶](#technical-details "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + +### Инсталяция[¶](#installation "Ссылка на этот заголовок") + +- [Требования](requirements.markdown) + +- [Инструкция по инсталяции](installation.markdown) + +- [Обновление Kanboard до новой версии](update.markdown) + +- [Инсталяция на Ubuntu](ubuntu-installation.markdown) + +- [Инсталяция на Debian](debian-installation.markdown) + +- [Инсталяция на Centos](centos-installation.markdown) + +- [Инсталяция на OpenSuse](suse-installation.markdown) + +- [Инсталяция на FreeBSD](freebsd-installation.markdown) + +- [Инсталяция на Windows Server и IIS](windows-iis-installation.markdown) + +- [Инсталяция на Windows Server и Apache](windows-apache-installation.markdown) + +- [Инсталяция на Heroku](heroku.markdown) + +- [Запуск Kanboard под Docker](docker.markdown) + +- [Запуск Kanboard под Vagrant](vagrant.markdown) + +- [Запуск Kanboard на Cloudron](cloudron.markdown) + +- [Запуск Kanboard на Nitrous](nitrous.markdown) + + +### Настройка[¶](#configuration "Ссылка на этот заголовок") + +- [Ежедневные фоновые задачи](cronjob.markdown) + +- [Конфигурационный файл](config.markdown) + +- [Переменные окружения](env.markdown) + +- [Настройка email](email-configuration.markdown) + +- [Переопределение URL](nice-urls.markdown) + +- [Директория плагинов](plugin-directory.markdown) + + + +### База данных[¶](#database "Ссылка на этот заголовок") + +- [База данных Sqlite](sqlite-database.markdown) + +- [Как использовать Mysql](mysql-configuration.markdown) + +- [Как использовать Postgresql](postgresql-configuration.markdown) + + +### Аутентификация[¶](#authentication "Ссылка на этот заголовок") + +- [LDAP аутентификация](ldap-authentication.markdown) + +- [Синхронизация групп LDAP](ldap-group-sync.markdown) + +- [Изображения из профиля LDAP](ldap-profile-picture.markdown) + +- [Параметры LDAP](ldap-parameters.markdown) + +- [Пример конфигурации LDAP](ldap-configuration-examples.markdown) + +- [Аутентификация Reverse proxy](reverse-proxy-authentication.markdown) + + +### Участие в проекте[¶](#contributors "Ссылка на этот заголовок") + +- [Руководство для участников проекта](contributing.markdown) + +- [Переводы на другие языки](translations.markdown) + +- [Стандарты при написании кода](coding-standards.markdown) + +- [Выполнение тестов](tests.markdown) + +- [Создание assets](assets.markdown) + + +[Документация](https://github.com/fguillot/kanboard/tree/master/doc) написана в формате [Markdown](https://ru.wikipedia.org/wiki/Markdown). Если вы желаете улучшить документацию - пошлите pull-request. + + + +* [Проект перевода документации Канборд на русский язык](https://github.com/hairetdin/kanboard-doc-ru). [Русская документация Канборд в формате html](http://kanboard.ru/doc/). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/installation.markdown b/doc/ru_RU/installation.markdown new file mode 100644 index 00000000..e59e43d2 --- /dev/null +++ b/doc/ru_RU/installation.markdown @@ -0,0 +1,117 @@ +Инсталяция +========== + + + +В первую очередь, ознакомтесь с [требованиями](requirements.markdown). + + + +Инсталяция из архива (стабильная версия)[¶](#from-the-archive-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + + +1. У вас должен быть установлен веб сервер с PHP + +2. Скачайте исходный код и скопируйте директорию `kanboard` в каталог веб сервера + +3. Проверьте, чтобы директория `data` была доступна на запись + +4. В вашем браузере перейдите по ссылке /вашвебсервер/kanboard + +5. Логин и пароль по умолчанию - **admin/admin** + +6. Все, теперь вы можете работать в Канборд + +7. Не забудьте сменить пароль! + + + +Место хранения данных: + + +- База данных Sqlite: `db.sqlite` + +- Файл отладки: `debug.log` (если включена отладка) + +- Загруженные файлы: `files/*` + +- Изображения: `files/thumbnails/*` + + + +Те, кто использует удаленную базу данных (Mysql/Postgresql) и удаленное файловое хранилище (Aws S3 или подобное), могут не назначать права доступа к локальным данным. + + +Инсталяция из репозитория (разрабатываемая версия)[¶](#from-the-repository-development-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------- + + + +Вы можете установить [composer](https://getcomposer.org/) для этого метода инсталяции. + + +1. `git clone https://github.com/fguillot/kanboard.git` + +2. `composer install --no-dev` + +3. Далее, перейдите к третьему шагу [Инсталяция из архива](installation.html#from-the-archive-stable-version) + + + +**Внимание**: Инсталируя **текущую разрабатываемую версию**, вы должны понимать, что это нестабильная версия и берете все риски по работе Канборд на себя. + + + +Инсталяция в другой каталог[¶](#installation-outside-of-the-document-root "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------- + + + +Если вы хотите инсталировать Канборд в другую директорию, вне корневого каталога веб сервера, вам нужно создать, как минимум, следующие символьные ссылки: + + . + + ├── assets -> ../kanboard/assets + ├── doc -> ../kanboard/doc + ├── favicon.ico -> ../kanboard/favicon.ico + ├── index.php -> ../kanboard/index.php + ├── jsonrpc.php -> ../kanboard/jsonrpc.php + └── robots.txt -> ../kanboard/robots.txt + + + +`.htaccess` необязательно, потому что его содержимое может быть включена прямо в конфигурацию Apache. + + +Вы можете указать текущее месторасположение директорий плагинов и файлов изменив [конфигурационный файл](config.markdown). + + + +Безопасность[¶](#security "Ссылка на этот заголовок") +----------------------------------------------------- + +- Не забудьте изменить логин и пароль пользователя admin, назначенный по умолчанию + +- Не предоставляйте всем права на директорию `data` через URL. + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + +- Некоторые возможности Канборда требуют, чтобы [ежедневно выполнялись фоновые задачи](cronjob.markdown). + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/kanban-vs-todo-and-scrum.markdown b/doc/ru_RU/kanban-vs-todo-and-scrum.markdown new file mode 100644 index 00000000..7c1b205b --- /dev/null +++ b/doc/ru_RU/kanban-vs-todo-and-scrum.markdown @@ -0,0 +1,75 @@ +Сравнение Kanban, Todo lists и Scrum +==================================== + + +Сравнение Kanban и Todo lists[¶](#kanban-vs-todo-lists "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + +### Todo lists (списки для исполнения):[¶](#todo-lists "Ссылка на этот заголовок") + +- Имеют одну фазу (только список пунктов) + +- Возможна многозадачность (не эффективна) + + + +### Kanban:[¶](#kanban "Ссылка на этот заголовок") + + + +- Имеет много фаз, каждая колонка представлена как шаг процесса + +- Концентрация внимания и исключение многозадачности, потому что вы можете установить этап процесса заданной колонкой + + + +Сравнение Kanban и Scrum[¶](#kanban-vs-scrum "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + +### [Scrum:](https://ru.wikipedia.org/wiki/Scrum)[¶](#scrum "Ссылка на этот заголовок") + + +- Спринты жестко фиксированные временем, обычно 2 или 4 недели + +- Не позволяет вносить изменения в течении итерации + +- Обязательна предварительная оценка + +- Используется скорость как единица измерения по умолчанию + +- Доска Scrum очищается между спринтами + +- Scrum имеет преопределенные роли, такие как, мастер, владелец продукта и команда + +- Множество встреч: планирование, беклог груминг (причесывание), ежедневные совещания, ретроспектива + + + +### Kanban:[¶](#id1 "Ссылка на этот заголовок") + +- Непрерывный поток + +- Гибкость - изменения могут быть сделаны в любое время + +- Предварительная оценка опциональна + +- Используется время выполнения (lead time) и время цикла (cycle time) для измерения производительности + +- Доска Kanboar постоянна + +- Kanban не навязывает строгих ограничений или встреч, процессы более гибкие + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/keyboard-shortcuts.markdown b/doc/ru_RU/keyboard-shortcuts.markdown new file mode 100644 index 00000000..a09c92bc --- /dev/null +++ b/doc/ru_RU/keyboard-shortcuts.markdown @@ -0,0 +1,99 @@ +Горячие клавиши +=============== + + +Горячие клавиши доступны в зависимости от страницы на которой вы находитесь. + + + +В проекте (Доска, Календарь, Список, Гант)[¶](#project-views-board-calendar-list-gantt "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------ + +- Переключиться на Обзор проектов = **v o** (переключите клавиатуру в английскую раскладку и нажмите клавиши **v** и **o** ) + + + +- Переключиться на Доску = **v b** + + + +- Переключиться на Календарь = **v c** + + + +- Переключиться на список = **v l** + + + +- Переключиться на диаграмму Ганта = **v g** + + + +На Доске[¶](#board-view "Ссылка на этот заголовок") +--------------------------------------------------- + +- Новая задача = **n** + + + +- Свернуть/развернуть задачи = **s** + + + +- Компактный/широкий вид = **c** + + + +В Задаче[¶](#task-view "Ссылка на этот заголовок") +-------------------------------------------------- + +- Редактировать задачу = **e** + + + +- Новая подзадача = **s** + + + +- Новый комментарий = **c** + + + +- Новая внутренняя ссылка = **l** + + + +В приложении (главное окно Канборд)[¶](#application "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + +- Показать список горячих клавиш = **?** + + + +- Открыть переключатель проектов = **b** + + + +- Переход в окно поиска = **f** + + + +- Очистить окно поиска = **r** + + + +- Закрыть окно диалога = **ESC** + + + +- Расширенный поиск = **CTRL+ENTER** or **⌘+ENTER** + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-authentication.markdown b/doc/ru_RU/ldap-authentication.markdown new file mode 100644 index 00000000..a94d8f89 --- /dev/null +++ b/doc/ru_RU/ldap-authentication.markdown @@ -0,0 +1,327 @@ +Аутентификация LDAP +=================== + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Включенное в PHP раширение LDAP + + + +- Сервер LDAP: + + + + - OpenLDAP + + - Microsoft Active Directory + + - Novell eDirectory + + + +Рабочий процесс[¶](#workflow "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +Когда активирована аутентификация LDAP, процесс входа выглядит следующим образом: + + + +1. Выполняется попытка аутентификации пользователя в базе данных Канборда + +2. Если пользователь не найден в базе Канборда, выполняется аутентификация LDAP + +3. Если аутентификация LDAP выполнена успешно, по умолчанию, локальный пользователь (в Канборде) создается автоматически без пароля и помечается как пользователь LDAP. + + + +Полное имя и email адрес автоматически подгружаются из сервера LDAP. + + + +Типы аутентификации[¶](#authentication-types "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + +| Тип | Описание | +|--------------|-------------------------------------------------------------| +| Proxy User | Использовать специального пользователя для просмотра директории LDAP | +| User | Использовать учетные данные конечного пользователя для просмотра директории LDAP | +| Anonymous | Не надо выполнять аутентификацию для доступа к каталогу LDAP | + + +**Рекомендуемый метод аутентификации - “Proxy”**. + + + +### Анонимный (Anonymous) метод[¶](#anonymous-mode "Ссылка на этот заголовок") + + + + define('LDAP_BIND_TYPE', 'anonymous'); + + define('LDAP_USERNAME', null); + + define('LDAP_PASSWORD', null); + + + +Этот метод используется по умолчанию, но некоторые сервера LDAP не поддерживают доступ анонимам, из соображений безопасности. + + + +### Proxy метод[¶](#proxy-mode "Ссылка на этот заголовок") + + + +Специальный пользователь используется для доступа к директории LDAP: + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'my proxy user'); + + define('LDAP_PASSWORD', 'my proxy password'); + + + +### Пользовательский метод (user)[¶](#user-mode "Ссылка на этот заголовок") + + + +Этот метод используется для доступа под учетной записью конечного пользователя. + + + +Например, Microsoft Active Directory не разрешает подключение под анонимным пользователем и если вы не хотите использовать пользователя proxy, то используйте этот метод. + + + + define('LDAP_BIND_TYPE', 'user'); + + define('LDAP_USERNAME', '%s@kanboard.local'); + + define('LDAP_PASSWORD', null); + + + +В этом методе, константа `LDAP_USERNAME` использутся как шаблон для пользователя ldap, например: + + + +- `%s@kanboard.local` будет заменен `my_user@kanboard.local` + + + +- `KANBOARD\\%s` будет заменен на `KANBOARD\my_user` + + + +Фильтр пользователей LDAP[¶](#user-ldap-filter "Ссылка на этот заголовок") +-------------------------------------------------------------------------- + + +Параметр конфигурации `LDAP_USER_FILTER` используется для поиска пользователей по директории LDAP. + + + +Например: + + + +- `(&(objectClass=user)(sAMAccountName=%s))` будет заменено на `(&(objectClass=user)(sAMAccountName=указанный_пользователь))` + + +- `uid=%s` is replaced by `uid=указанный_пользователь` + + + +Другие примеры [фильтров для Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx) + + + +Пример фильра доступа в Канборд: + + + +`(&(objectClass=user)(sAMAccountName=%s)(memberOf=CN=Kanboard Users,CN=Users,DC=kanboard,DC=local))` + + + +Этот пример разрешает подключатся к Канборду только пользователям участникам группы “Kanboard Users” + + + +Пример для Microsoft Active Directory[¶](#example-for-microsoft-active-directory "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------ + + + +Предположим, что мы имеем домен `KANBOARD` (kanboard.local) и контролер домена `myserver.kanboard.local`. + + + +Первый пример для метода прокси (proxy): + + + + + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'administrator@kanboard.local'); + + define('LDAP_PASSWORD', 'secret'); + + + + define('LDAP_USER_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', '(&(objectClass=user)(sAMAccountName=%s))'); + + + + define('LDAP_USER_ATTRIBUTE_USERNAME', 'samaccountname'); + + define('LDAP_USER_ATTRIBUTE_FULLNAME', 'displayname'); + + define('LDAP_USER_ATTRIBUTE_PHOTO', 'jpegPhoto'); + + define('LDAP_USER_ATTRIBUTE_LANGUAGE', 'preferredLanguage'); + + + + define('LDAP_GROUP_ADMIN_DN', 'CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_MANAGER_DN', 'CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с memberOf overlay[¶](#openldap-with-memberof-overlay "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------ + +Пример LDIF пользователя: + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: top + + objectClass: person + + objectClass: organizationalPerson + + objectClass: inetOrgPerson + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + mail: manager@kanboard.local + + userPassword: password + + memberOf: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: top + + objectClass: groupOfNames + + cn: Kanboard Managers + + member: uid=manager,ou=Users,dc=kanboard,dc=local + + + +Конфигурация Канборд: + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + + + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=groupOfNames)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с Posix groups (memberUid)[¶](#openldap-with-posix-groups-memberuid "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + +Пример LDIF пользователя: + + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: inetOrgPerson + + objectClass: posixAccount + + objectClass: shadowAccount + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + uidNumber: 10001 + + gidNumber: 8000 + + userPassword: password + + homeDirectory: /home/manager + + mail: manager@kanboard.local + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: posixGroup + + cn: Kanboard Managers + + gidNumber: 5001 + + memberUid: manager + + + +Конфигурация Канборд: + + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + + + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + // This filter is used to find the groups of our user + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=posixGroup)(memberUid=%s))'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + +OpenLDAP с groupOfNames[¶](#openldap-with-groupofnames "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + +Пример LDIF пользователя: + + + + dn: uid=manager,ou=Users,dc=kanboard,dc=local + + objectClass: top + + objectClass: person + + objectClass: organizationalPerson + + objectClass: inetOrgPerson + + uid: manager + + sn: Lastname + + givenName: Firstname + + cn: Kanboard Manager + + displayName: Kanboard Manager + + mail: manager@kanboard.local + + userPassword: password + + + +Пример LDIF группы: + + + + dn: cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local + + objectClass: top + + objectClass: groupOfNames + + cn: Kanboard Managers + + member: uid=manager,ou=Users,dc=kanboard,dc=local + + + +Конфигурация Канборд: + + + +- Аутентификация пользователя + + + +- Роли в Канборд сопоставляются с группами LDAP + + + +- Поставщики групп LDAP включены + + + + + + + + define('LDAP_AUTH', true); + + + + define('LDAP_SERVER', 'my-ldap-server'); + + define('LDAP_PORT', 389); + + + + define('LDAP_BIND_TYPE', 'proxy'); + + define('LDAP_USERNAME', 'cn=admin,DC=kanboard,DC=local'); + + define('LDAP_PASSWORD', 'password'); + + + + define('LDAP_USER_BASE_DN', 'OU=Users,DC=kanboard,DC=local'); + + define('LDAP_USER_FILTER', 'uid=%s'); + + + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + + // This filter is used to find the groups of our user + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=groupOfNames)(member=uid=%s,ou=Users,dc=kanboard,dc=local))'); + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=groupOfNames)(cn=%s*))'); + + define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-group-sync.markdown b/doc/ru_RU/ldap-group-sync.markdown new file mode 100644 index 00000000..87d9d1cc --- /dev/null +++ b/doc/ru_RU/ldap-group-sync.markdown @@ -0,0 +1,153 @@ +Синхронизация групп LDAP +======================== + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Правильно настроенную аутентификацию LDAP + + + +- Используется сервер LDAP, который поддерживает `memberOf` или `memberUid` (PosixGroups) + + + +Автоматическое определение ролей пользователей на основании LDAP групп[¶](#define-automatically-user-roles-based-on-ldap-groups "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +Используйте следующие константы в вашем конфигурационном файле: + + + +- `LDAP_GROUP_ADMIN_DN`: Уникальные имена (Distinguished Names) для администраторов приложения + + + +- `LDAP_GROUP_MANAGER_DN`: Уникальные имена (Distinguished Names) для менеджеров приложения + + + +### Пример для Active Directory:[¶](#example-for-active-directory "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_ADMIN_DN', 'CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local'); + + define('LDAP_GROUP_MANAGER_DN', 'CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local'); + + + +- Участники группы “Kanboard Admins” будут иметь роль “Администратор” + + + +- Участники группы “Kanboard Managers” будут иметь роль “Менеджер” + + + +- Все, кто не попадает под предыдущие определения, будут иметь роль “Пользователь” + + + +### Пример OpenLDAP с Posix Groups:[¶](#example-for-openldap-with-posix-groups "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_USER_FILTER', '(&(objectClass=posixGroup)(memberUid=%s))'); + + define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'); + + + +Вы **должны определить параметр** `LDAP_GROUP_USER_FILTER`, если ваше сервер LDAP использует `memberUid` вместо `memberOf`. Все параметры в этом примере обязательные. + + + +Автоматическая загрузка групп LDAP для Канборд проекта[¶](#automatically-load-ldap-groups-for-project-permissions "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------------------- + + + +Эта возможность позволяет вам синхронизировать автоматически группы LDAP с группами Канборд. Каждая группа может иметь разные роли в проектах. + + + +В проекте на странице *Разрешения*, можно ввести имя группы (имеется автодополнение) и Канборд будет искать группу во всех подключенных поставщиках. + + + +Если группа не найдена в локальной базе данных, то она будет автоматически синхронизированна. + + + +- `LDAP_GROUP_PROVIDER`: Включение поставщика группы LDAP + + + +- `LDAP_GROUP_BASE_DN`: Уникальное имя (Distinguished Names) для поиска группы в LDAP директории + + + +- `LDAP_GROUP_FILTER`: фильтр LDAP используемый для выполнения запроса + + + +- `LDAP_GROUP_ATTRIBUTE_NAME`: атрибут LDAP используемый для получения имени группы + + + +### Пример для Active Directory:[¶](#id1 "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'CN=Groups,DC=kanboard,DC=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))'); + + + +С помощью фильтра, в примере выше, Канборд будет искать группы соответсвующие запросу. Если пользователь введет текст “Мои группы” в автозаполняемое поле, Канборд вернет все группы которые соответсвуют шаблону: `(&(objectClass=group)(sAMAccountName=Мои группы*))`. + + + +- Примечание 1: Спец символ `*` очень важен, в противном случает **будет выбрано только точное совпадение** + + + +- Примечание 2: Эта функция возможна только с аутентификацией LDAP настроенной на метод “proxy” или “anonymous” + + + +[Больше примеров фильтров LDAP для Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx) + + + +### Пример OpenLDAP с Posix Groups:[¶](#id2 "Ссылка на этот заголовок") + + + + define('LDAP_GROUP_PROVIDER', true); + + define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local'); + + define('LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(cn=%s*))'); + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-parameters.markdown b/doc/ru_RU/ldap-parameters.markdown new file mode 100644 index 00000000..5d00913d --- /dev/null +++ b/doc/ru_RU/ldap-parameters.markdown @@ -0,0 +1,49 @@ +Параметры LDAP для конфигурации +=============================== + + + +Список доступных параметров LDAP: + + +| Параметр | Значение по умолчанию |Описание | +|---------------------------|------------------------------|-----------------------------| +| `LDAP_AUTH` | false | Включить аутентификацию LDAP | +| `LDAP_SERVER` | Нет значения | Имя сервера LDAP | +| `LDAP_PORT` | 389 | Порт сервера LDAP | +| `LDAP_SSL_VERIFY` | true | Проверка сертификата для URL `ldaps://` | +| `LDAP_START_TLS` | false | Включение LDAP start TLS | +| `LDAP_USERNAME_CASE_SENSITIVE` | false | Включение/выключение нижнего и верхнего регистра букв в Канборд для пользователей ldap для исключения дублирования пользователей (база данных чувствительна к регистру) | +| `LDAP_BIND_TYPE` | anonymous | Тип подключения: “anonymous”, “user” or “proxy” | +| `LDAP_USERNAME` | null | Имя пользователя LDAP для использования в методе proxy или шаблон имени пользователя для использования в методе user | +| `LDAP_PASSWORD` | null | Пароль LDAP при использовании метода proxy | +| `LDAP_USER_BASE_DN`| Нет значения | Уникальное имя (DN) LDAP для пользователей (Пример: “CN=Users,DC=kanboard,DC=local”) | +| `LDAP_USER_FILTER` | Нет значения | Шаблон LDAP, который используется для поиска пользователей (Пример: “(&(objectClass=user)(sAMAccountName=%s))”) | +| `LDAP_USER_ATTRIBUTE_USERNAME` | uid | Атрибут LDAP для имени пользователя (Например: “samaccountname”) | +| `LDAP_USER_ATTRIBUTE_FULLNAME` | cn | Атрибут LDAP полного имени пользователя (Например: “displayname”) | +| `LDAP_USER_ATTRIBUTE_EMAIL` | mail | Атрибут LDAP для email пользователя | +| `LDAP_USER_ATTRIBUTE_GROUPS` | memberof | Атрибут LDAP для поиска групп в профиле пользователя | +| `LDAP_USER_ATTRIBUTE_PHOTO` | Нет значения | Атрибут LDAP для поиска фотографии пользователя (jpegPhoto или thumbnailPhoto) | +| `LDAP_USER_ATTRIBUTE_LANGUAGE` | Нет значения | Атрибут LDAP для языка пользователя (preferredlanguage), применимый формат языка - “ru-RU” | +| `LDAP_USER_CREATION` | true | Включение автоматического создания пользователя из LDAP | +| `LDAP_GROUP_ADMIN_DN` | Нет значения | Уникальное имя (DN) LDAP для администраторов (Например: “CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local”) | +| `LDAP_GROUP_MANAGER_DN` | Нет значения | Уникальное имя (DN) LDAP для менеджеров (Например: “CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local”) | +| `LDAP_GROUP_PROVIDER` | false | Включение поставщика групп LDAP для “Разрешения” в проектах | +| `LDAP_GROUP_BASE_DN` | Нет значения | Уникальное имя (Base DN) LDAP для групп | +| `LDAP_GROUP_FILTER` | Нет значения | Фильтр групп LDAP (Например: “(&(objectClass=group)(sAMAccountName=%s\*))”) | +| `LDAP_GROUP_USER_FILTER` | Empty | Если определено, то Канборд будет искать группы пользователей в LDAP\_GROUP\_BASE\_DN с помощью этого фильтра, это удобно только для posixGroups (Например: `(&(objectClass=posixGroup)(memberUid=%s))`| +| `LDAP_GROUP_ATTRIBUTE_NAME` | cn | атрибут LDAP для имени группы | + + +Примечание + + + +- Атрибуты LDAP должны быть в нижнем регистре + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ldap-profile-picture.markdown b/doc/ru_RU/ldap-profile-picture.markdown new file mode 100644 index 00000000..9d6bb543 --- /dev/null +++ b/doc/ru_RU/ldap-profile-picture.markdown @@ -0,0 +1,46 @@ +Фотография пользователя из профиля LDAP +======================================= + + + +Канборд может автоматически загружать фотографию пользователя из сервера LDAP. + + + +Эта функция возможна только если активирована аутентификация LDAP и указан параметр `LDAP_USER_ATTRIBUTE_PHOTO`. + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +В вашем `config.php`, вы должны установить атрибут LDAP, используемый для хранения изображения. + + + + define('LDAP_USER_ATTRIBUTE_PHOTO', 'jpegPhoto'); + + + +Обычно используются атрибуты `jpegPhoto` или `thumbnailPhoto`. Изображения могут хранится в формате JPEG или PNG. + + + +Для загрузки изображения в пользовательски профиль, администраторы Active Directory могут использовать программу [AD Photo Edit](http://www.cjwdev.co.uk/Software/ADPhotoEdit/Info.html). + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + +Изображение из профиля **загружается при входе, только если изображение не было загружено ранее**. + +Для смены изображения, нужно вручную удалить ранее загруженное изображение из профиля пользователя. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/link-labels.markdown b/doc/ru_RU/link-labels.markdown new file mode 100644 index 00000000..d091a33c --- /dev/null +++ b/doc/ru_RU/link-labels.markdown @@ -0,0 +1,23 @@ +Настройки ссылки +================ + + +Связи в задачах могут быть изменены в настройках приложения (**Настройки** -\> **Настройки ссылки**) + +![Link Labels](https://kanboard.net/screenshots/documentation/link-labels.png) + +Рисунок. Метки для ссылок. + + +Каждая метка может иметь противоположное опеределение. Если нет противоположного значения, метка считается двунаправленная. + +![Link Label Creation](https://kanboard.net/screenshots/documentation/link-label-creation.png) + +Рисунок. Создание ссылки. + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/mysql-configuration.markdown b/doc/ru_RU/mysql-configuration.markdown new file mode 100644 index 00000000..82c02b37 --- /dev/null +++ b/doc/ru_RU/mysql-configuration.markdown @@ -0,0 +1,128 @@ +Настройка Mysql/MariaDB +======================= + + + +По умолчанию Канборд использует для хранения данных Sqlite. Вместо Sqlite возможно использовать Mysql или MariaDB. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Сервер Mysql + + + +- Установленное расширение PHP - `pdo_mysql` + + + +Примечание: работа Канборда протестирована с **Mysql \>= 5.5 и MariaDB \>= 10.0** + + + +Настройка Mysql[¶](#mysql-configuration "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +### Создание базы данных[¶](#create-a-database "Ссылка на этот заголовок") + + + +Первым шагом надо создать базу данных на вашем сервере Mysql. Например, вы можете создать базу в командной строке клиента mysql: + + + + CREATE DATABASE kanboard; + + + +### Создание файла конфигурации[¶](#create-a-config-file "Ссылка на этот заголовок") + + + +Файл `config.php` должен содержать следующие значения: + + + + + + AllowOverride FileInfo Options=All,MultiViews AuthConfig + + + + + +URL ярлыки[¶](#url-shortcuts "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +- Перейти к задаче \#123: **/t/123** + + + +- Перейти на доску в проект \#2: **/b/2** + + + +- Перейти в календарь проекта \#5: **/c/5** + + + +- Перейти к просмотру списком проекта \#8: **/l/8** + + + +- Перейти к настройкам проекта для проекта id \#42: **/p/42** + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +По умолчанию, Канборд проверяет включен ли в Apache mode rewrite. + + + +Для исключения автоматической проверки переопределения URL на веб сервере, вы должны включить эту опцию в вашем конфигурационном фале: + + + + define('ENABLE_URL_REWRITE', true); + + + +Когда константа имеет значение `true`: + + + +- Сгенерированные из утилиты командной строки URL будут также преобразованы + + + +- Если вы используете другой веб сервер вместо Apache, например Nginx или Microsoft IIS, вы можете сами настроить переопределение URL + + + +Примечание: Канборд всегда использует URL по “старинке”, если данная константа не настроена. Эта настройка опциональна. + + + +Пример настройки Nginx[¶](#nginx-configuration-example "Ссылка на этот заголовок") +---------------------------------------------------------------------------------- + + + +В разделе `server`, вашего конфигурационного файла Nginx, вы можете использовать этот пример: + + + + index index.php; + + + + location / { + + try_files $uri $uri/ /index.php$is_args$args; + + + + # If Kanboard is under a subfolder + + # try_files $uri $uri/ /kanboard/index.php; + + } + + + + location ~ \.php$ { + + try_files $uri =404; + + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass unix:/var/run/php5-fpm.sock; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + + fastcgi_index index.php; + + include fastcgi_params; + + } + + + + # Deny access to the directory data + + location ~* /data { + + deny all; + + return 404; + + } + + + + # Deny access to .htaccess + + location ~ /\.ht { + + deny all; + + return 404; + + } + + + +В конфигурационном файле Канборда `config.php`: + + + + define('ENABLE_URL_REWRITE', true); + + + +Адаптируйте пример приведенный выше к вашей конфигурации. + + + +Пример настройки IIS[¶](#iis-configuration-example "Ссылка на этот заголовок") +------------------------------------------------------------------------------ + + + +Создайте web.config в каталоге где установлен Канборд: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +В конфигурационном файле Канборда `config.php`: + + + + define('ENABLE_URL_REWRITE', true); + + + +Адаптируйте пример приведенный выше к вашей конфигурации. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/nitrous.markdown b/doc/ru_RU/nitrous.markdown new file mode 100644 index 00000000..8b975b0d --- /dev/null +++ b/doc/ru_RU/nitrous.markdown @@ -0,0 +1,16 @@ +Nitrous быстрый старт +===================== + + +Создайте свободное окружение разработки для проекта Kanboard в облаке на [Nitrous.io](https://www.nitrous.io). + +Зайдите на ваш сайт через ссылку в IDE `Preview > 3000`{.docutils .literal}. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/notifications.markdown b/doc/ru_RU/notifications.markdown new file mode 100644 index 00000000..8fc37876 --- /dev/null +++ b/doc/ru_RU/notifications.markdown @@ -0,0 +1,111 @@ +Уведомления +=========== + + + +Канборд имеет возможность отправлять сообщения по нескольким каналам: + + + +- Email + +- Веб (уведомления в Канборд) + + + +Внешние плагины позволяют вам посылать уведомления в Slack, Hipchat, Jabber или другие чат системы. + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +Любой пользователь может включить уведоления в своем профиле: в правом верхнем углу выберите во всплывающем меню **Мой профиль** -\> **Уведомления**. Уведомления по умолчанию выключены. + + + +Для получения уведомлений по email вам надо иметь электронную почту (email), которая должна быть указана в вашем профиле, и Канборд должен быть настроен на отправку электронной почты. + + + +![Notifications](https://kanboard.net/screenshots/documentation/notifications.png) + +Рисунок. Уведомления + + + +Вы можете выбрать, удобный для вас, способ получения уведомлений: + + + +- Email + + + +- Веб (смотрите ниже) + + + +Для каждого проекта в котором вы являетесь участником, вы можете выбрать получение уведомления для: + + + +- Всех задач + + + +- Только для задач назначеных вам + + + +- Только для задач, которые создали вы + + + +- Только для задач, созданных вами и назначенных вам + + + +Также, вы можете выбрать проекты из которых хотите получать уведомления. По умолчанию - все проекты, в которых вы являетесь участником. + + + +Веб уведомления[¶](#web-notifications "Ссылка на этот заголовок") +----------------------------------------------------------------- + + + +Веб уведомления доступны на рабочей панели **Мои уведомления** или вверху в виде иконки: + + + +![Web Notifications Icon](https://kanboard.net/screenshots/documentation/web-notifications-icon.png) + +Рисунок. Иконка веб уведомления. + + + +Уведомления отображаются списком. Вы можете выбрать действие **Пометить как прочитанное** для каждого сообщения или отметить сразу все. + + + +![Web Notifications](https://kanboard.net/screenshots/documentation/web-notifications.png) + +Рисунок. Веб уведомления. + + + +Таким образом, вы можете получать веб уведомления без использования электронной почты. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/plugin-directory.markdown b/doc/ru_RU/plugin-directory.markdown new file mode 100644 index 00000000..1920c91d --- /dev/null +++ b/doc/ru_RU/plugin-directory.markdown @@ -0,0 +1,38 @@ +Настройка директории плагинов +============================= + + + +Для установки, обновления и удаления плагинов в интерфейсе пользователя, вам необходимо выполнить следующие пункты: + + + +- Директория плагинов должна быть доступна на запись от пользователя веб сервера + + + +- Расширение zip должно быть доступно на вашем сервере + + + +- Параметр в конфигурации `PLUGIN_INSTALLER` должен быть установлен в `true` + + + +Для отключения этой возможности, измените значение в конфигурационном файле `PLUGIN_INSTALLER` на `false`. Также, вы должны изменить права доступа на директорию плагинов. + + + +Только администраторы могут устанавливать плагины через пользовательский интерфейс. + + + +По умолчанию, доступны только плагины из списка на веб сайте Канборда. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/plugins.markdown b/doc/ru_RU/plugins.markdown new file mode 100644 index 00000000..e5ec2719 --- /dev/null +++ b/doc/ru_RU/plugins.markdown @@ -0,0 +1,167 @@ +Разработка плагина +================== + + + +**Внимание: API плагинов на данный момент в состоянии альфа.** + + + +Плагины удобны для расширения базового функционала Канборда: добавление возможностей, создание тем или изменения базового поведения. + + + +Создатели плагина должны указать точную версию Канборда, под которую написан плагин. Внутренний код Канборда может изменяться и ваш плагин должен тестироваться на совместимость с новой версией. Всегда следите за [ChangeLog](https://github.com/fguillot/kanboard/blob/master/ChangeLog) для внесения изменений. + + + +- [Создание вашего плагина](plugin-registration.markdown) + + + +- [Использование plugin hooks](plugin-hooks.markdown) + + + +- [События](plugin-events.markdown) + + + +- [Изменение базового поведения приложений](plugin-overrides.markdown) + + + +- [Добавление миграции схемы для плагинов](plugin-schema-migrations.markdown) + + + +- [Пользовательские маршруты](plugin-routes.markdown) + + + +- [Добавление обработчиков](plugin-helpers.markdown) + + + +- [Добавление почтовых трансляторов](plugin-mail-transports.markdown) + + + +- [Добавление типов оповещений](plugin-notifications.markdown) + + + +- [Добавление автоматических действий](plugin-automatic-actions.markdown) + + + +- [Расширение данных пользователей, задач и проектов](plugin-metadata.markdown) + + + +- [Архитектура аутентификации](plugin-authentication-architecture.markdown) + + + +- [Регистрация плагина аутентификации](plugin-authentication.markdown) + + + +- [Архитектура авторизации](plugin-authorization-architecture.markdown) + + + +- [Провайдер пользовательской группы](plugin-group-provider.markdown) + + + +- [Провайдер внешней ссылки](plugin-external-link.markdown) + + + +- [Добавление провайдера аватара](plugin-avatar-provider.markdown) + + + +- [Клиент LDAP](plugin-ldap-client.markdown) + + + +Примеры плагинов[¶](#examples-of-plugins "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +- [Двухуровневая аутентификация SMS](https://github.com/kanboard/plugin-sms-2fa) + + + +- [Аутентификация Reverse-Proxy с поддержкой LDAP](https://github.com/kanboard/plugin-reverse-proxy-ldap) + + + +- [Slack](https://github.com/kanboard/plugin-slack) + + + +- [Hipchat](https://github.com/kanboard/plugin-hipchat) + + + +- [Jabber](https://github.com/kanboard/plugin-jabber) + + + +- [Sendgrid](https://github.com/kanboard/plugin-sendgrid) + + + +- [Mailgun](https://github.com/kanboard/plugin-mailgun) + + + +- [Postmark](https://github.com/kanboard/plugin-postmark) + + + +- [Amazon S3](https://github.com/kanboard/plugin-s3) + + + +- [Планирование бюджета](https://github.com/kanboard/plugin-budget) + + + +- [Расписание пользователя](https://github.com/kanboard/plugin-timetable) + + + +- [Прогнозирование подзадач](https://github.com/kanboard/plugin-subtask-forecast) + + + +- [Пример автоматических действий](https://github.com/kanboard/plugin-example-automatic-action) + + + +- [Пример плагина темы](https://github.com/kanboard/plugin-example-theme) + + + +- [Пример плагина CSS](https://github.com/kanboard/plugin-example-css) + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/postgresql-configuration.markdown b/doc/ru_RU/postgresql-configuration.markdown new file mode 100644 index 00000000..9407ce59 --- /dev/null +++ b/doc/ru_RU/postgresql-configuration.markdown @@ -0,0 +1,92 @@ +Настройка Postgresql +==================== + + + +По умолчанию, Канборд использует для хранения данных Sqlite, но возможно использовать и Postgresql. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Установленный и настроенный сервер Postgresql + + + +- Установленное PHP расширение - `pdo_pgsql` (Debian/Ubuntu: `apt-get install php5-pgsql`) + + + +Примечание: работа Канборда протестирована с **Postgresql 9.3 и 9.4** + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +### Создайте пустую базу данных выполнив команду `pgsql`:[¶](#create-an-empty-database-with-the-command-pgsql "Ссылка на этот заголовок") + + + + CREATE DATABASE kanboard; + + + +### Создание конфигурационного файла[¶](#create-a-config-file "Ссылка на этот заголовок") + + + +Файл `config.php` должен содержать следующие значения: + +```php + Настройки -\> Разрешения** + + + +![Project Permissions](screenshots/project-permissions.png) + +Рисунок. Права доступа к проекту + + + +Если вы выберите **Разрешить любому**, то все пользователи Канборд будут считаться участниками Проекта. В таком случае, нет необходимости назначать роли. Потому что, разрешения, назначенные пользователям и группам, на доступ к Проекту не будут работать. + + + +Приватный проект не позволяет устанавливать разрешения. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-types.markdown b/doc/ru_RU/project-types.markdown new file mode 100644 index 00000000..d1169241 --- /dev/null +++ b/doc/ru_RU/project-types.markdown @@ -0,0 +1,27 @@ +Типы проектов +============= + + + +Проекты могут быть двух типов: + + + +| Тип | Описание | +|-----------------|----------------------------------------------------------| +| Командный проект| В проекте могут принимать участие пользователи и группы | +| Приватный проект| Проект принадлежит только одному пользователю, к проекту нельзя присоединить участников| + + + +- Командный проект могут создавать только пользователи с ролью Администратор и Менеджер. +- Приватный проект могут создавать все пользователи. + + +[Читать документацию про роли в Kanboard](roles.markdown) + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/project-views.markdown b/doc/ru_RU/project-views.markdown new file mode 100644 index 00000000..1d1f1117 --- /dev/null +++ b/doc/ru_RU/project-views.markdown @@ -0,0 +1,154 @@ +Представления Доска, Календарь, Список и Гант +============================================= + + + +В каждом проекте задачи могут быть отображены в разных представлениях: **Доска, Календарь, Список и Гант**. Для отображения представлений используется фильтр в верхней части рабочей панели. Для поиска используется [расширенный синтаксис](ext-search.markdown). + + + +Представление - Доска[¶](#board-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + +![Board view](screenshots/board-view.png) + +Рисунок. Представление зачад в виде доски + + + +- В этом представлении вы можете мышкой перемещать задачи между колонками. + + + +- Также, для перемещения задач на доске, можно использовать горячие клавиши **“v b”**. + + + +- Затемнения вокруг задачи показывает активную или измененную задачу. + + + +![Board Task Limit](screenshots/board-task-limit.png) + +Рисунок. Лимит задач на Доске + + + +Когда лимит задач для колонки достигнут, тогда фон колонки становится красный. Это означает, что слишком много задач выполняются одновременно. + + + +[Ознакомится с настройками Доски](board-configuration.markdown) + + + +Представление - Календарь[¶](#calendar-view "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +![Calendar view](screenshots/calendar-view.png) + +Рисунок. Представление в виде календаря + + + +- В этом представлении вы можете видеть задачи на конкретные даты. + + + +- Вы можете сделать настройки, которые позволят вам видеть задачи в работе. + + + +- Вы можете использовать горячие клавиши **“v c”** для перехода на представление Календарь. + + + +- [Ознакомится с настройками Календаря](calendar-configuration.markdown) + + + +Представление - Список[¶](#list-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + +![List view](https://kanboard.net/screenshots/documentation/list-view.png) + +Рисунок. Представление списком. + + + +- С помощью этого представления все результаты отображаются в виде таблицы. + + + +- Для быстрого перехода на представление Список вы можете использовать горячие клавиши **“v l”**. + + + +Представление - Гант.[¶](#gantt-view "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + +![Gantt view](screenshots/gantt-view.png) + +Рисунок. Представление диаграммой Ганта. + + + +- Представление Гант отображает задачи горизонтальными графиками + + + +- Для построения графика используется дата начала и срок выполнения + + + +- Для быстрого перехода к представлению Гант используйте горячие клавиши : **“v g”** + + + +Обзор Проекта[¶](#project-overview "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +![Project overview](screenshots/project-view.png) + +Рисунок. Представления проекта + + + +- Отображает описание проекта + + + +- Показывает прикрепленные и загруженные документы проекта + + + +- Показывает список участников проекта + + + +- Показывает последнюю активность в проекте + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/recurring-tasks.markdown b/doc/ru_RU/recurring-tasks.markdown new file mode 100644 index 00000000..a6572f2c --- /dev/null +++ b/doc/ru_RU/recurring-tasks.markdown @@ -0,0 +1,67 @@ +Повторяющиеся задачи +==================== + + + +Для соответсвия методологии Канбан, повторяющиеся задачи не имеют в качестве основы дату, а запускаются при наступлении событий на Доске. + + + +- Повторяющиеся задачи копируются (появляются вновь) в первой колонке Доски когда наступает определенное событие + + + +- Дата завершения (срок выполнения задачи) пересчитывается автоматически + + + +- Each task records the task id of the parent task that created it and the child task created + + + +Настройка[¶](#configuration "Ссылка на этот заголовок") +------------------------------------------------------- + + + +Перейдите на страницу детального представления задачи или используйте выпадающее меню на доске, выберите **Редактировать повторы**. + + + +![Recurring task](https://kanboard.net/screenshots/documentation/recurring-tasks.png) + +Рисунок. Редактировать повторы. + + + +В редактировании повторов имеется выбор 3 триггеров для генерации периодической задачи: + + + +- Когда задача перемещается из первой колонки + + + +- Когда задача перемещается в последнюю колонку + + + +- Когда задача закрывается + + + +Дата завершения, если установлена для текущей задачи, может быть пересчитана с учетом **Коэффициента для расчета новой даты** и **Период для рассчета новой даты завершения** (например, 7 дней, 6 месяцев, 1 год). Базовой датой вычисления новой даты завершения может быть и имеющаяся дата завершения, или дата действия. + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/requirements.markdown b/doc/ru_RU/requirements.markdown new file mode 100644 index 00000000..aa6933b9 --- /dev/null +++ b/doc/ru_RU/requirements.markdown @@ -0,0 +1,137 @@ +Системные требования +==================== + + + +На сервере[¶](#server-side "Ссылка на этот заголовок") +------------------------------------------------------ + + + +### Поддерживаемые операционные системы[¶](#compatible-operating-systems "Ссылка на этот заголовок") + +|Операционная система| +|-----------------------------------| +|Linux Ubuntu Xenial Xerus 16.04 LTS| +| Linux Ubuntu Trusty 14.04 LTS| +| Linux Centos 6.x| +| Linux Centos 7.x| +| Linux Redhat 6.x| +|Linux Redhat 7.x| +| Linux Debian 8| +| FreeBSD 10.x| +| Microsoft Windows 2012 R2| +| Microsoft Windows 2008| + + + +### Поддерживаемые базы данных[¶](#compatible-databases "Ссылка на этот заголовок") + + +|База данных | +|----------------------| +|Sqlite 3.x | +|Mysql \>= 5.5 | +|MariaDB \>= 10 | +| Postgresql \>= 9.3 | + + + +Какую базу данных выбрать? + + +| Тип | Когда использовать | +|--------------------|--------------------------------------------------------| +| Sqlite | Один пользователь или небольшая команда | +| Mysql/Postgres | Большая команда, конфигурация высокой доступности | + + + + +Не используйте Sqlite на смонтированном NFS. Используйте Sqlite только на дисках с высокой скоростью чтение/запись. + + + +### Совместимые веб сервера[¶](#compatible-web-servers "Ссылка на этот заголовок") + +Apache HTTP Server, Nginx , Microsoft IIS + +Канборд изначально сконфигурирован для работы с Apache (URL rewriting). + + + +### Версии PHP[¶](#php-versions "Ссылка на этот заголовок") + + +PHP \>= 5.3.3, PHP 5.4, PHP 5.5, PHP 5.6, PHP 7.x + + + +### Требуемые расширения для PHP[¶](#php-extensions-required "Ссылка на этот заголовок") + + +| Требуемые расширения для PHP | Примечание | +|----------------------------------|-----------------------------------------| +| pdo\_sqlite | Только при использовании Sqlite | +| pdo\_mysql | Только при использоании Mysql/MariaDB | +| pdo\_pgsql | Только при использовании Postgres | +| gd |   | +| mbstring |   | +| openssl |   | +| json |   | +| hash |   | +| ctype |   | +| session |   | +| ldap | Только для аутентификации LDAP | +| Zend OPcache | Рекомендуется | + + +### Рекомендуется[¶](#recommendations "Ссылка на этот заголовок") + + + +- Современная Linux или Unix операционная система. + + + +- Высокая производительность достигается с последней версией PHP со включенным кешированием OPcode. + + + +На клиенте[¶](#client-side "Ссылка на этот заголовок") +------------------------------------------------------ + + + +### Браузеры[¶](#browsers "Ссылка на этот заголовок") + + + +Используйте современные браузеры, обновленные до последней версии: + +|Браузер | +|-----------------| +| Safari | +| Google Chrome | +| Mozilla Firefox | +| Microsoft Internet Explorer \>= 11| +| Microsoft Edge | + + + +### Устройства[¶](#devices "Ссылка на этот заголовок") + + +| Устройство | Разрешение экрана | +|--------------------------------------|--------------------------------------| +| Персональный компьютер или ноутбук | \>= 1366 x 768 | +| Планшет | \>= 1024 x 768 | + + +Канборд, пока, не оптимизирован для работы на смартфонах. Конечно, он работает, но пользовательский интерфейс не совсем удобный для использования. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/reverse-proxy-authentication.markdown b/doc/ru_RU/reverse-proxy-authentication.markdown new file mode 100644 index 00000000..2d97a6e4 --- /dev/null +++ b/doc/ru_RU/reverse-proxy-authentication.markdown @@ -0,0 +1,138 @@ +Аутентификация Reverse Proxy +============================ + + + +Этот метод аутентификации часто используется для [SSO](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%85%D0%BD%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F_%D0%B5%D0%B4%D0%B8%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D1%85%D0%BE%D0%B4%D0%B0) (Технология единого входа), особенно удобно в больших организациях. + + + +Аутентификация выполняется с помощью другой системы, поэтому Канборд не знает вашего пароля и допускает вас к приложению, так как вы уже прошли аутентификацию. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Правильно сконфигурированный reverse proxy + + + +или + + + +- Apache Auth на том же сервере + + + +Как это работает?[¶](#how-does-this-work "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +1. Ваш reverse proxy аутентифицирует пользователя и посылает имя пользователя через заголовок HTTP. + + + +2. Канборд извлекает имя пользователя из запроса + + + + - Пользователь создается в Канборд автоматически (опция настраивается) + + + + - Открывается новая сессия Канборд (дополнительная аутентификация в Канборд не нужна) + + + +Инструкция по установке[¶](#installation-instructions "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +### Настройка вашего reverse proxy[¶](#setting-up-your-reverse-proxy "Ссылка на этот заголовок") + + + +В рамках данной документации не рассматривается установка и настройка reverse proxy. Вы должны убедится, что логин пользователя отправляется с reverse proxy в заголовке HTTP. + + + +### Настройки Канборда[¶](#setting-up-kanboard "Ссылка на этот заголовок") + + + +Создайте свой файл конфигурации `config.php` или скопируйте конфигурацию из файла `config.default.php`: + + + + \`\_\_ имя заголовка будет `REMOTE_USER`. Например, Apache добавляет `REMOTE_USER` по умолчанию, если установлено `Require valid-user`. + + + +- Если Apache служит reverse proxy для другого Apache выполняющего Канборд, то заголовок `REMOTE_USER` не установлен (это же относится к IIS и Nginx). + + + +- Если у вас имеется действующий reverse proxy, то [проект HTTP ICAP](http://tools.ietf.org/html/draft-stecher-icap-subid-00#section-3.4) предполагает, что заголовок должен быть `X-Authenticated-User`. Этот стандарт де-факто был принят разными инструментами. + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/roles.markdown b/doc/ru_RU/roles.markdown new file mode 100644 index 00000000..5af8a937 --- /dev/null +++ b/doc/ru_RU/roles.markdown @@ -0,0 +1,44 @@ +Пользовательские роли +===================== + + + +Роли в приложениях[¶](#application-roles "Ссылка на этот заголовок") + +-------------------------------------------------------------------- + + + +Каждый пользователь системы Канборд имеет одну из этих ролей + + + +| Роль | Описание | +|----------------|-----------------------------------------------------------| +| Администратор | Имеет доступ ко всему | +| Менеджер | Может создавать командные проекты, но не может изменять настройки приложения | +| Пользователь | Может создавать только приватные проекты | + + + + +Роли в проектах[¶](#project-roles "Ссылка на этот заголовок") + +------------------------------------------------------------- + + + +В каждом командном проекте могут быть назначены разные роли для пользователей и групп: + + +| Роль | Описание | +|-----------------|----------------------------------------------------------| +| Менеджер проекта| Может изменять настройки проекта, имеет доступ к диаграмме Ганта и отчетам | +| Участник проекта| Может создавать задачи и пользоваться доской | +| Наблюдатель проекта | Имеет доступ к доске и задачам только на просмотр (чтение) | + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/rss.markdown b/doc/ru_RU/rss.markdown new file mode 100644 index 00000000..c4718880 --- /dev/null +++ b/doc/ru_RU/rss.markdown @@ -0,0 +1,58 @@ +RSS/Atom подписки +================= + + + +Канборд поддерживает RSS ленты для проектов и пользователей. + + + +- RSS/Atom лента для проекта - содержит только активность в проекте + + + +- RSS/Atom лента пользователя - содержит поток активности пользователя во всех проектах, в которых пользователь является участником + + + +Эти подписки доступны только при включенном общем доступе в пользовательском профиле или в настройках проекта. + + + +Включение/выключение RSS ленты проекта[¶](#enable-disable-project-rss-feeds "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------- + + + +Перейдите в **Настройки проекта** -\> **Общий доступ** + + + +![Disable public access](https://kanboard.net/screenshots/documentation/project-disable-sharing.png) + +Рисунок. Выключение общего доступа. + + + +Включение/выключение RSS ленты пользователя[¶](#enable-disable-user-rss-feeds "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + + + +Перейдите в **Мой профиль** -\> **Общий доступ** + + + +Ссылка на RSS ленту защищена случайным ключом, только пользователи, которые знают URL ссылку, могут иметь доступ к ленте. + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/screenshots.markdown b/doc/ru_RU/screenshots.markdown new file mode 100644 index 00000000..2260f258 --- /dev/null +++ b/doc/ru_RU/screenshots.markdown @@ -0,0 +1,74 @@ +Добавление снимков экрана (скриншота) +===================================== + + + +Для экономии времени вы можете копировать и вставлять изображения прямо в Канборде. Загруженные изображения прикрепляются к задаче. + + + +Например, очень удобно для решения проблемы прикрепить снимок экрана. + + + +Вы можете добавить снимок экрана прямо из Доски нажав на выпадающее меню задачи и выбрав **Прикрепить картинку** или на странице детального просмотра задачи. + + + +![Drop-down screenshot menu](https://kanboard.net/screenshots/documentation/dropdown-screenshot.png) + + + +Рисунок. Выпадающее меню задачи - **Прикрепить картинку**. + + + +Для добавления нового снимка экрана (скриншота), сделайте снимок экрана (нажмите клавиши Ctrl+PrtScn) и вставьте его используя сочетания клавиш CTRL+V или Command+V + + + +![Screenshot page](https://kanboard.net/screenshots/documentation/task-screenshot.png) + +Рисунок. Прикрепить картинку. + + + +В Mac OS X вы можете использовать следующие горячие клавиши для создания снимка экрана: + + + +- Command-Control-Shift-3: Делает снимок экрана и сохраняет его в буфер обмена + + + +- Command-Control-Shift-4 и выделите необходимую область на экране: Делает снимок экрана для области экрана и сохраняет ее в буфер обмена + + + +- Command-Control-Shift-4, затем пробел, затем нажать на окно: Делает снимок окна и сохраняет его в буфер обмена + + + +Имеется много разных других программ для создания снимков с экрана с примечаниями и разными формами. + + + +**Заметка**: Эта возможность работает не во всех браузерах. Например, не работает в Safari из-за этой ошибки: [https://bugs.webkit.org/show\_bug.cgi?id=49141](https://bugs.webkit.org/show_bug.cgi?id=49141) + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/search.markdown b/doc/ru_RU/search.markdown new file mode 100644 index 00000000..14c3f5b1 --- /dev/null +++ b/doc/ru_RU/search.markdown @@ -0,0 +1,24 @@ +Поиск + +===== + + + +Для работы поиска включите JavaScript в браузере. + + + +Здесь можно делать поиск по всем разделам этой документации. Введите ключевые слова в текстовое поле и нажмите кнопку «искать». Внимание: будут найдены только те страницы, в которых есть все указанные слова. Страницы, где есть только часть этих слов, отобраны не будут. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/sharing-projects.markdown b/doc/ru_RU/sharing-projects.markdown new file mode 100644 index 00000000..e8448189 --- /dev/null +++ b/doc/ru_RU/sharing-projects.markdown @@ -0,0 +1,82 @@ +Публичные доски и задачи +======================== + + + +По умолчанию, Доска имеет приватный доступ, но имеется возможность сделать Доску публичной. + + + +Публичная доска **не может быть изменена (имеется только доступ на чтение)**. Доступ к доске защищен случайно сгенерированным ключом, только пользователи знающие правильный URL могут увидеть публичную Доску. + + + +Публичная Доска автоматически обновляется каждые 60 секунд. Детали задач, также, доступны только для чтения. + + + +Пример использования: + + + +- Публикация вашей Доски для кого-либо снаружи (работник из другой организации) + + + +- Отображение Доски на большом экране в вашем офисе + + + +Включение общего доступа[¶](#enable-public-access "Ссылка на этот заголовок") +----------------------------------------------------------------------------- + + + +Выберете ваш проект, затем нажмите на ссылку **“Общий доступ”** и в завершении нажмите на кнопку **“Включить общий доступ”** + + + +![Enable public access](screenshots/project-enable-sharing.png) + +Рисунок. Включение общего доступа + + + +Когда общий доступ к проекту включен, сгенерируется несколько ссылок: + + + +- Ссылка для просмотра + + + +- RSS лента + + + +- iCalendar данные + + + +![Disable public access](screenshots/project-disable-sharing.png) + +Рисунок. Отключить общий доступ. + + + +Вы можете выключить общий доступ к проекту в любой момент. + + + +Каждый раз, когда вы включаете или выключаете общий доступ, генерируется новый ключ. **Доступ по предыдущей ссылке будет невозможен**. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/sqlite-database.markdown b/doc/ru_RU/sqlite-database.markdown new file mode 100644 index 00000000..202452cb --- /dev/null +++ b/doc/ru_RU/sqlite-database.markdown @@ -0,0 +1,96 @@ +Настройка базы данных Sqlite +============================ + + + +Канборд использует для хранения данных Sqlite по умолчанию. Все задачи, проекты и учетные записи пользователей храняться в этой базе данных. + + + +База данных Sqlite хранит данные в файле `db.sqlite` в директории `data`. + + + +Экспорт/Резервное копирование[¶](#export-backup "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + + +### Командная строка[¶](#command-line "Ссылка на этот заголовок") + + + +Создание резервных копий выполняется просто, надо скопировать файл `data/db.sqlite` туда, где у вас будут хранится резервные копии. + + + +### Пользовательский интерфейс[¶](#user-interface "Ссылка на этот заголовок") + + + +Также, в любое время, вы можете скачать базу данных прямо через меню **Настройки**. + + + +Выгружаемая база данных упакована с помощью Gzip и имя базы выглядитит как `db.sqlite.gz`. + + + +Импорт/Восстановление[¶](#import-restoration "Ссылка на этот заголовок") +------------------------------------------------------------------------ + + + +Загрузить базу данных через пользовательский интерфейс невозможно. Восстановление должно быть выполнено вручную, когда никто не работает с программой. + + + +- Для восстановления резервной копии, достаточно заменить рабочий файл `data/db.sqlite`. + + + +- Для разархивирования базы данных упакованной с помощью gzip, выполните следующую команду в терминале: `gunzip db.sqlite.gz`. + + + +Оптимизация[¶](#optimization "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +Время от времени, рекомендуется оптимизировать базу данных выполнив команду `VACUUM`. Эта команда пересоздает всю базу данных и используется в следующих случаях: + + + +- Для уменьшения размера файла базы данных. В процессе работы пользователей, после удаления записей, в базе данных остается пустое пространство и, соответственно, размер файла базы данных остается прежним. + + + +- Дефрагментация, база данных фрагментирована выполнением частыми вставками или обновлениями. + + + +### Выполнение оптимизации в командной строке[¶](#from-the-command-line "Ссылка на этот заголовок") + + + + sqlite3 data/db.sqlite 'VACUUM' + + + +### Выполнение оптимизации через пользовательский интерфейс[¶](#from-the-user-interface "Ссылка на этот заголовок") + + + +Перейдите в правое выпадающее меню **Настройки** и нажмите на ссылку **Оптимизировать базу данных** + + + +Для дополнительной информации, изучите [документацию Sqlite](https://sqlite.org/lang_vacuum.html). + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/subtasks.markdown b/doc/ru_RU/subtasks.markdown new file mode 100644 index 00000000..c78aee73 --- /dev/null +++ b/doc/ru_RU/subtasks.markdown @@ -0,0 +1,111 @@ +Подзадачи +========= + + +Подзадачи - это прекрасная возможность разделить основную задачу на части. + + + +Каждая подзадача: + + + +- Может быть назначена участнику проекта + + + +- Имеет 3 разных статуса: **Для исполнения**, **В работе**, **Выполнено** + + + +- Имеет информацию по отслеживанию времени: **затраченное время** и **запланированное время** + + + +- Может быть перемещена в списке, для изменения порядка выполнения + + + +Создание подзадачи[¶](#creating-subtasks "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +В детальном представлении задачи, в левой боковой панели нажмите **Добавить подзадачу**: + + + +![Add a subtask](https://kanboard.net/screenshots/documentation/add-subtask.png) + +Рисунок. Добавление подзадачи. + + + +Вы, также, можете быстро добавить подзадачу нажав на заголовок: + + + +![Add a subtask from the task view](https://kanboard.net/screenshots/documentation/add-subtask-shortcut.png) + +Рисунок. Добавление подзадачи на странице детального просмотра задачи. + + + +Изменение статуса подзадачи[¶](#change-subtask-status "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +Когда вы нажимаете на заголовок подзадачи стату меняется: + + + +![Subtask in progress](https://kanboard.net/screenshots/documentation/subtask-status-inprogress.png) + +Рисунок. Выполнение подзадачи. + + + +Иконка перед названием подзадачи обновляется в соответсвии со статусом. + + + +![Subtask done](https://kanboard.net/screenshots/documentation/subtask-status-done.png) + +Рисунок. Подзадача выполнена. + + + +**Заметка**: Когда задача закрыта, то все подзадачи меняют статус на **Выполнена**. + + + +Таймер подзадачи[¶](#subtask-timer "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +- Когда подзадача выполняется, таймер должен быт запущен. Таймер можно запустить и остановить в любое время. + + + +- Время таймера записывается автоматически в затраченное время. Так же, вы можете изменить вручную значение **затраченного времени** при редактировании подзадачи. + + + +- Подсчитываемое время округляется до 15 минут. Эта информация записывается в отдельную таблицу. + + + +- Время, затраченное на выполнение задачи, и запланированнное время обновляется автоматически, в соответсвии с суммой всех подзадач. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/suse-installation.markdown b/doc/ru_RU/suse-installation.markdown new file mode 100644 index 00000000..6d508708 --- /dev/null +++ b/doc/ru_RU/suse-installation.markdown @@ -0,0 +1,36 @@ +Инсталяция на OpenSuse +====================== + + + +OpenSuse Leap 42.1[¶](#opensuse-leap-42-1 "Ссылка на этот заголовок") +--------------------------------------------------------------------- + + + + sudo zypper install php5 php5-sqlite php5-gd php5-json php5-mcrypt php5-mbstring php5-openssl + + cd /srv/www/htdocs + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chmod -R 777 kanboard + + sudo rm kanboard-latest.zip + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/swimlanes.markdown b/doc/ru_RU/swimlanes.markdown new file mode 100644 index 00000000..d6e36fdd --- /dev/null +++ b/doc/ru_RU/swimlanes.markdown @@ -0,0 +1,81 @@ +Дорожки +======= + + + +Дорожки - это горизонтальное разделение вашей Доски. Например, очень удобно разделять релизы программ, разделить ваши задачи для разных продуктов, команд или чего-то еще. + + + +Доска с дорожками[¶](#board-with-swimlanes "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +![Swimlanes](screenshots/swimlanes.png) + +Рисунок. Дорожки + + + +- Вы можете свернуть дорожку нажав на иконку слева + + + +- “Стандатная дорожка” всегда расположена сверху + + + +Управление дорожками[¶](#managing-swimlanes "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +- Все проекты имеют дорожку по умолчанию - **Стандартная дорожка** + + + +- Если имеется больше одной дорожки, то на Доске будут показаны все имеющиеся дорожки. + + + +- Вы можете перемещать мышкой задачи между дорожками. + + + +Для настройки дорожек перейдите на страницу **настройки проекта** (Меню -\> Настройки) и нажмите **Дорожки** (слева). + + + +![Swimlanes Configuration](screenshots/swimlane-configuration.png) + +Рисунок. Настройка Дорожек. + + + +Теперь вы можете добавить новую дорожку или переименовать стандартную дорожку. Также, вы можете выключить дорожку или изменить расположение любой дорожки. + + + +- Стандартная дорожка всегда расположена сверху, но вы можете ее выключить и она не будет отображаться на Доске. + + + +- Выключенные дорожки не отображаются на Доске. + + + +- **Удаление дорожки не влечет за собой удаление расположенных на этой дорожке задач**, эти задачи будут перемещены в “Стандартную дорожку”. + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/syntax-guide.markdown b/doc/ru_RU/syntax-guide.markdown new file mode 100644 index 00000000..9d7414a8 --- /dev/null +++ b/doc/ru_RU/syntax-guide.markdown @@ -0,0 +1,246 @@ +Руководство по синтаксису +========================= + + + +Канборд использует [Markdown синтаксис](https://ru.wikipedia.org/wiki/Markdown) для комментариев или описания задач. Далее приведены примеры: + + + +Жирный и курсив[¶](#bold-and-italic "Ссылка на этот заголовок") +--------------------------------------------------------------- + +- Жирный текст: Используйте 2 звездочки или 2 подчеркивания вокруг слов(а) + + + +- Курсив: Используйте 1 звездочку или 1 подчеркивание вокруг слов(а) + + + +### Пример написания (источник)[¶](#source "Ссылка на этот заголовок") + + + + This **word** is very __important__. + + + + And here, an *italic* word with one _underscore_. + + + +### Результат[¶](#result "Ссылка на этот заголовок") + + + +This **word** is very **important**. + + + +And here, an *italic* word with one *underscore*. + + + +Неупорядоченные списки[¶](#unordered-lists "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +Неупорядоченный список использует звездочки, минусы или плюсы вначале абзаца + + + +### Пример написания (источник)[¶](#id1 "Ссылка на этот заголовок") + + + + - Item 1 + + - Item 2 + + - Item 3 + + + + or + + + + * Item 1 + + * Item 2 + + * Item 3 + + + +### Результат[¶](#id2 "Ссылка на этот заголовок") + + + +- Item 1 + +- Item 2 + +- Item 3 + + + +Упорядоченные списки[¶](#ordered-lists "Ссылка на этот заголовок") +------------------------------------------------------------------ + + + +Упорядоченные списки префиксом имеют цифру: + + + +### Пример написания (источник)[¶](#id3 "Ссылка на этот заголовок") + + + + 1. Do that first + + 2. Do this + + 3. And that + + + +### Результат[¶](#id4 "Ссылка на этот заголовок") + + + +1. Do that first + +2. Do this + +3. And that + + + +Ссылки[¶](#links "Ссылка на этот заголовок") +-------------------------------------------- + + + +### Пример написания (источник)[¶](#id5 "Ссылка на этот заголовок") + + + + [My link title](https://kanboard.net/) + + + + + + + +### Результат[¶](#id6 "Ссылка на этот заголовок") + + + +[My link title](https://kanboard.net/) + + + +[https://kanboard.net](https://kanboard.net) + + + +Исходный код[¶](#source-code "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +### Код встраиваемый в текст[¶](#inline-code "Ссылка на этот заголовок") + + + +Используйте обратные кавычки (переключитесь на анлийскую раскладку и нажмите ё) + + + + Execute this command: `tail -f /var/log/messages`. + + + +### Результат[¶](#id7 "Ссылка на этот заголовок") + + + +Execute this command: `tail -f /var/log/messages`{.docutils .literal}. + + + +### Блоки кода[¶](#code-blocks "Ссылка на этот заголовок") + + + +Используйте 3 обратных кавычки с указанием языка программирования + + + + ```php + + + + ``` + + + +### Результат[¶](#id8 "Ссылка на этот заголовок") + + + + + + + +Заголовки[¶](#titles "Ссылка на этот заголовок") +------------------------------------------------ + + + +### Пример написания (источник)[¶](#id9 "Ссылка на этот заголовок") + + + + # Title level 1 + + + + ## Title level 2 + + + + ### Title level 3 + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/task-links.markdown b/doc/ru_RU/task-links.markdown new file mode 100644 index 00000000..2912f91b --- /dev/null +++ b/doc/ru_RU/task-links.markdown @@ -0,0 +1,93 @@ +Ссылки на задачи +================ + + + +Задачи могут быть созданы вместе с предопределенными связями: + + + +![Task Links](https://kanboard.net/screenshots/documentation/task-links.png) + +Рисунок. Ссылки на задачи + + + +Связи по умолчанию: + + + +- **относится к** + + + +- **блокирована**| блокирует + + + +- **блокирует** | блокирована + + + +- **дублирована** | дублирует + + + +- **дублирует** | дублирована + + + +- **является продолжением** | является началом для + + + +- **является началом для** | является продолжением + + + +- **часть вехи** | является вехой для + + + +- **является вехой для** | часть вехи + + + +- **исправлено** | исправляет + + + +- **исправляет** | исправлено + + + +Эти названия могут быть быть изменены в настройках приложения. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/tests.markdown b/doc/ru_RU/tests.markdown new file mode 100644 index 00000000..2373d030 --- /dev/null +++ b/doc/ru_RU/tests.markdown @@ -0,0 +1,262 @@ +Автоматизированные тесты +======================== + + + +[PHPUnit](https://phpunit.de/) используется для запуска автоматизированных тестов в Канборд. + + + +Вы можете запускать тесты для разных баз данных (Sqlite, Mysql and Postgresql), чтобы убедится, что результаты будут одинаковые. + + + +Требования[¶](#requirements "Ссылка на этот заголовок") +------------------------------------------------------- + + + +- Компьютер Linux/Unix + + + +- PHP cli + + + +- Установленный PHPUnit + + + +- Mysql и Postgresql (опционально) + + + +Unit тесты[¶](#unit-tests "Ссылка на этот заголовок") +----------------------------------------------------- + + + +### Тестирование с Sqlite[¶](#test-with-sqlite "Ссылка на этот заголовок") + + + +Sqlite тестирование использует базу данных в памяти, без использования записи на файловую систему. + + + +Конфигурационный файл PHPUnit - `tests/units.sqlite.xml`. Из директории Kanboard запустите команду `phpunit -c tests/units.sqlite.xml`. + + + +Пример: + + + + phpunit -c tests/units.sqlite.xml + + + + PHPUnit 5.0.0 by Sebastian Bergmann and contributors. + + + + ............................................................... 63 / 649 ( 9%) + + ............................................................... 126 / 649 ( 19%) + + ............................................................... 189 / 649 ( 29%) + + ............................................................... 252 / 649 ( 38%) + + ............................................................... 315 / 649 ( 48%) + + ............................................................... 378 / 649 ( 58%) + + ............................................................... 441 / 649 ( 67%) + + ............................................................... 504 / 649 ( 77%) + + ............................................................... 567 / 649 ( 87%) + + ............................................................... 630 / 649 ( 97%) + + ................... 649 / 649 (100%) + + + + Time: 1.22 minutes, Memory: 151.25Mb + + + + OK (649 tests, 43595 assertions) + + + +### Тестирование с Mysql[¶](#test-with-mysql "Ссылка на этот заголовок") + + + +У вас должна быть локально установлена база данных Mysql или MariaDb. + + + +По умолчанию, используются следующие учетные данные: + + + +- Hostname: **localhost** + +- Username: **root** + +- Password: none + +- Database: **kanboard\_unit\_test** + + + +При каждом выполнении база данных удаляется и создается снова. + + + +Конфигурационный файл HPUnit - `tests/units.mysql.xml`. Из директории Kanboard запустите команду `phpunit -c tests/units.mysql.xml`. + + + +### Тестирование с Postgresql[¶](#test-with-postgresql "Ссылка на этот заголовок") + + + +У вас должен быть локально установлен Postgresql. + + + +По умолчанию, используются следующие учетные данные: + + + +- Hostname: **localhost** + +- Username: **postgres** + +- Password: none + +- Database: **kanboard\_unit\_test** + + + +Убедитесь, что пользователь `postgres` может создавать и удалять базу данных. База данных пересоздается при каждом выполнении теста. + + + +Конфигурационных файл PHPUnit - `tests/units.postgres.xml`. Из директории Kanboard, запустите команду `phpunit -c tests/units.postgres.xml`. + + + +Тесты интеграции[¶](#integration-tests "Ссылка на этот заголовок") +------------------------------------------------------------------ + + + +Фактически тестируются только вызовы API. + + + +Реальные HTTP calls выполняются с этими тестами. Поэтому, необходим локальный экземпляр Канборда, который слушает на `http://localhost:8000/`. + + + +Все данные будут удалены/изменены при тестировании. Более того скрипт будет сброшен и установлен новый ключ API. + + + +1. Запустите локольный экземпляр Канборда: `php -S 127.0.0.1:8000` + + + +2. Запустите тест в другом терминале + + + +Этот же метод используется для запуска тестов для разных баз данных: + + + +- Sqlite: `phpunit -c tests/integration.sqlite.xml` + +- Mysql: `phpunit -c tests/integration.mysql.xml` + +- Postgresql: `phpunit -c tests/integration.postgres.xml` + + + +Пример: + + + + phpunit -c tests/integration.sqlite.xml + + + + PHPUnit 5.0.0 by Sebastian Bergmann and contributors. + + + + ............................................................... 63 / 135 ( 46%) + + ............................................................... 126 / 135 ( 93%) + + ......... 135 / 135 (100%) + + + + Time: 1.18 minutes, Memory: 14.75Mb + + + + OK (135 tests, 526 assertions) + + + +Непрерывная интеграция с Travis-CI[¶](#continuous-integration-with-travis-ci "Ссылка на этот заголовок") + +-------------------------------------------------------------------------------------------------------- + + + +После каждого commit влитого в мой репозиторий, юнит тесты выполняются для 5 различных версий PHP: + + + +- PHP 7.0 + +- PHP 5.6 + +- PHP 5.5 + +- PHP 5.4 + +- PHP 5.3 + + + +При тестировании каждой версии PHP используются 3 поддерживаемые базы данных: Sqlite, Mysql and Postgresql. + + + +Конфигурационный файл Travis - `.travis.yml` - находится в корневой директории Kanboard. + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/time-tracking.markdown b/doc/ru_RU/time-tracking.markdown new file mode 100644 index 00000000..98364d38 --- /dev/null +++ b/doc/ru_RU/time-tracking.markdown @@ -0,0 +1,112 @@ +Отслеживание времени +==================== + + + +Отслеживание времени (контроль времени) может быть использовано для уровня задач или для уровня подзадач. + + + +Отслеживание времени испольнения задач[¶](#task-time-tracking "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------- + + + +![Task time tracking](https://kanboard.net/screenshots/documentation/task-time-tracking.png) + +Рисунок. Отслеживание времени испольнения задач + + + +Задачи имеют два поля: + + + +- Запланировано времени + + + +- Затрачено времени + + + +Эти значения показывают время работы и могут быть установлены вручную + + + +Отслеживание времени подзадач[¶](#subtask-time-tracking "Ссылка на этот заголовок") +----------------------------------------------------------------------------------- + + + +![Subtask time tracking](https://kanboard.net/screenshots/documentation/subtask-time-tracking.png) + +Рисунок. Отслеживание времени подзадач + + + +Подзадачи тоже имеют поля “Запланировано” и “Затрачено” время. + + + +Когда вы меняете значения в этих полях, **отслеживание времени задачи обновляется автоматически и формируется суммарное время всех подзадач** + + + +Канборд записывает время между изменениями статуса каждой подзадачи в отдельную таблицу. + + + +- При изменении статуса подзадачи с **“Для испольнения”** на **“В работе”**, записывается время начала + + + +- При изменении статуса подзадачи с **“В работе”** на **“Выполнено”**, записывается как время окончания и, при этом, обновляется **затраченное время** в подзадаче и в задаче. + + + +Анализ всех записей можно увидеть на странице детального просмотра задачи: + + + +![Task timesheet](https://kanboard.net/screenshots/documentation/task-timesheet.png) + +Рисунок. Таблица учета времени. + + + +Для каждой подзадачи, таймер может быть остановлен и запущен в любое время: + + + +![Subtask timer](https://kanboard.net/screenshots/documentation/subtask-timer.png) + +Рисунок. Таймер подзадач. + + + +- Таймер не зависит от статуса подзадачи + + + +- Вы можете запустить таймер для новой записи, созданной в таблице отслеживания задач, в любое время + + + +- Вы можете остановить учет времени даты завершения в таблице отслеживания задач, в любое время + + + +- Подсчет затраченного времени округляется до четверти часа + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/transitions.markdown b/doc/ru_RU/transitions.markdown new file mode 100644 index 00000000..efb95c50 --- /dev/null +++ b/doc/ru_RU/transitions.markdown @@ -0,0 +1,60 @@ +Перемещения задач +================= + + + +Запись о перемещении отражает каждое движение задачи между колонками. + + + +![Transitions](https://kanboard.net/screenshots/documentation/transitions.png) + +Рисунок. Перемещения. + + + +Перемещение доступно в боковом меню в детальном представлении задачи (**Перемещения**). Вы можете увидеть следующую информацию: + + + +- Дата, когда было выполенено перемещение + + + +- Исходная колонка - колонка, из которой было сделано перемещение + + + +- Колонка назначения - колонка, в которую была перемещена задача + + + +- Исполнитель (пользователь, который переместил задачу) + + + +- Время проведенное в колонке (сколько времени было затрачено на выполнение задачи в указанной колонке) + + + +Данные о перемещении задач, также, могут быть экспортированы со страницы настроек проекта (**Меню** -\> **Экспорт**). + + + +![Transitions Export](https://kanboard.net/screenshots/documentation/transitions-export.png) + +Рисунок. Экспорт перемещений задач. + + + +Для указанного промежутка времени вы можете сформировать CSV файл, который вы можете импортировать в любое программное обеспечение с электронными таблицами (например, Excell). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/translations.markdown b/doc/ru_RU/translations.markdown new file mode 100644 index 00000000..f4bcafc0 --- /dev/null +++ b/doc/ru_RU/translations.markdown @@ -0,0 +1,155 @@ +Переводы на другие языки (локализация) +====================================== + + + +Как перевести Канборд на новый язык?[¶](#how-to-translate-kanboard-to-a-new-language "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------------- + + + +- Переводы хранятся в директории `app/Locale` + + + +- В этой директории есть поддиректории для разных языков, например, для русского имеется `ru_RU`, для французского - `fr_FR` и т.д. + + + +- Переводы находятся в PHP файле, который возвращает массив с парой ключ-значение + + + +- Ключ - оригинальный текст на английском и значение - перевод на соответсвующем языке + + + +- **Французские переводы всегда в актуальном состоянии** + + + +- Всегда используйте последнюю версию (branch master) + + + +### Создание нового перевода[¶](#create-a-new-translation "Ссылка на этот заголовок") + + + +1. Создайте новую директорию: `app/Locale/xx_XX`, например `app/Locale/fr_CA` для канадского фрацузского + + + +2. Создайте новый файл для перевода: `app/Locale/xx_XX/translations.php` + + + +3. Используйте как образец содержимое французского перевода (локализации) и замените значения + + + +4. Внесите изменения в файл `app/Model/Language.php` + + + +5. Проверьте добавленный язык на локальной версии Канборда + + + +6. Пошлите [pull-request на Github](https://help.github.com/articles/using-pull-requests/) + + + +Как обновить имеющийся перевод?[¶](#how-to-update-an-existing-translation "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------- + + + +1. Откройте файл перевода `app/Locale/xx_XX/translations.php` + + + +2. Отсутсвующие переводы закоментированы - `//` и значения пустые, нужно заполнить значения и удалить коментарий + + + +3. Проверьте внесенные изменения на локальной версии Канборда и пошлите [pull-request](https://help.github.com/articles/using-pull-requests/) + + + +Как добавить новый текст перевода в приложение?[¶](#how-to-add-new-translated-text-in-the-application "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------------------- + + + +Переводы отображаются с помощью функций в исходном коде: + + + +- `t()`: показывает текст с HTML escaping + + + +- `e()`: показывает текст без HTML escaping + + + +Всегда используйте английскую версию исходного кода. + + + +Текстовые строки используют функцию `sprintf()` для замены элементов: + + + +- `%s` используется для замены строки + + + +- `%d` используется для замены цифры + + + +Ознакомится с доступными форматами вы можете в [документации PHP](http://php.net/sprintf). + + + +Как найти отсутствующие переводы в приложении?[¶](#how-to-find-missing-translations-in-the-applications "Ссылка на этот заголовок") +----------------------------------------------------------------------------------------------------------------------------------- + + + +Из терминала запустите следующую команду: + + + + ./kanboard locale:compare + + + +Все отсутствующие и неиспользуемые переводы будут показаны на экране. Добавьте их во французскую локализацию и синхронизируйте с другими локализациями (смотрите ниже) + + + +Как синхронизировать файлы переводов?[¶](#how-to-synchronize-translation-files "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + + + +В оболочке Unix запустите следующую команду: + + + + ./kanboard locale:sync + + + +Французский перевод используется для ссылки на другие локализации. + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/ubuntu-installation.markdown b/doc/ru_RU/ubuntu-installation.markdown new file mode 100644 index 00000000..ac3cb565 --- /dev/null +++ b/doc/ru_RU/ubuntu-installation.markdown @@ -0,0 +1,111 @@ +Как инсталировать Канборд на Ubuntu? +==================================== + + + +Ubuntu Xenial 16.04 LTS[¶](#ubuntu-xenial-16-04-lts "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y apache2 libapache2-mod-php7.0 php7.0-cli php7.0-mbstring php7.0-sqlite3 \ + + php7.0-opcache php7.0-json php7.0-mysql php7.0-pgsql php7.0-ldap php7.0-gd + + + +Установите Канборд: + + + + cd /var/www/html + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Ubuntu Trusty 14.04 LTS[¶](#ubuntu-trusty-14-04-lts "Ссылка на этот заголовок") +------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y php5 php5-sqlite php5-mysql php5-pgsql php5-ldap php5-gd php5-json php5-mcrypt unzip + + + +Установите Канборд: + + + + cd /var/www/html + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Ubuntu Precise 12.04 LTS[¶](#ubuntu-precise-12-04-lts "Ссылка на этот заголовок") +--------------------------------------------------------------------------------- + + + +Установите Apache и PHP: + + + + sudo apt-get update + + sudo apt-get install -y php5 php5-sqlite php5-mysql php5-pgsql php5-ldap php5-gd php5-json php5-mcrypt unzip + + + +Установите Канборд: + + + + cd /var/www + + sudo wget https://kanboard.net/kanboard-latest.zip + + sudo unzip kanboard-latest.zip + + sudo chown -R www-data:www-data kanboard/data + + sudo rm kanboard-latest.zip + + + +Некоторые возможности Канборда требуют [запуска ежедневных фоновых задач](cronjob.markdown). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/update.markdown b/doc/ru_RU/update.markdown new file mode 100644 index 00000000..7cfabdb0 --- /dev/null +++ b/doc/ru_RU/update.markdown @@ -0,0 +1,57 @@ +Обновление Канборд до новой версии +================================== + + +Обновление Канборда до новой версии бесшовное. Процесс сводится к тому, что надо просто скопировать каталог с данными из старой версии в новый Канборд. Канборд запустит миграцию баз данных автоматически. + + + +Важные замечания перед обновлением[¶](#important-things-to-do-before-updating "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------- + +- Перед обновлением, обязательно сделайте копию ваших данных со старой версии Канборда + +- Всегда следите за [историей изменений](https://github.com/fguillot/kanboard/blob/master/ChangeLog) для отслеживания критических изменений + +- Всегда закрывайте все пользовательские сессии (очищайте все сессии на сервере) + + +Обновление из архива (стабильная версия)[¶](#from-the-archive-stable-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------- + + + +1. Скачайте и распакуйте архив с новой версией + +2. Скопируйте содержимое каталога с данными старой версии во вновь распакованный каталог + +3. Скопируйте из старой версии Канборда `config.php`, если вы его создавали + +4. Скопируйте плагины, если есть + +5. Убедитесь, что директория `data` имеет права на запись от пользователя веб сервера + +6. Проверьте работу новой версии + +7. Удалите старую версию Канборда + + +Обновление из репозитория (разрабатываемая версия)[¶](#from-the-repository-development-version "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------------- + + + +1. `git pull` + +2. `composer install --no-dev` + +3. Выполните вход и проверьте, что все работает корректно + + +**Внимание**: Выполняя обновление из разрабатываемой версии, вы должны понимать, что это нестабильная версия и берете все риски по работе Канборд на себя. + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/usage-examples.markdown b/doc/ru_RU/usage-examples.markdown new file mode 100644 index 00000000..d0d580e8 --- /dev/null +++ b/doc/ru_RU/usage-examples.markdown @@ -0,0 +1,193 @@ +Примеры использования +===================== + + + +Вы можете настроить вашу доску в соответсвии с вашими бизнес-процессами + + + +Разработка программного обеспечения[¶](#software-development "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + +- Заказ + + + +- Готов + + + +- В работе + + + +- Требуется утверждение + + + +- Утверждено + + + +- Развернуто в продакшн + + + +Отслеживание ошибок[¶](#bug-tracking "Ссылка на этот заголовок") +---------------------------------------------------------------- + + + +- Сообщение + + + +- Подтверждено + + + +- В работе + + + +- Проверено + + + +- Исправлено + + + +Продажи[¶](#sales "Ссылка на этот заголовок") +--------------------------------------------- + + + +- Клиенты + + + +- Встречи + + + +- Предложения + + + +- Приобретение + + + +Эффективное управление бизнесом[¶](#lean-business-management "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------- + + + +- Идеи + + + +- События + + + +- Мероприятия + + + +- Анализы + + + +- Исполненно + + + +Подбор персонала[¶](#recruiting-process "Ссылка на этот заголовок") +------------------------------------------------------------------- + + + +- Предложения о работе + + + +- Кандидаты + + + +- Телефонный отбор + + + +- Собеседование + + + +- Наем + + + +Онлайн магазин[¶](#online-shops "Ссылка на этот заголовок") +----------------------------------------------------------- + + + +- Заказы + + + +- Упаковка + + + +- Готов к отправке + + + +- Отправлен + + + +Производство[¶](#manufactory "Ссылка на этот заголовок") +-------------------------------------------------------- + + + +- Заказы покупателей + + + +- Сборка + + + +- Проверка + + + +- Упаковка + + + +- Готово к отгрузке + + + +- Отправлен + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-management.markdown b/doc/ru_RU/user-management.markdown new file mode 100644 index 00000000..ce74b7f8 --- /dev/null +++ b/doc/ru_RU/user-management.markdown @@ -0,0 +1,89 @@ +Управление пользователями +========================= + + + +Создание нового пользователя[¶](#add-a-new-user "Ссылка на этот заголовок") +--------------------------------------------------------------------------- + + + +Только администратор может создавать нового пользователя. + + + +1. В выпадающем меню, в правом верхнем углу, выберите **Управление пользователями** + + + +2. Вверху имеются ссылки - **Новый локальный пользователь** и **Новый удаленный пользователь** + + + +3. При создании пользователя нужно заполнить форму и сохранить + + + +![New user](screenshots/new-user.png) + +Рисунок. Форма создания нового пользователя. + + + +При создании **Локального пользователя** вы должны, как минимум, заполнить следующие поля: + + + +- **Имя пользователя**: это поле является уникальным идентификатором вашего пользователя (логин) + + + +- **Пароль**: Пароль пользователя должен иметь минимум 6 символов + + + +Для **удаленных пользователей** обязательно только **Имя пользователя**. + + + +Редактирование пользователей[¶](#edit-users "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +После перехода в **Управление пользователями**, вам будет доступен список пользователей. Кликните на пользователя в столбце **Имя пользователя**. Далее, вам будет доступно редактирование настроек и профиля пользователя. + + + +- Если вы имеете права пользователя, то вы сможете только изменить ваш профиль + + + +- Для редактирования любого пользователя вам должны быть назначены права администратора + + + +Удаеление пользователей[¶](#remove-users "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +В списке пользователей выберите в колонке **Действия** в выпадающем меню **Удалить**. Эта ссылка доступна только для администраторов. + + + +Если вы удалите пользователя, то все задачи назначенные пользователю перестанут быть назначенными. + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-mentions.markdown b/doc/ru_RU/user-mentions.markdown new file mode 100644 index 00000000..766103e3 --- /dev/null +++ b/doc/ru_RU/user-mentions.markdown @@ -0,0 +1,49 @@ +Ссылка на пользователя +====================== + + + +В Канборде есть возможность посылать уведомления пользователю, если кто-то ссылается на него в тексте. + + + +Если вы хотите заострить внимание о ком-либо в комментарии или в задаче, то вы можете использовать символ @ и следом указать имя пользователя. Канборд автоматически предлагает список пользователей: + + + +![User Mention](screenshots/mention-autocomplete.png) + +Рисунок. Ссылка на пользователя. + + + +- В данный момент, добавлять ссылку на пользователя можно только в описании задачи и тексте комментария. + + + +- Ссылка на пользователя работает только в задачах и при создании комментария. + + + +- Для получения уведомления, пользователь, на которого ссылаются, должен быть участником проекта, в котором создается ссылка. + + + +- Если была создана ссылка на пользователя, то этот пользователь получит уведомление. + + + +- @username - выглядит как ссылка на публичный профиль пользователя. + + + +Уведомление посылаются пользователю в соответсвии с пользовательскими настройками: это может быть email, уведомление на веб странице или даже сообщение в Slack/Hipchat/Jabber, если вы установили соответсвующие плагины. + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/user-types.markdown b/doc/ru_RU/user-types.markdown new file mode 100644 index 00000000..9afb58b8 --- /dev/null +++ b/doc/ru_RU/user-types.markdown @@ -0,0 +1,26 @@ +Типы пользователей +================== + + + +В Канборде могут быть два типа пользователей: + + + +| Тип | Описание | +|--------------|-------------------------------------------------------------| +| Локальный пользователь | Пароль пользователя хранится в базе данных Канборда| +| Удаленный пользователь | Учетные данные пользователя управляются (контролируются) другой системой (например, LDAP сервер). Другими словами, аутентификация пользователя происходит во внешней системе, не в Канборде.| + + + +Примеры удаленных пользователей: + +- LDAP пользователь + +- Аутентификация пользователя через реверс-прокси + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/vagrant.markdown b/doc/ru_RU/vagrant.markdown new file mode 100644 index 00000000..59e920cc --- /dev/null +++ b/doc/ru_RU/vagrant.markdown @@ -0,0 +1,51 @@ +Запуск Канборда с Vagrant +========================= + + + +Вы можете легко развернуть Канборд с Vagrant: + + + +- Склонируйте проект с репозитория git + + + +- Выполните `vagrant up` + + + +- Для входа в приложение используйте URL `http://localhost:8001/` + + + +Виртуальная машина построена на Ubuntu 14.04 с PHP 5.5. + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/webhooks.markdown b/doc/ru_RU/webhooks.markdown new file mode 100644 index 00000000..c598abf9 --- /dev/null +++ b/doc/ru_RU/webhooks.markdown @@ -0,0 +1,536 @@ +Web Hooks +========= + + + +Webhooks служат для взаимодействия с внешними приложениями. Webhook посылает уведомление стороннему приложению о событиях, которые произошли в Канборд. + + + +- Webhooks могут быть использованы для создания задач вызовом простого URL (Вы можете сделать это и при помощи API) + + + +- Обращение к внешнему приложению может происходить автоматически, когда наступает какое-либо событие в Канборд (создана задача, обновлен комментарий и т.д.) + + + +Как написать webhook приемник во внешнем приложении?[¶](#how-to-write-a-web-hook-receiver "Ссылка на этот заголовок") +--------------------------------------------------------------------------------------------------------------------- + + + +Все внутренние события в Канборде могут быть посланы во внешний URL. + + + +- Webhook URL (url приемник внешнего приложения) может быть задан в **Настройки** -\> **Webhooks** -\> **Webhook URL** + + + +- Когда в Канборде происходит событие, Канборд обращается к указанному URL автоматически + + + +- Данные конвертируются в формат JSON и передаются с помощью POST HTTP запроса + + + +- Webhook ключ передается в составе запроса в виде строкового параметра. Таким образом, вы можете проверить, что запрос на самом деле пришел из Канборда. + + + +- **Созданный вами URL должен среагировать в течении 1 секунды**. Это желательно сделать потому, что запросы являются синхронными (ограничения языка PHP) и возможны тормоза в пользовательском интерфейсе, если скрипт будет слишком медленный! + + + +### Список поддерживаемых событий[¶](#list-of-supported-events "Ссылка на этот заголовок") + + + +- comment.create (комментарий.создать) + + + +- comment.update (комментарий.обновить) + + + +- file.create (файл.создать) + + + +- task.move.project (задача.переместить.проект) + + + +- task.move.column (задача.переместить.колонка) + + + +- task.move.position (задача.переместить.место) + + + +- task.move.swimlane (задача.переместить.дорожка) + + + +- task.update (задача.обновить) + + + +- task.create (задача.создать) + + + +- task.close (задача.закрыть) + + + +- task.open (задача.открыть) + + + +- task.assignee\_change (задача.назначить\_изменить) + + + +- subtask.update (подзадача.обновить) + + + +- subtask.create (подзадача.создать) + + + +### Пример HTTP запроса[¶](#example-of-http-request "Ссылка на этот заголовок") + + + + POST https://your_webhook_url/?token=WEBHOOK_TOKEN_HERE + + User-Agent: Kanboard Webhook + + Content-Type: application/json + + Connection: close + + + + { + + "event_name": "task.move.column", + + "event_data": { + + "task_id": "1", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "2", + + "dst_column_id": "1", + + "date_moved": "1431991532", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Функциональная часть всех событий имеет следующий формат: + + + + { + + "event_name": "model.event_name", + + "event_data": { + + "key1": "value1", + + "key2": "value2", + + ... + + } + + } + + + +Значения `event_data`{.docutils .literal} могут быть неупорядочены в событиях. + + + +### Пример функциональной части события[¶](#examples-of-event-payloads "Ссылка на этот заголовок") + + + +Создание задачи: + + + + { + + "event_name": "task.create", + + "event_data": { + + "title": "Demo", + + "description": "", + + "project_id": "1", + + "owner_id": "1", + + "category_id": 0, + + "swimlane_id": 0, + + "column_id": "2", + + "color_id": "yellow", + + "score": 0, + + "time_estimated": 0, + + "date_due": 0, + + "creator_id": 1, + + "date_creation": 1431991532, + + "date_modification": 1431991532, + + "date_moved": 1431991532, + + "position": 1, + + "task_id": 1 + + } + + } + + + +Изменение задачи: + + + + { + + "event_name": "task.update", + + "event_data": { + + "id": "1", + + "title": "Demo", + + "description": "", + + "date_creation": "1431991532", + + "color_id": "yellow", + + "project_id": "1", + + "column_id": "1", + + "owner_id": "1", + + "position": "1", + + "is_active": "1", + + "date_completed": null, + + "score": "0", + + "date_due": "0", + + "category_id": "2", + + "creator_id": "1", + + "date_modification": 1431991603, + + "reference": "", + + "date_started": 1431993600, + + "time_spent": 0, + + "time_estimated": 0, + + "swimlane_id": "0", + + "date_moved": "1431991572", + + "recurrence_status": "0", + + "recurrence_trigger": "0", + + "recurrence_factor": "0", + + "recurrence_timeframe": "0", + + "recurrence_basedate": "0", + + "recurrence_parent": null, + + "recurrence_child": null, + + "task_id": "1", + + "changes": { + + "category_id": "2" + + } + + } + + } + + + +События изменеия задачи имеют поле `changes`{.docutils .literal}, которое содержит обновленные значения. + + + +Перемещение задачи в другую колонку: + + + + { + + "event_name": "task.move.column", + + "event_data": { + + "task_id": "1", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "2", + + "dst_column_id": "1", + + "date_moved": "1431991532", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Перемещение задачи в другое место: + + + + { + + "event_name": "task.move.position", + + "event_data": { + + "task_id": "2", + + "project_id": "1", + + "position": 1, + + "column_id": "1", + + "swimlane_id": "0", + + "src_column_id": "1", + + "dst_column_id": "1", + + "date_moved": "1431996905", + + "recurrence_status": "0", + + "recurrence_trigger": "0" + + } + + } + + + +Создание комментария: + + + + { + + "event_name": "comment.create", + + "event_data": { + + "id": 1, + + "task_id": "1", + + "user_id": "1", + + "comment": "test", + + "date_creation": 1431991615 + + } + + } + + + +Изменение комментария: + + + + { + + "event_name": "comment.update", + + "event_data": { + + "id": "1", + + "task_id": "1", + + "user_id": "1", + + "comment": "test edit" + + } + + } + + + +Создание подзадачи: + + + + { + + "event_name": "subtask.create", + + "event_data": { + + "id": 3, + + "task_id": "1", + + "title": "Test", + + "user_id": "1", + + "time_estimated": "2", + + "position": 3 + + } + + } + + + +Изменение подзадачи: + + + + { + + "event_name": "subtask.update", + + "event_data": { + + "id": "1", + + "status": 1, + + "task_id": "1" + + } + + } + + + +Загрузка файла: + + + + { + + "event_name": "file.create", + + "event_data": { + + "task_id": "1", + + "name": "test.png" + + } + + } + + + +Создан снимок экрана: + + + + { + + "event_name": "file.create", + + "event_data": { + + "task_id": "2", + + "name": "Screenshot taken May 19, 2015 at 10:56 AM" + + } + + } + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/what-is-kanban.markdown b/doc/ru_RU/what-is-kanban.markdown new file mode 100644 index 00000000..46196bbb --- /dev/null +++ b/doc/ru_RU/what-is-kanban.markdown @@ -0,0 +1,80 @@ +Что такое Kanban? +================= + + + +Kanban - методология, которая первоначально применила компания Toyota для увеличения производительности. Описание в википедии - [Канбан доска](https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%BD%D0%B1%D0%B0%D0%BD-%D0%B4%D0%BE%D1%81%D0%BA%D0%B0) + + + +Смысл Kanban заключается в следующем: + + + +- Визуализация рабочих процессов + + + +- Уменьшение времени для достижения цели + + + +Визуализация рабочих процессов[¶](#visualize-your-workflow "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------- + + + +- Ваш рабочий процесс отображается на доске и вы ясно видете картину вашего проекта + + + +- Каждая колонка представляет шаг вашего рабочего процесса + + + +Сосредоточте внимание и избегайте многозадачности[¶](#bring-focus-and-avoid-multitasking "Ссылка на этот заголовок") +-------------------------------------------------------------------------------------------------------------------- + + + +- Каждая фаза может иметь работу ограниченную временем + + + +- Уменьшайте объем для определения узких мест + + + +- Ограничте количество одновременно выполняемых задач + + + +Подсчет производительности и улучшений[¶](#measure-performance-and-improvement "Ссылка на этот заголовок") +---------------------------------------------------------------------------------------------------------- + + + +Kanban использует время выполнения (lead time) и время цикла (cycle time) для подсчета производительности: + + + +- **Время выполнения**: Время между созданием задачи и ее завершением + + + +- **Время цикла**: Время между началом выполнения задачи и ее завершением + + + +Например, вам заложено время выполнения - 100 дней, а затратили на выполнение задачи (время цикла) всего 1 час. + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/windows-apache-installation.markdown b/doc/ru_RU/windows-apache-installation.markdown new file mode 100644 index 00000000..7f181e10 --- /dev/null +++ b/doc/ru_RU/windows-apache-installation.markdown @@ -0,0 +1,253 @@ +Установка Канборд на Windows Server и Apache +============================================ + + + +Это руководство поможет вам шаг за шагом установить Канборд на Windows Server с Apache и PHP + + + +**Внимание**: Если у вас 64 разрядная платформа, то вам нужно выбрать “x64”, и выберите “x86” для 32 разрядной операционной системы. + + + +Установка распространяемого пакета Visual C++[¶](#visual-c-redistributable-installation "Ссылка на этот заголовок") +------------------------------------------------------------------------------------------------------------------- + + + +PHP и Apache скомпилированы с Visual Studio, поэтому вам нужно установить эту библиотеку, если вы не сделали это ранее. + + + +1. Скачайте библиотеку с [официального вебсайта Microsoft](http://www.microsoft.com/en-us/download/details.aspx?id=30679) + + + +2. Запустите установку `vcredist_x64.exe` или `vcredist_x86.exe`, в соответствии с вашей платформой + + + +Установка Apache[¶](#apache-installation "Ссылка на этот заголовок") +-------------------------------------------------------------------- + + + +1. Скачайте исходники Apache с [Apache Lounge](http://www.apachelounge.com/download/) + + + +2. Разархивируйте Apache24 в каталог `C:\Apache24` + + + +### Назначение имени сервера[¶](#define-the-server-name "Ссылка на этот заголовок") + + + +Откройте файл `C:\Apache24\conf\httpd.conf` и добавьте директиву: + + + + ServerName localhost + + + +### Установка сервиса Apache[¶](#install-the-apache-service "Ссылка на этот заголовок") + + + +Откройте консоль (`cmd.exe`), перейдите в каталог `C:\Apache24\bin` и установите сервис Apache: + + + + cd C:\Apache24\bin + + + + # Install the windows service + + httpd.exe -k install + + + +### Установка ApacheMonitor[¶](#install-apachemonitor "Ссылка на этот заголовок") + + + +- Выполните `C:\Apache24\bin\ApacheMonitor.exe` и добавьте его в автозагрузку. + + + +- Теперь во всплывающем меню, при нажатии правой кнопки мыши на иконке, нажмите запустить Apache + + + +### Проверка работы Apache[¶](#check-the-apache-installation "Ссылка на этот заголовок") + + + +В браузере откройте . Вы должны увидеть пустую страницу и текст “It works!”. + + + +Установка PHP[¶](#php-installation "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +1. Скачайте последнюю стабильную версию PHP с [официального сайта PHP](http://windows.php.net/download/), выберите версию **Thread Safe** и используйте соответствующую разрядность: x86 or x64. + + + +2. Разархивируйте файлы в `C:\php` + + + +3. Перейдите в каталог PHP (`C:\php`) и переименуйе файл `php.ini-production` в `php.ini` + + + +Отредактируйте `php.ini`: + + + +Раскоментируйте директорию расширений: + + + + extension_dir = "C:/php/ext" + + + +Раскоментируйте следующие модули PHP: + + + + extension=php_gd2.dll + + extension=php_ldap.dll + + extension=php_mbstring.dll + + extension=php_openssl.dll + + extension=php_pdo_sqlite.dll + + + +Установите часовой пояс: + + + + date.timezone = America/Montreal + + + +Список всех поддерживаемых часовых поясов можно посмотреть в [документации PHP](http://php.net/manual/en/timezones.america.php). + + + +Загрузка модулей PHP для Apache: + + + +Добавьте следующие строки конфигурации в файл `C:\Apache24\conf\httpd.conf`: + + + + LoadModule php5_module "c:/php/php5apache2_4.dll" + + AddHandler application/x-httpd-php .php + + + + # configure the path to php.ini + + PHPIniDir "C:/php" + + + + # change this directive + + DirectoryIndex index.php index.html + + + +Перезапустите Apache. + + + +Проверка работы PHP: + + + +Создайте файл `phpinfo.php` в каталоге `C:\Apache24\htdocs`: + + + + + + + +Откройте в браузере [http://localhost/phpinfo.php](http://localhost/phpinfo.php) и вы должны увидеть информацию о PHP. + + + +Устновка Канборд[¶](#kanboard-installation "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +- [Скачайте zip файл](https://kanboard.net/downloads) + + + +- Разархивируйте архив в `C:\Apache24\htdocs\kanboard` + + + +- Откройте в браузере . Ура. Теперь вы можете работать в Канборд. Все легко и просто. + + + +- Учетная запись и пароль по умолчанию - **admin/admin** + + + +Протестировано на[¶](#tested-configuration "Ссылка на этот заголовок") +---------------------------------------------------------------------- + + + +- Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8 + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Некоторые функции Канборда требуют выполнять [запуск ежедневных фоновых задач](cronjob.markdown). + + + + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/ru_RU/windows-iis-installation.markdown b/doc/ru_RU/windows-iis-installation.markdown new file mode 100644 index 00000000..0aabca6a --- /dev/null +++ b/doc/ru_RU/windows-iis-installation.markdown @@ -0,0 +1,150 @@ +Инсталяция Kanboard на Windows 2008/2012 с IIS +============================================== + + + +Это пошаговое руководство поможет вам установить Канборд на Windows Server с IIS и PHP. + + + +Установка PHP[¶](#php-installation "Ссылка на этот заголовок") +-------------------------------------------------------------- + + + +- Установите IIS на ваш Windows сервер (Добавьте новую роль и не забудьте включить CGI/FastCGI) + + + +- При инсталяции PHP можете использовать следующую официальную документацию: + + + + - [Microsoft IIS 5.1 and IIS 6.0](http://php.net/manual/en/install.windows.iis6.php) + + - [Microsoft IIS 7.0 and later](http://php.net/manual/en/install.windows.iis7.php) + + - [PHP for Windows is available here](http://windows.php.net/download/) + + + +Отредактируйте `php.ini`, раскоментируйте эти PHP модули: + + + + extension=php_gd2.dll + + extension=php_ldap.dll + + extension=php_mbstring.dll + + extension=php_openssl.dll + + extension=php_pdo_sqlite.dll + + + +Установите часовой пояс + + + + date.timezone = America/Montreal + + + +Список поддерживаемых часовых поясов можно посмотреть в [документации PHP](http://php.net/manual/en/timezones.america.php). + + + +Проверьте, что PHP работает корректно: + + + +Перейдите в корневой каталог IIS `C:\inetpub\wwwroot` и создайте файл `phpinfo.php`, со следующим содержимым: + + + + + + + +В браузере откройте страницу `http://localhost/phpinfo.php` и вы должны увидеть текущие настройки PHP. Если вы видите ошибку 500, значит что-то сделано неправильно при установке. + + + +Примечание: + + + +- Если вы используете PHP \< 5.4, то необходимо включить короткие теги (short tags) в php.ini + + + +- Не забудьте включить необходимые php расширения, упомянутые выше + + + +- Если вы наблюдаете ошибку “the library MSVCP110.dll is missing”, то возможно вам нужно скачать распространяемый пакет Visual C++ для Visual Studio с сайта Microsoft. + + + +Установка Канборд[¶](#kanboard-installation "Ссылка на этот заголовок") +----------------------------------------------------------------------- + + + +- Скачайте zip файл + + + +- Распакуйте архив в `C:\inetpub\wwwroot\kanboard` (например) + + + +- Убедитесь, что у пользователя вебсервера IIS имеется доступ на запись на директорию `data` + + + +- Откройте веб браузер и используйте Kanboard + + + +- Пользователь и пароль по умолчанию - **admin/admin** + + + +Работа Канборд тестировалось на[¶](#tested-configurations "Ссылка на этот заголовок") +------------------------------------------------------------------------------------- + + + +- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16 + +- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29 + + + +Примечание[¶](#notes "Ссылка на этот заголовок") +------------------------------------------------ + + + +- Некоторые возможности Канборда требуют [запуск выполнения ежедневных фоновых задач](cronjob.markdown). + + + + + + + + +[Русская документация Kanboard](http://kanboard.ru/doc/) + diff --git a/doc/web.config b/doc/web.config new file mode 100644 index 00000000..1461fe2d --- /dev/null +++ b/doc/web.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From 5fe81ae6ef59ee73e7d6f34fb333d3d19a08a266 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 22 Jul 2016 17:58:39 -0400 Subject: Add new template hooks --- app/Template/board/table_column.php | 1 + app/Template/header.php | 1 + doc/plugin-hooks.markdown | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) (limited to 'doc') diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php index 6336234a..75a6eb4c 100644 --- a/app/Template/board/table_column.php +++ b/app/Template/board/table_column.php @@ -47,6 +47,7 @@
  • + hook->render('template:board:column:dropdown', array('swimlane' => $swimlane, 'column' => $column)) ?> diff --git a/app/Template/header.php b/app/Template/header.php index 13521ae7..f99f1031 100644 --- a/app/Template/header.php +++ b/app/Template/header.php @@ -59,6 +59,7 @@ url->link(t('New private project'), 'ProjectCreationController', 'createPrivate', array(), false, 'popover') ?> + hook->render('template:header:creation-dropdown') ?> diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 1f90bdbc..787c62df 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -155,6 +155,7 @@ List of template hooks: | `template:board:public:task:after-title` | Task in public board: after title | | `template:board:task:footer` | Task in board: footer | | `template:board:task:icons` | Task in board: tooltip icon | +| `template:board:column:dropdown` | Dropdown menu in board columns | | `template:config:sidebar` | Sidebar on settings page | | `template:config:application ` | Application settings form | | `template:config:email` | Email settings page | @@ -162,7 +163,8 @@ List of template hooks: | `template:dashboard:sidebar` | Sidebar on dashboard page | | `template:export:sidebar` | Sidebar on export pages | | `template:import:sidebar` | Sidebar on import pages | -| `template:header:dropdown` | Dropdown on header | +| `template:header:dropdown` | Page header dropdown menu (user avatar icon) | +| `template:header:creation-dropdown` | Page header dropdown menu (plus icon) | | `template:layout:head` | Page layout `` tag | | `template:layout:top` | Page layout top header | | `template:layout:bottom` | Page layout footer | -- cgit v1.2.3 From 8e6e335c9d99ff710ecd70dff293f15a25bf9a98 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 19:21:32 -0400 Subject: Update webhooks documentation --- doc/ru_RU/webhooks.markdown | 99 ++------ doc/webhooks.markdown | 546 +++++++++++++++++++++++++++++++------------- 2 files changed, 401 insertions(+), 244 deletions(-) (limited to 'doc') diff --git a/doc/ru_RU/webhooks.markdown b/doc/ru_RU/webhooks.markdown index c598abf9..dbba0867 100644 --- a/doc/ru_RU/webhooks.markdown +++ b/doc/ru_RU/webhooks.markdown @@ -1,16 +1,10 @@ -Web Hooks -========= - - +Webhooks +======== Webhooks служат для взаимодействия с внешними приложениями. Webhook посылает уведомление стороннему приложению о событиях, которые произошли в Канборд. - - Webhooks могут быть использованы для создания задач вызовом простого URL (Вы можете сделать это и при помощи API) - - - - Обращение к внешнему приложению может происходить автоматически, когда наступает какое-либо событие в Канборд (создана задача, обновлен комментарий и т.д.) @@ -18,89 +12,36 @@ Webhooks служат для взаимодействия с внешними п Как написать webhook приемник во внешнем приложении?[¶](#how-to-write-a-web-hook-receiver "Ссылка на этот заголовок") --------------------------------------------------------------------------------------------------------------------- - - Все внутренние события в Канборде могут быть посланы во внешний URL. - - - Webhook URL (url приемник внешнего приложения) может быть задан в **Настройки** -\> **Webhooks** -\> **Webhook URL** - - - - Когда в Канборде происходит событие, Канборд обращается к указанному URL автоматически - - - - Данные конвертируются в формат JSON и передаются с помощью POST HTTP запроса - - - - Webhook ключ передается в составе запроса в виде строкового параметра. Таким образом, вы можете проверить, что запрос на самом деле пришел из Канборда. - - - - **Созданный вами URL должен среагировать в течении 1 секунды**. Это желательно сделать потому, что запросы являются синхронными (ограничения языка PHP) и возможны тормоза в пользовательском интерфейсе, если скрипт будет слишком медленный! ### Список поддерживаемых событий[¶](#list-of-supported-events "Ссылка на этот заголовок") - - -- comment.create (комментарий.создать) - - - -- comment.update (комментарий.обновить) - - - -- file.create (файл.создать) - - - -- task.move.project (задача.переместить.проект) - - - -- task.move.column (задача.переместить.колонка) - - - -- task.move.position (задача.переместить.место) - - - -- task.move.swimlane (задача.переместить.дорожка) - - - -- task.update (задача.обновить) - - - -- task.create (задача.создать) - - - -- task.close (задача.закрыть) - - - -- task.open (задача.открыть) - - - -- task.assignee\_change (задача.назначить\_изменить) - - - -- subtask.update (подзадача.обновить) - - - -- subtask.create (подзадача.создать) +- comment.create +- comment.update +- comment.delete +- file.create +- task.move.project +- task.move.column +- task.move.position +- task.move.swimlane +- task.update +- task.create +- task.close +- task.open +- task.assignee_change +- subtask.update +- subtask.create +- subtask.delete +- task_internal_link.create_update +- task_internal_link.delete diff --git a/doc/webhooks.markdown b/doc/webhooks.markdown index 628c7e38..e43ab9ce 100644 --- a/doc/webhooks.markdown +++ b/doc/webhooks.markdown @@ -1,5 +1,5 @@ -Web Hooks -========= +Webhooks +======== Webhooks are useful to perform actions with external applications. @@ -21,6 +21,7 @@ All internal events of Kanboard can be sent to an external URL. - comment.create - comment.update +- comment.delete - file.create - task.move.project - task.move.column @@ -33,6 +34,9 @@ All internal events of Kanboard can be sent to an external URL. - task.assignee_change - subtask.update - subtask.create +- subtask.delete +- task_internal_link.create_update +- task_internal_link.delete ### Example of HTTP request @@ -43,19 +47,65 @@ Content-Type: application/json Connection: close { - "event_name": "task.move.column", - "event_data": { - "task_id": "1", - "project_id": "1", - "position": 1, - "column_id": "1", - "swimlane_id": "0", - "src_column_id": "2", - "dst_column_id": "1", - "date_moved": "1431991532", - "recurrence_status": "0", - "recurrence_trigger": "0" - } + "event_name": "task.move.column", + "event_data": { + "task_id": "4", + "task": { + "id": "4", + "reference": "", + "title": "My task", + "description": "", + "date_creation": "1469314356", + "date_completed": null, + "date_modification": "1469315422", + "date_due": "1469491200", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "green", + "project_id": "1", + "column_id": "1", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "0", + "category_id": "0", + "priority": "0", + "swimlane_id": "0", + "date_moved": "1469315422", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Backlog", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + }, + "changes": { + "src_column_id": "2", + "dst_column_id": "1", + "date_moved": "1469315398" + }, + "project_id": "1", + "position": 1, + "column_id": "1", + "swimlane_id": "0", + "src_column_id": "2", + "dst_column_id": "1", + "date_moved": "1469315398", + "recurrence_status": "0", + "recurrence_trigger": "0" + } } ``` @@ -80,26 +130,51 @@ Task creation: ```json { - "event_name": "task.create", - "event_data": { - "title": "Demo", - "description": "", - "project_id": "1", - "owner_id": "1", - "category_id": 0, - "swimlane_id": 0, - "column_id": "2", - "color_id": "yellow", - "score": 0, - "time_estimated": 0, - "date_due": 0, - "creator_id": 1, - "date_creation": 1431991532, - "date_modification": 1431991532, - "date_moved": 1431991532, - "position": 1, - "task_id": 1 - } + "event_name": "task.create", + "event_data": { + "task_id": 5, + "task": { + "id": "5", + "reference": "", + "title": "My new task", + "description": "", + "date_creation": "1469315481", + "date_completed": null, + "date_modification": "1469315481", + "date_due": "0", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "orange", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "3", + "category_id": "0", + "priority": "2", + "swimlane_id": "0", + "date_moved": "1469315481", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Ready", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + } + } } ``` @@ -107,113 +182,121 @@ Task modification: ```json { - "event_name": "task.update", - "event_data": { - "id": "1", - "title": "Demo", - "description": "", - "date_creation": "1431991532", - "color_id": "yellow", - "project_id": "1", - "column_id": "1", - "owner_id": "1", - "position": "1", - "is_active": "1", - "date_completed": null, - "score": "0", - "date_due": "0", - "category_id": "2", - "creator_id": "1", - "date_modification": 1431991603, - "reference": "", - "date_started": 1431993600, - "time_spent": 0, - "time_estimated": 0, - "swimlane_id": "0", - "date_moved": "1431991572", - "recurrence_status": "0", - "recurrence_trigger": "0", - "recurrence_factor": "0", - "recurrence_timeframe": "0", - "recurrence_basedate": "0", - "recurrence_parent": null, - "recurrence_child": null, - "task_id": "1", - "changes": { - "category_id": "2" + "event_name": "task.update", + "event_data": { + "task_id": "5", + "task": { + "id": "5", + "reference": "", + "title": "My new task", + "description": "New description", + "date_creation": "1469315481", + "date_completed": null, + "date_modification": "1469315531", + "date_due": "1469836800", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "purple", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "3", + "category_id": "0", + "priority": "2", + "swimlane_id": "0", + "date_moved": "1469315481", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Ready", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + }, + "changes": { + "description": "New description", + "color_id": "purple", + "date_due": 1469836800 + } } - } } ``` Task update events have a field called `changes` that contains updated values. -Move a task to another column: - -```json -{ - "event_name": "task.move.column", - "event_data": { - "task_id": "1", - "project_id": "1", - "position": 1, - "column_id": "1", - "swimlane_id": "0", - "src_column_id": "2", - "dst_column_id": "1", - "date_moved": "1431991532", - "recurrence_status": "0", - "recurrence_trigger": "0" - } -} -``` - -Move a task to another position: - -```json -{ - "event_name": "task.move.position", - "event_data": { - "task_id": "2", - "project_id": "1", - "position": 1, - "column_id": "1", - "swimlane_id": "0", - "src_column_id": "1", - "dst_column_id": "1", - "date_moved": "1431996905", - "recurrence_status": "0", - "recurrence_trigger": "0" - } -} -``` - Comment creation: ```json { - "event_name": "comment.create", - "event_data": { - "id": 1, - "task_id": "1", - "user_id": "1", - "comment": "test", - "date_creation": 1431991615 - } -} -``` - -Comment modification: - -``` -{ - "event_name": "comment.update", - "event_data": { - "id": "1", - "task_id": "1", - "user_id": "1", - "comment": "test edit" - } + "event_name": "comment.create", + "event_data": { + "comment": { + "id": "1", + "task_id": "5", + "user_id": "1", + "date_creation": "1469315727", + "comment": "My comment.", + "reference": null, + "username": "admin", + "name": null, + "email": null, + "avatar_path": null + }, + "task": { + "id": "5", + "reference": "", + "title": "My new task", + "description": "New description", + "date_creation": "1469315481", + "date_completed": null, + "date_modification": "1469315531", + "date_due": "1469836800", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "purple", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "3", + "category_id": "0", + "priority": "2", + "swimlane_id": "0", + "date_moved": "1469315481", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Ready", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + } + } } ``` @@ -221,28 +304,65 @@ Subtask creation: ```json { - "event_name": "subtask.create", - "event_data": { - "id": 3, - "task_id": "1", - "title": "Test", - "user_id": "1", - "time_estimated": "2", - "position": 3 - } -} -``` - -Subtask modification: - -```json -{ - "event_name": "subtask.update", - "event_data": { - "id": "1", - "status": 1, - "task_id": "1" - } + "event_name": "subtask.create", + "event_data": { + "subtask": { + "id": "1", + "title": "My subtask", + "status": "0", + "time_estimated": "0", + "time_spent": "0", + "task_id": "5", + "user_id": "1", + "position": "1", + "username": "admin", + "name": null, + "timer_start_date": 0, + "status_name": "Todo", + "is_timer_started": false + }, + "task": { + "id": "5", + "reference": "", + "title": "My new task", + "description": "New description", + "date_creation": "1469315481", + "date_completed": null, + "date_modification": "1469315531", + "date_due": "1469836800", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "purple", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "3", + "category_id": "0", + "priority": "2", + "swimlane_id": "0", + "date_moved": "1469315481", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Ready", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + } + } } ``` @@ -250,22 +370,118 @@ File upload: ```json { - "event_name": "file.create", - "event_data": { - "task_id": "1", - "name": "test.png" - } + "event_name": "task.file.create", + "event_data": { + "file": { + "id": "1", + "name": "kanboard-latest.zip", + "path": "tasks/5/6f32893e467e76671965b1ec58c06a2440823752", + "is_image": "0", + "task_id": "5", + "date": "1469315613", + "user_id": "1", + "size": "4907308" + }, + "task": { + "id": "5", + "reference": "", + "title": "My new task", + "description": "New description", + "date_creation": "1469315481", + "date_completed": null, + "date_modification": "1469315531", + "date_due": "1469836800", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "purple", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "3", + "category_id": "0", + "priority": "2", + "swimlane_id": "0", + "date_moved": "1469315481", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Ready", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + } + } } ``` -Screenshot created: +Task link creation: ```json { - "event_name": "file.create", - "event_data": { - "task_id": "2", - "name": "Screenshot taken May 19, 2015 at 10:56 AM" - } + "event_name": "task_internal_link.create_update", + "event_data": { + "task_link": { + "id": "2", + "opposite_task_id": "5", + "task_id": "4", + "link_id": "3", + "label": "is blocked by", + "opposite_link_id": "2" + }, + "task": { + "id": "4", + "reference": "", + "title": "My task", + "description": "", + "date_creation": "1469314356", + "date_completed": null, + "date_modification": "1469315422", + "date_due": "1469491200", + "date_started": "0", + "time_estimated": "0", + "time_spent": "0", + "color_id": "green", + "project_id": "1", + "column_id": "1", + "owner_id": "1", + "creator_id": "1", + "position": "1", + "is_active": "1", + "score": "0", + "category_id": "0", + "priority": "0", + "swimlane_id": "0", + "date_moved": "1469315422", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "Demo Project", + "default_swimlane": "Default swimlane", + "column_title": "Backlog", + "assignee_username": "admin", + "assignee_name": null, + "creator_username": "admin", + "creator_name": null + } + } } ``` -- cgit v1.2.3 From ed4a71370625158760b420f1780ecae142cd502d Mon Sep 17 00:00:00 2001 From: Eskiso Date: Sat, 30 Jul 2016 20:50:18 +0100 Subject: Updated API Doc for Task Procedure Added new API calls for task metadata --- doc/api-task-procedures.markdown | 138 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) (limited to 'doc') diff --git a/doc/api-task-procedures.markdown b/doc/api-task-procedures.markdown index 934b1e09..2897c81a 100644 --- a/doc/api-task-procedures.markdown +++ b/doc/api-task-procedures.markdown @@ -695,3 +695,141 @@ Response example: ] } ``` + +## getTaskMetadata + +- Purpose: **Get all metadata related to a task by task unique id** +- Parameters: + - **task_id** (integer, required) +- Result on success: **list of metadata** +- Result on failure: **empty array** + +Request example to fetch all the metada of a task: + +```json +{ + "jsonrpc": "2.0", + "method": "getTaskMetadata", + "id": 133280317, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133280317, + "result": [ + { + "metaKey1": "metaValue1", + "metaKey2": "metaValue2", + ... + } + ] +} +``` + +## getTaskMetadataByName + +- Purpose: **Get metadata related to a task by task unique id and metakey (name)** +- Parameters: + - **task_id** (integer, required) + - **name** (string, required) +- Result on success: **metadata value** +- Result on failure: **empty string** + +Request example to fetch metada of a task by name: + +```json +{ + "jsonrpc": "2.0", + "method": "getTaskMetadataByName", + "id": 133280317, + "params": [ + 1, + "metaKey1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133280317, + "result": "metaValue1" +} +``` + +## saveTaskMetadata + +- Purpose: **Save/update task metadata** +- Parameters: + - **task_id** (integer, required) + - **array("name" => "value")** (array, required) +- Result on success: **true** +- Result on failure: **false** + +Request example to add/update metada of a task: + +```json +{ + "jsonrpc": "2.0", + "method": "saveTaskMetadata", + "id": 133280317, + "params": [ + 1, + { + "metaName" : "metaValue" + } + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133280317, + "result": true +} +``` + +## removeTaskMetadata + +- Purpose: **Remove task metadata by name** +- Parameters: + - **task_id** (integer, required) + - **name** (string, required) +- Result on success: **true** +- Result on failure: **false** + +Request example to remove metada of a task by name: + +```json +{ + "jsonrpc": "2.0", + "method": "removeTaskMetadata", + "id": 133280317, + "params": [ + 1, + "metaKey1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133280317, + "result": true +} +``` -- cgit v1.2.3 From 4ffaba2ba0dd6b5810adea1916080c3b645f3d29 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 13 Aug 2016 14:23:53 -0400 Subject: Add reference hooks --- ChangeLog | 1 + app/Core/Filter/LexerBuilder.php | 2 +- app/Core/Plugin/Hook.php | 17 ++++++++++ app/Formatter/BoardFormatter.php | 11 ++++--- app/Pagination/SubtaskPagination.php | 5 ++- app/Pagination/TaskPagination.php | 5 ++- doc/plugin-hooks.markdown | 25 +++++++++++++++ tests/units/Core/Plugin/HookTest.php | 62 ++++++++++++++++++++++-------------- 8 files changed, 97 insertions(+), 31 deletions(-) (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 68537a3a..25ce7eea 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ Version 1.0.33 (unreleased) Improvements: +* Add "reference" hooks * Show project name in task forms * Convert vanilla CSS to SASS diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php index 626d7614..e3ab725b 100644 --- a/app/Core/Filter/LexerBuilder.php +++ b/app/Core/Filter/LexerBuilder.php @@ -51,7 +51,7 @@ class LexerBuilder */ public function __construct() { - $this->lexer = new Lexer; + $this->lexer = new Lexer(); $this->queryBuilder = new QueryBuilder(); } diff --git a/app/Core/Plugin/Hook.php b/app/Core/Plugin/Hook.php index ade69150..ca197937 100644 --- a/app/Core/Plugin/Hook.php +++ b/app/Core/Plugin/Hook.php @@ -96,4 +96,21 @@ class Hook return null; } + + /** + * Hook with reference + * + * @access public + * @param string $hook + * @param mixed $param + * @return mixed + */ + public function reference($hook, &$param) + { + foreach ($this->getListeners($hook) as $listener) { + $listener($param); + } + + return $param; + } } diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php index 350dde6c..df443a52 100644 --- a/app/Formatter/BoardFormatter.php +++ b/app/Formatter/BoardFormatter.php @@ -44,6 +44,13 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface { $swimlanes = $this->swimlaneModel->getSwimlanes($this->projectId); $columns = $this->columnModel->getAll($this->projectId); + + if (empty($swimlanes) || empty($columns)) { + return array(); + } + + $this->hook->reference('formatter:board:query', $this->query); + $tasks = $this->query ->eq(TaskModel::TABLE.'.project_id', $this->projectId) ->asc(TaskModel::TABLE.'.position') @@ -52,10 +59,6 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface $task_ids = array_column($tasks, 'id'); $tags = $this->taskTagModel->getTagsByTasks($task_ids); - if (empty($swimlanes) || empty($columns)) { - return array(); - } - return BoardSwimlaneFormatter::getInstance($this->container) ->withSwimlanes($swimlanes) ->withColumns($columns) diff --git a/app/Pagination/SubtaskPagination.php b/app/Pagination/SubtaskPagination.php index f0cd6148..c55d0fb4 100644 --- a/app/Pagination/SubtaskPagination.php +++ b/app/Pagination/SubtaskPagination.php @@ -26,11 +26,14 @@ class SubtaskPagination extends Base */ public function getDashboardPaginator($user_id, $method, $max) { + $query = $this->subtaskModel->getUserQuery($user_id, array(SubtaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS)); + $this->hook->reference('pagination:dashboard:subtask:query', $query); + return $this->paginator ->setUrl('DashboardController', $method, array('pagination' => 'subtasks', 'user_id' => $user_id)) ->setMax($max) ->setOrder(TaskModel::TABLE.'.id') - ->setQuery($this->subtaskModel->getUserQuery($user_id, array(SubtaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))) + ->setQuery($query) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); } } diff --git a/app/Pagination/TaskPagination.php b/app/Pagination/TaskPagination.php index a395ab84..5fe986e7 100644 --- a/app/Pagination/TaskPagination.php +++ b/app/Pagination/TaskPagination.php @@ -25,11 +25,14 @@ class TaskPagination extends Base */ public function getDashboardPaginator($user_id, $method, $max) { + $query = $this->taskFinderModel->getUserQuery($user_id); + $this->hook->reference('pagination:dashboard:task:query', $query); + return $this->paginator ->setUrl('DashboardController', $method, array('pagination' => 'tasks', 'user_id' => $user_id)) ->setMax($max) ->setOrder(TaskModel::TABLE.'.id') - ->setQuery($this->taskFinderModel->getUserQuery($user_id)) + ->setQuery($query) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); } } diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 787c62df..5e01e93a 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -115,6 +115,31 @@ List of asset Hooks: - `template:layout:css` - `template:layout:js` + +Reference hooks +--------------- + +Reference hooks are passing a variable by reference. + +Example: + +```php +$this->hook->on('formatter:board:query', function (\PicoDb\Table &query) { + $query->eq('color_id', 'red'); +}); +``` + +The code above will show only tasks in red on the board. + +List of reference hooks: + +| Hook | Description | +|--------------------------------------------|---------------------------------------------------------------| +| `formatter:board:query` | Alter database query before rendering board | +| `pagination:dashboard:task:query` | Alter database query for tasks pagination on the dashboard | +| `pagination:dashboard:subtask:query` | Alter database query for subtasks pagination on the dashboard | + + Template Hooks -------------- diff --git a/tests/units/Core/Plugin/HookTest.php b/tests/units/Core/Plugin/HookTest.php index d1c139b3..acadede0 100644 --- a/tests/units/Core/Plugin/HookTest.php +++ b/tests/units/Core/Plugin/HookTest.php @@ -8,89 +8,103 @@ class HookTest extends Base { public function testGetListeners() { - $h = new Hook; - $this->assertEmpty($h->getListeners('myhook')); + $hook = new Hook; + $this->assertEmpty($hook->getListeners('myhook')); - $h->on('myhook', 'A'); - $h->on('myhook', 'B'); + $hook->on('myhook', 'A'); + $hook->on('myhook', 'B'); - $this->assertEquals(array('A', 'B'), $h->getListeners('myhook')); + $this->assertEquals(array('A', 'B'), $hook->getListeners('myhook')); } public function testExists() { - $h = new Hook; - $this->assertFalse($h->exists('myhook')); + $hook = new Hook; + $this->assertFalse($hook->exists('myhook')); - $h->on('myhook', 'A'); + $hook->on('myhook', 'A'); - $this->assertTrue($h->exists('myhook')); + $this->assertTrue($hook->exists('myhook')); } public function testMergeWithNoBinding() { - $h = new Hook; + $hook = new Hook; $values = array('A', 'B'); - $result = $h->merge('myhook', $values, array('p' => 'c')); + $result = $hook->merge('myhook', $values, array('p' => 'c')); $this->assertEquals($values, $result); } public function testMergeWithBindings() { - $h = new Hook; + $hook = new Hook; $values = array('A', 'B'); $expected = array('A', 'B', 'c', 'D'); - $h->on('myhook', function ($p) { + $hook->on('myhook', function ($p) { return array($p); }); - $h->on('myhook', function () { + $hook->on('myhook', function () { return array('D'); }); - $result = $h->merge('myhook', $values, array('p' => 'c')); + $result = $hook->merge('myhook', $values, array('p' => 'c')); $this->assertEquals($expected, $result); $this->assertEquals($expected, $values); } public function testMergeWithBindingButReturningBadData() { - $h = new Hook; + $hook = new Hook; $values = array('A', 'B'); $expected = array('A', 'B'); - $h->on('myhook', function () { + $hook->on('myhook', function () { return 'string'; }); - $result = $h->merge('myhook', $values); + $result = $hook->merge('myhook', $values); $this->assertEquals($expected, $result); $this->assertEquals($expected, $values); } public function testFirstWithNoBinding() { - $h = new Hook; + $hook = new Hook; - $result = $h->first('myhook', array('p' => 2)); + $result = $hook->first('myhook', array('p' => 2)); $this->assertEquals(null, $result); } public function testFirstWithMultipleBindings() { - $h = new Hook; + $hook = new Hook; - $h->on('myhook', function ($p) { + $hook->on('myhook', function ($p) { return $p + 1; }); - $h->on('myhook', function ($p) { + $hook->on('myhook', function ($p) { return $p; }); - $result = $h->first('myhook', array('p' => 3)); + $result = $hook->first('myhook', array('p' => 3)); $this->assertEquals(4, $result); } + + public function testHookWithReference() + { + $hook = new Hook(); + + $hook->on('myhook', function (&$p) { + $p = 2; + }); + + $param = 123; + $result = $hook->reference('myhook', $param); + $this->assertSame(2, $result); + $this->assertSame(2, $param); + } } -- cgit v1.2.3 From ffe61abc6910670c5c2c243eb82d9f5851f06c6b Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 13 Aug 2016 17:49:27 -0400 Subject: Improve form helpers and add more hooks --- app/Controller/TaskModificationController.php | 3 -- app/Controller/TaskViewController.php | 11 ------- app/Helper/FormHelper.php | 42 +++++++++++++++++++++++++++ app/Helper/TaskHelper.php | 18 +++--------- app/Model/TaskCreationModel.php | 2 ++ app/Model/TaskModificationModel.php | 2 ++ app/Template/dashboard/show.php | 2 ++ app/Template/dashboard/sidebar.php | 2 +- assets/css/app.min.css | 2 +- assets/sass/_form.sass | 25 ++++++++-------- doc/plugin-hooks.markdown | 3 ++ 11 files changed, 70 insertions(+), 42 deletions(-) (limited to 'doc') diff --git a/app/Controller/TaskModificationController.php b/app/Controller/TaskModificationController.php index d37f4bb4..cbc3777a 100644 --- a/app/Controller/TaskModificationController.php +++ b/app/Controller/TaskModificationController.php @@ -84,9 +84,6 @@ class TaskModificationController extends BaseController $values = $task; $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values)); - $values = $this->dateParser->format($values, array('date_due'), $this->dateParser->getUserDateFormat()); - $values = $this->dateParser->format($values, array('date_started'), $this->dateParser->getUserDateTimeFormat()); - return $values; } } diff --git a/app/Controller/TaskViewController.php b/app/Controller/TaskViewController.php index f40f8bea..e40ebdc0 100644 --- a/app/Controller/TaskViewController.php +++ b/app/Controller/TaskViewController.php @@ -22,7 +22,6 @@ class TaskViewController extends BaseController { $project = $this->projectModel->getByToken($this->request->getStringParam('token')); - // Token verification if (empty($project)) { throw AccessForbiddenException::getInstance()->withoutLayout(); } @@ -63,19 +62,9 @@ class TaskViewController extends BaseController $task = $this->getTask(); $subtasks = $this->subtaskModel->getAll($task['id']); - $values = array( - 'id' => $task['id'], - 'date_started' => $task['date_started'], - 'time_estimated' => $task['time_estimated'] ?: '', - 'time_spent' => $task['time_spent'] ?: '', - ); - - $values = $this->dateParser->format($values, array('date_started'), $this->dateParser->getUserDateTimeFormat()); - $this->response->html($this->helper->layout->task('task/show', array( 'task' => $task, 'project' => $this->projectModel->getById($task['project_id']), - 'values' => $values, 'files' => $this->taskFileModel->getAllDocuments($task['id']), 'images' => $this->taskFileModel->getAllImages($task['id']), 'comments' => $this->commentModel->getAll($task['id'], $this->userSession->getCommentSorting()), diff --git a/app/Helper/FormHelper.php b/app/Helper/FormHelper.php index c2ea1d72..0bb94d39 100644 --- a/app/Helper/FormHelper.php +++ b/app/Helper/FormHelper.php @@ -306,6 +306,48 @@ class FormHelper extends Base return $this->input('text', $name, $values, $errors, $attributes, $class.' form-numeric'); } + /** + * Date field + * + * @access public + * @param string $label + * @param string $name + * @param array $values + * @param array $errors + * @param array $attributes + * @return string + */ + public function date($label, $name, array $values, array $errors = array(), array $attributes = array()) + { + $userFormat = $this->dateParser->getUserDateFormat(); + $values = $this->dateParser->format($values, array($name), $userFormat); + $attributes = array_merge(array('placeholder="'.date($userFormat).'"'), $attributes); + + return $this->helper->form->label($label, $name) . + $this->helper->form->text($name, $values, $errors, $attributes, 'form-date'); + } + + /** + * Datetime field + * + * @access public + * @param string $label + * @param string $name + * @param array $values + * @param array $errors + * @param array $attributes + * @return string + */ + public function datetime($label, $name, array $values, array $errors = array(), array $attributes = array()) + { + $userFormat = $this->dateParser->getUserDateTimeFormat(); + $values = $this->dateParser->format($values, array($name), $userFormat); + $attributes = array_merge(array('placeholder="'.date($userFormat).'"'), $attributes); + + return $this->helper->form->label($label, $name) . + $this->helper->form->text($name, $values, $errors, $attributes, 'form-datetime'); + } + /** * Display the form error class * diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php index 599146b9..32f2a9ae 100644 --- a/app/Helper/TaskHelper.php +++ b/app/Helper/TaskHelper.php @@ -207,24 +207,14 @@ 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="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'); - - return $html; + $attributes = array_merge(array('tabindex="12"'), $attributes); + return $this->helper->form->datetime(t('Start Date'), 'date_started', $values, $errors, $attributes); } 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="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'); - - return $html; + $attributes = array_merge(array('tabindex="13"'), $attributes); + return $this->helper->form->date(t('Due Date'), 'date_due', $values, $errors, $attributes); } public function formatPriority(array $project, array $task) diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index 1c0fd7d9..b9b07d5e 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -85,5 +85,7 @@ class TaskCreationModel extends Base $values['date_modification'] = $values['date_creation']; $values['date_moved'] = $values['date_creation']; $values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; + + $this->hook->reference('model:task:creation:prepare', $values); } } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index 16b48f3d..6e16fbec 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -106,6 +106,8 @@ class TaskModificationModel extends Base $this->helper->model->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); $values['date_modification'] = time(); + + $this->hook->reference('model:task:modification:prepare', $values); } /** diff --git a/app/Template/dashboard/show.php b/app/Template/dashboard/show.php index cb18b79d..aec6f591 100644 --- a/app/Template/dashboard/show.php +++ b/app/Template/dashboard/show.php @@ -15,3 +15,5 @@ render('dashboard/projects', array('paginator' => $project_paginator, 'user' => $user)) ?> render('dashboard/tasks', array('paginator' => $task_paginator, 'user' => $user)) ?> render('dashboard/subtasks', array('paginator' => $subtask_paginator, 'user' => $user)) ?> + +hook->render('template:dashboard:show', array('user' => $user)) ?> diff --git a/app/Template/dashboard/sidebar.php b/app/Template/dashboard/sidebar.php index df4e91a5..108c028a 100644 --- a/app/Template/dashboard/sidebar.php +++ b/app/Template/dashboard/sidebar.php @@ -21,6 +21,6 @@
  • app->checkMenuSelection('DashboardController', 'notifications') ?>> url->link(t('My notifications'), 'DashboardController', 'notifications', array('user_id' => $user['id'])) ?>
  • - hook->render('template:dashboard:sidebar') ?> + hook->render('template:dashboard:sidebar', array('user' => $user)) ?> diff --git a/assets/css/app.min.css b/assets/css/app.min.css index ae139039..5570afec 100644 --- a/assets/css/app.min.css +++ b/assets/css/app.min.css @@ -1 +1 @@ -h1,li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}small{font-size:0.8em}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.pull-right{text-align:right}ul.no-bullet li{list-style-type:none;margin-left:0}.chosen-select{min-height:27px}#app-loading-icon{position:fixed;right:3px;bottom:3px}.assign-me{vertical-align:bottom}a{color:#36c;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h1{font-size:1.5em}h2{font-size:1.4em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px}th{border:1px solid #eee;padding:0.5em 3px}td{border:1px solid #eee;padding:0.5em 3px;vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:0.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field){color:#999;border:1px solid #ccc;width:300px;max-width:95%;font-size:1em;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;-moz-appearance:none}input[type="number"]:focus,input[type="date"]:focus,input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}textarea:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input.form-numeric,input[type="number"]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}.tag-autocomplete{width:400px}span.select2-container{margin-top:2px}::-webkit-input-placeholder,::-ms-input-placeholder,::-moz-placeholder{color:#999;opacity:0.2;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:0.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-datetime,input.form-date{width:150px}input.form-input-large{width:400px}input.form-input-small{width:150px}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-weight:bold}.reset-password{margin-top:20px}.reset-password a{color:#999}.input-addon{display:flex}.input-addon-field{flex:1;font-size:1em;color:#999;margin:0;-webkit-appearance:none;-moz-appearance:none}.input-addon-item{background-color:rgba(147,128,108,0.1);color:#666;font:inherit;font-weight:normal}@media (max-width: 480px){.input-addon-item .dropdown .fa-caret-down{display:none}}.input-addon-field,.input-addon-item{border:1px solid rgba(147,128,108,0.25);padding:4px 0.75em}.input-addon-field:not(:first-child),.input-addon-item:not(:first-child){border-left:0}.input-addon-field:first-child,.input-addon-item:first-child{border-radius:5px 0 0 5px}.input-addon-field:last-child,.input-addon-item:last-child{border-radius:0 5px 5px 0}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;margin-bottom:0;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}a.btn{text-decoration:none}.btn{-webkit-appearance:none;-moz-appearance:none;font-size:1.2em;font-weight:normal;cursor:pointer;display:inline-block;border-radius:2px;padding:3px 10px;margin:0;border:1px solid #ddd;background:#f5f5f5;color:#333}.btn:hover,.btn:focus{border-color:#bbb;background:#fafafa;color:#000}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{border-color:#b0281a;background:#c53727;color:#fff}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{border-color:#3079ed;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border-color:#ccc;background:#f7f7f7}.buttons-header{font-size:0.8em;margin-top:5px;margin-bottom:15px}.tooltip-arrow:after{background:#fff;border:1px solid #aaaaaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.ui-tooltip li{list-style-type:none}.tooltip .fa-info-circle{color:#999}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.dropdown-submenu-open li{display:block;margin:0;padding:8px 10px;font-size:0.9em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a{color:#fff}.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus{color:#333}.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed{margin-bottom:25px}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}#main .confirm{max-width:700px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}.popover-form{margin-bottom:0}.pagination{text-align:center}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}header{box-sizing:border-box;display:flex;flex-wrap:wrap;margin-top:5px;margin-bottom:5px;border-bottom:1px solid #dedede}header>*{box-sizing:border-box}header>*{width:1%}header .menus-container{width:10%}@media (min-width: 768px) and (max-width: 1150px){header .menus-container{width:15%}}@media (max-width: 768px){header .menus-container{width:65%;order:2}}header .board-selector-container{width:15%}@media (min-width: 768px) and (max-width: 1150px){header .board-selector-container{width:20%}}@media (max-width: 768px){header .board-selector-container{width:35%;order:1;margin-bottom:5px}}header .title-container{width:75%}@media (min-width: 768px) and (max-width: 1150px){header .title-container{width:65%}}@media (max-width: 768px){header .title-container{width:100%;order:3}}header h1{font-size:1.5em}header h1 .tooltip{opacity:0.3;font-size:0.7em}.web-notification-icon{color:#36c}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}.logo a{opacity:0.5;color:#d40000;text-decoration:none}.logo span{color:#333}.logo a:hover{opacity:0.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}.page-header{margin-bottom:20px}.page-header .dropdown{padding-right:10px}.page-header h2{margin:0;padding:0;font-weight:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#999}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.page-header li{display:inline;padding-right:15px}.page-header li.active a{color:#333;text-decoration:none;font-weight:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}.menu-inline li{display:inline;padding-right:15px}.menu-inline li .active a{font-weight:bold;color:#000;text-decoration:none}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:bold}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}.sidebar>ul li:last-child{margin-bottom:15px}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:bold;color:#b94a48}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:0.5}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:0.9em;color:#555}.file-thumbnail-description{font-size:0.8em;color:#999;margin-top:8px;margin-bottom:5px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.filter-box{max-width:800px}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:hover,.action-menu:focus{text-decoration:underline}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{text-align:center;margin-right:3%;padding:3px 15px 3px 15px;border:1px dashed #ddd}.project-overview-column small{color:#999}.project-overview-column strong{color:#555}.project-header{box-sizing:border-box;display:flex;flex-wrap:wrap}.project-header>*{box-sizing:border-box}.project-header>*{width:1%}.project-header .dropdown-component{width:5%}@media (min-width: 768px) and (max-width: 1150px){.project-header .dropdown-component{width:8%}}@media (max-width: 768px){.project-header .dropdown-component{width:100%}}.project-header .views-switcher-component{width:38%}@media (max-width: 1300px){.project-header .views-switcher-component{width:45%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .views-switcher-component{width:92%}}@media (max-width: 768px){.project-header .views-switcher-component{width:100%}}.project-header .filter-box-component{margin:0;width:55%}@media (max-width: 1300px){.project-header .filter-box-component{width:50%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}@media (max-width: 768px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}.project-header .filter-box-component form{margin:0}.views{display:inline-block;margin-right:10px;font-size:0.9em}@media (max-width: 560px){.views{width:100%}}@media (max-width: 768px){.views{margin-top:10px;font-size:1em}}.views li{white-space:nowrap;background:#fafafa;border:1px solid #ddd;border-right:none;padding:4px 8px;display:inline}@media (max-width: 560px){.views li{display:block;margin-top:5px;border-radius:5px;border:1px solid #ddd}}.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.dashboard-project-stats small{margin-right:10px;color:#999}.dashboard-table-link{font-weight:bold;color:#000;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast: active), (-ms-high-contrast: none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size:1.6em;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:normal}a.board-swimlane-toggle{text-decoration:none}a.board-swimlane-toggle:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:none}.board-task-list{min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;word-wrap:break-word;font-size:0.9em}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:bold}.task-board .task-score{font-weight:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.task-board-title{margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-saving-state{opacity:0.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px 1px 2px;border-radius:4px}.task-board-category:hover{opacity:0.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-change-assignee:hover{opacity:0.6}.task-board-icons{font-size:0.8em;text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:0.5}.task-board-icons span{opacity:0.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1.0}.flag-milestone{color:green}.task-board-age{display:inline-block}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}.task-board-date{font-weight:bold;color:#000}span.task-board-date-today{opacity:1.0}span.task-board-date-overdue{opacity:1.0}.task-tags li{display:inline;margin:0;margin-right:4px;padding:2px;color:#555;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}#task-summary{margin-bottom:15px}#task-summary h2{color:#555;font-size:1.6em;margin-top:0;padding-top:0}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{color:#333}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;margin:0;padding:8px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.comment-sorting{text-align:right}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#999}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-date{color:#999;font-weight:200}.comment-actions{font-size:0.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table td{vertical-align:middle}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.task-link-closed{text-decoration:line-through}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-weight:bold}.markdown h2{font-weight:bold}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#555}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;margin-bottom:30px}.documentation h2{text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.activity-description{color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}div.ganttview-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#555}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#555}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.ganttview-vtheader-series-name a{color:#555;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#555}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid #c0c0c0;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:0.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:-0}.user-mention-link{font-weight:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} +h1,li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}small{font-size:0.8em}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.pull-right{text-align:right}ul.no-bullet li{list-style-type:none;margin-left:0}.chosen-select{min-height:27px}#app-loading-icon{position:fixed;right:3px;bottom:3px}.assign-me{vertical-align:bottom}a{color:#36c;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h1{font-size:1.5em}h2{font-size:1.4em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px}th{border:1px solid #eee;padding:0.5em 3px}td{border:1px solid #eee;padding:0.5em 3px;vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:0.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field){color:#999;border:1px solid #ccc;width:300px;max-width:95%;font-size:1em;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;-moz-appearance:none}input[type="number"]:focus,input[type="date"]:focus,input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input[type="number"]{width:70px}input[type="text"]:not(.input-addon-field).form-numeric{width:70px}input[type="text"]:not(.input-addon-field).form-datetime,input[type="text"]:not(.input-addon-field).form-date{width:150px}input[type="text"]:not(.input-addon-field).form-input-large{width:400px}input[type="text"]:not(.input-addon-field).form-input-small{width:150px}textarea:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}.tag-autocomplete{width:400px}span.select2-container{margin-top:2px}::-webkit-input-placeholder,::-ms-input-placeholder,::-moz-placeholder{color:#999;opacity:0.2;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:0.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-weight:bold}.reset-password{margin-top:20px}.reset-password a{color:#999}.input-addon{display:flex}.input-addon-field{flex:1;font-size:1em;color:#999;margin:0;-webkit-appearance:none;-moz-appearance:none}.input-addon-item{background-color:rgba(147,128,108,0.1);color:#666;font:inherit;font-weight:normal}@media (max-width: 480px){.input-addon-item .dropdown .fa-caret-down{display:none}}.input-addon-field,.input-addon-item{border:1px solid rgba(147,128,108,0.25);padding:4px 0.75em}.input-addon-field:not(:first-child),.input-addon-item:not(:first-child){border-left:0}.input-addon-field:first-child,.input-addon-item:first-child{border-radius:5px 0 0 5px}.input-addon-field:last-child,.input-addon-item:last-child{border-radius:0 5px 5px 0}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;margin-bottom:0;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}a.btn{text-decoration:none}.btn{-webkit-appearance:none;-moz-appearance:none;font-size:1.2em;font-weight:normal;cursor:pointer;display:inline-block;border-radius:2px;padding:3px 10px;margin:0;border:1px solid #ddd;background:#f5f5f5;color:#333}.btn:hover,.btn:focus{border-color:#bbb;background:#fafafa;color:#000}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{border-color:#b0281a;background:#c53727;color:#fff}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{border-color:#3079ed;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border-color:#ccc;background:#f7f7f7}.buttons-header{font-size:0.8em;margin-top:5px;margin-bottom:15px}.tooltip-arrow:after{background:#fff;border:1px solid #aaaaaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.ui-tooltip li{list-style-type:none}.tooltip .fa-info-circle{color:#999}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.dropdown-submenu-open li{display:block;margin:0;padding:8px 10px;font-size:0.9em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a{color:#fff}.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus{color:#333}.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed{margin-bottom:25px}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}#main .confirm{max-width:700px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}.popover-form{margin-bottom:0}.pagination{text-align:center}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}header{box-sizing:border-box;display:flex;flex-wrap:wrap;margin-top:5px;margin-bottom:5px;border-bottom:1px solid #dedede}header>*{box-sizing:border-box}header>*{width:1%}header .menus-container{width:10%}@media (min-width: 768px) and (max-width: 1150px){header .menus-container{width:15%}}@media (max-width: 768px){header .menus-container{width:65%;order:2}}header .board-selector-container{width:15%}@media (min-width: 768px) and (max-width: 1150px){header .board-selector-container{width:20%}}@media (max-width: 768px){header .board-selector-container{width:35%;order:1;margin-bottom:5px}}header .title-container{width:75%}@media (min-width: 768px) and (max-width: 1150px){header .title-container{width:65%}}@media (max-width: 768px){header .title-container{width:100%;order:3}}header h1{font-size:1.5em}header h1 .tooltip{opacity:0.3;font-size:0.7em}.web-notification-icon{color:#36c}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}.logo a{opacity:0.5;color:#d40000;text-decoration:none}.logo span{color:#333}.logo a:hover{opacity:0.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}.page-header{margin-bottom:20px}.page-header .dropdown{padding-right:10px}.page-header h2{margin:0;padding:0;font-weight:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#999}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.page-header li{display:inline;padding-right:15px}.page-header li.active a{color:#333;text-decoration:none;font-weight:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}.menu-inline li{display:inline;padding-right:15px}.menu-inline li .active a{font-weight:bold;color:#000;text-decoration:none}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:bold}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}.sidebar>ul li:last-child{margin-bottom:15px}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:bold;color:#b94a48}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:0.5}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:0.9em;color:#555}.file-thumbnail-description{font-size:0.8em;color:#999;margin-top:8px;margin-bottom:5px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.filter-box{max-width:800px}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:hover,.action-menu:focus{text-decoration:underline}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{text-align:center;margin-right:3%;padding:3px 15px 3px 15px;border:1px dashed #ddd}.project-overview-column small{color:#999}.project-overview-column strong{color:#555}.project-header{box-sizing:border-box;display:flex;flex-wrap:wrap}.project-header>*{box-sizing:border-box}.project-header>*{width:1%}.project-header .dropdown-component{width:5%}@media (min-width: 768px) and (max-width: 1150px){.project-header .dropdown-component{width:8%}}@media (max-width: 768px){.project-header .dropdown-component{width:100%}}.project-header .views-switcher-component{width:38%}@media (max-width: 1300px){.project-header .views-switcher-component{width:45%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .views-switcher-component{width:92%}}@media (max-width: 768px){.project-header .views-switcher-component{width:100%}}.project-header .filter-box-component{margin:0;width:55%}@media (max-width: 1300px){.project-header .filter-box-component{width:50%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}@media (max-width: 768px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}.project-header .filter-box-component form{margin:0}.views{display:inline-block;margin-right:10px;font-size:0.9em}@media (max-width: 560px){.views{width:100%}}@media (max-width: 768px){.views{margin-top:10px;font-size:1em}}.views li{white-space:nowrap;background:#fafafa;border:1px solid #ddd;border-right:none;padding:4px 8px;display:inline}@media (max-width: 560px){.views li{display:block;margin-top:5px;border-radius:5px;border:1px solid #ddd}}.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.dashboard-project-stats small{margin-right:10px;color:#999}.dashboard-table-link{font-weight:bold;color:#000;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast: active), (-ms-high-contrast: none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size:1.6em;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:normal}a.board-swimlane-toggle{text-decoration:none}a.board-swimlane-toggle:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:none}.board-task-list{min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;word-wrap:break-word;font-size:0.9em}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:bold}.task-board .task-score{font-weight:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.task-board-title{margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-saving-state{opacity:0.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px 1px 2px;border-radius:4px}.task-board-category:hover{opacity:0.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-change-assignee:hover{opacity:0.6}.task-board-icons{font-size:0.8em;text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:0.5}.task-board-icons span{opacity:0.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1.0}.flag-milestone{color:green}.task-board-age{display:inline-block}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}.task-board-date{font-weight:bold;color:#000}span.task-board-date-today{opacity:1.0}span.task-board-date-overdue{opacity:1.0}.task-tags li{display:inline;margin:0;margin-right:4px;padding:2px;color:#555;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}#task-summary{margin-bottom:15px}#task-summary h2{color:#555;font-size:1.6em;margin-top:0;padding-top:0}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{color:#333}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;margin:0;padding:8px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.comment-sorting{text-align:right}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#999}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-date{color:#999;font-weight:200}.comment-actions{font-size:0.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table td{vertical-align:middle}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.task-link-closed{text-decoration:line-through}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-weight:bold}.markdown h2{font-weight:bold}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#555}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;margin-bottom:30px}.documentation h2{text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.activity-description{color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}div.ganttview-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#555}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#555}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.ganttview-vtheader-series-name a{color:#555;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#555}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid #c0c0c0;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:0.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:-0}.user-mention-link{font-weight:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} diff --git a/assets/sass/_form.sass b/assets/sass/_form.sass index e89fdc24..e46e14d4 100644 --- a/assets/sass/_form.sass +++ b/assets/sass/_form.sass @@ -26,16 +26,25 @@ input outline: 0 box-shadow: 0 0 8px rgba(82, 168, 236, 0.6) +input[type="number"] + width: 70px + +input[type="text"]:not(.input-addon-field) + &.form-numeric + width: 70px + &.form-datetime, &.form-date + width: 150px + &.form-input-large + width: 400px + &.form-input-small + width: 150px + textarea:focus color: color('dark') border-color: rgba(82, 168, 236, 0.8) outline: 0 box-shadow: 0 0 8px rgba(82, 168, 236, 0.6) -input - &.form-numeric, &[type="number"] - width: 70px - textarea border: 1px solid #ccc width: 400px @@ -101,14 +110,6 @@ ul.form-errors li .form-inline-group display: inline -input - &.form-datetime, &.form-date - width: 150px - &.form-input-large - width: 400px - &.form-input-small - width: 150px - .form-columns display: -webkit-flex display: flex diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 5e01e93a..9a4bdab2 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -138,6 +138,8 @@ List of reference hooks: | `formatter:board:query` | Alter database query before rendering board | | `pagination:dashboard:task:query` | Alter database query for tasks pagination on the dashboard | | `pagination:dashboard:subtask:query` | Alter database query for subtasks pagination on the dashboard | +| `model:task:creation:prepare` | Alter form values before to save a task | +| `model:task:modification:prepare` | Alter form values before to edit a task | Template Hooks @@ -186,6 +188,7 @@ List of template hooks: | `template:config:email` | Email settings page | | `template:config:integrations` | Integration page in global settings | | `template:dashboard:sidebar` | Sidebar on dashboard page | +| `template:dashboard:show` | Main page of the dashboard | | `template:export:sidebar` | Sidebar on export pages | | `template:import:sidebar` | Sidebar on import pages | | `template:header:dropdown` | Page header dropdown menu (user avatar icon) | -- cgit v1.2.3 From 2ebe8b32728c341ec16e1197fe2e12d32ddd5de5 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 13 Aug 2016 18:08:46 -0400 Subject: Add the possibility to attach template hooks with local variables --- ChangeLog | 1 + app/Helper/HookHelper.php | 25 +++++++++++++++------- doc/plugin-hooks.markdown | 8 +++++++ tests/units/Helper/HookHelperTest.php | 40 +++++++++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 17 deletions(-) (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index 25ce7eea..d013ad9a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ Version 1.0.33 (unreleased) Improvements: +* Add the possibility to attach template hooks with local variables * Add "reference" hooks * Show project name in task forms * Convert vanilla CSS to SASS diff --git a/app/Helper/HookHelper.php b/app/Helper/HookHelper.php index cb4dc1ef..418c55a0 100644 --- a/app/Helper/HookHelper.php +++ b/app/Helper/HookHelper.php @@ -24,8 +24,8 @@ class HookHelper extends Base { $buffer = ''; - foreach ($this->hook->getListeners($hook) as $file) { - $buffer .= $this->helper->asset->$type($file); + foreach ($this->hook->getListeners($hook) as $params) { + $buffer .= $this->helper->asset->$type($params['template']); } return $buffer; @@ -43,8 +43,12 @@ class HookHelper extends Base { $buffer = ''; - foreach ($this->hook->getListeners($hook) as $template) { - $buffer .= $this->template->render($template, $variables); + foreach ($this->hook->getListeners($hook) as $params) { + if (! empty($params['variables'])) { + $variables = array_merge($variables, $params['variables']); + } + + $buffer .= $this->template->render($params['template'], $variables); } return $buffer; @@ -54,13 +58,18 @@ class HookHelper extends Base * Attach a template to a hook * * @access public - * @param string $hook - * @param string $template + * @param string $hook + * @param string $template + * @param array $variables * @return $this */ - public function attach($hook, $template) + public function attach($hook, $template, array $variables = array()) { - $this->hook->on($hook, $template); + $this->hook->on($hook, array( + 'template' => $template, + 'variables' => $variables, + )); + return $this; } } diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 9a4bdab2..a700e34b 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -153,6 +153,14 @@ Example to add new content in the dashboard sidebar: $this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar'); ``` +Example to attach a template with local variables: + +```php +$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar', array( + 'variable' => 'foobar', +)); +``` + This call is usually defined in the `initialize()` method. The first argument is name of the hook and the second argument is the template name. diff --git a/tests/units/Helper/HookHelperTest.php b/tests/units/Helper/HookHelperTest.php index 6e03acd1..66d13381 100644 --- a/tests/units/Helper/HookHelperTest.php +++ b/tests/units/Helper/HookHelperTest.php @@ -6,6 +6,28 @@ use Kanboard\Helper\HookHelper; class HookHelperTest extends Base { + public function testAttachLocalVariables() + { + $this->container['template'] = $this + ->getMockBuilder('\Kanboard\Core\Template') + ->setConstructorArgs(array($this->container['helper'])) + ->setMethods(array('render')) + ->getMock(); + + $this->container['template'] + ->expects($this->once()) + ->method('render') + ->with( + $this->equalTo('tpl1'), + $this->equalTo(array('k0' => 'v0', 'k1' => 'v1')) + ) + ->will($this->returnValue('tpl1_content')); + + $hookHelper = new HookHelper($this->container); + $hookHelper->attach('test', 'tpl1', array('k1' => 'v1')); + $this->assertEquals('tpl1_content', $hookHelper->render('test', array('k0' => 'v0'))); + } + public function testMultipleHooks() { $this->container['template'] = $this @@ -32,10 +54,10 @@ class HookHelperTest extends Base ) ->will($this->returnValue('tpl2_content')); - $h = new HookHelper($this->container); - $h->attach('test', 'tpl1'); - $h->attach('test', 'tpl2'); - $this->assertEquals('tpl1_contenttpl2_content', $h->render('test')); + $hookHelper = new HookHelper($this->container); + $hookHelper->attach('test', 'tpl1'); + $hookHelper->attach('test', 'tpl2'); + $this->assertEquals('tpl1_contenttpl2_content', $hookHelper->render('test')); } public function testAssetHooks() @@ -64,11 +86,11 @@ class HookHelperTest extends Base ) ->will($this->returnValue('')); - $h = new HookHelper($this->container); - $h->attach('test1', 'skin.css'); - $h->attach('test2', 'skin.js'); + $hookHelper = new HookHelper($this->container); + $hookHelper->attach('test1', 'skin.css'); + $hookHelper->attach('test2', 'skin.js'); - $this->assertContains('', $h->asset('css', 'test1')); - $this->assertContains('', $h->asset('js', 'test2')); + $this->assertContains('', $hookHelper->asset('css', 'test1')); + $this->assertContains('', $hookHelper->asset('js', 'test2')); } } -- cgit v1.2.3 From 010199e8f846f6c0b4f23336338bfda17ec04901 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 13 Aug 2016 18:41:01 -0400 Subject: Add the possibility to attach template hooks with a callback --- ChangeLog | 2 +- app/Helper/HookHelper.php | 27 +++++++++++++++++++ doc/plugin-hooks.markdown | 8 ++++++ tests/units/Helper/HookHelperTest.php | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) (limited to 'doc') diff --git a/ChangeLog b/ChangeLog index d013ad9a..25a92168 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,7 +3,7 @@ Version 1.0.33 (unreleased) Improvements: -* Add the possibility to attach template hooks with local variables +* Add the possibility to attach template hooks with local variables and callback * Add "reference" hooks * Show project name in task forms * Convert vanilla CSS to SASS diff --git a/app/Helper/HookHelper.php b/app/Helper/HookHelper.php index 418c55a0..e43cfdfd 100644 --- a/app/Helper/HookHelper.php +++ b/app/Helper/HookHelper.php @@ -46,6 +46,12 @@ class HookHelper extends Base foreach ($this->hook->getListeners($hook) as $params) { if (! empty($params['variables'])) { $variables = array_merge($variables, $params['variables']); + } elseif (! empty($params['callable'])) { + $result = call_user_func_array($params['callable'], $variables); + + if (is_array($result)) { + $variables = array_merge($variables, $result); + } } $buffer .= $this->template->render($params['template'], $variables); @@ -72,4 +78,25 @@ class HookHelper extends Base return $this; } + + /** + * Attach a template to a hook with a callable + * + * Arguments passed to the callback are the one passed to the hook + * + * @access public + * @param string $hook + * @param string $template + * @param callable $callable + * @return $this + */ + public function attachCallable($hook, $template, callable $callable) + { + $this->hook->on($hook, array( + 'template' => $template, + 'callable' => $callable, + )); + + return $this; + } } diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index a700e34b..97816d5f 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -161,6 +161,14 @@ $this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/ )); ``` +Example to attach a template with a callable: + +```php +$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar', function($hook_param1, $hook_param2) { + return array('new_template_variable' => 'foobar'); // Inject a new variable into the plugin template +}); +``` + This call is usually defined in the `initialize()` method. The first argument is name of the hook and the second argument is the template name. diff --git a/tests/units/Helper/HookHelperTest.php b/tests/units/Helper/HookHelperTest.php index 66d13381..a67eaed9 100644 --- a/tests/units/Helper/HookHelperTest.php +++ b/tests/units/Helper/HookHelperTest.php @@ -6,6 +6,57 @@ use Kanboard\Helper\HookHelper; class HookHelperTest extends Base { + public function testAttachCallable() + { + $this->container['template'] = $this + ->getMockBuilder('\Kanboard\Core\Template') + ->setConstructorArgs(array($this->container['helper'])) + ->setMethods(array('render')) + ->getMock(); + + $this->container['template'] + ->expects($this->once()) + ->method('render') + ->with( + $this->equalTo('tpl1'), + $this->equalTo(array('k0' => 'v0', 'k1' => 'v1')) + ) + ->will($this->returnValue('tpl1_content')); + + $hookHelper = new HookHelper($this->container); + $hookHelper->attachCallable('test', 'tpl1', function() { + return array( + 'k1' => 'v1', + ); + }); + + $this->assertEquals('tpl1_content', $hookHelper->render('test', array('k0' => 'v0'))); + } + + public function testAttachCallableWithNoResult() + { + $this->container['template'] = $this + ->getMockBuilder('\Kanboard\Core\Template') + ->setConstructorArgs(array($this->container['helper'])) + ->setMethods(array('render')) + ->getMock(); + + $this->container['template'] + ->expects($this->once()) + ->method('render') + ->with( + $this->equalTo('tpl1'), + $this->equalTo(array('k0' => 'v0')) + ) + ->will($this->returnValue('tpl1_content')); + + $hookHelper = new HookHelper($this->container); + $hookHelper->attachCallable('test', 'tpl1', function() { + }); + + $this->assertEquals('tpl1_content', $hookHelper->render('test', array('k0' => 'v0'))); + } + public function testAttachLocalVariables() { $this->container['template'] = $this -- cgit v1.2.3 From cab8ff8989fac739b6ec24046f8368792b1fbc04 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 14 Aug 2016 14:29:27 -0400 Subject: Fix typo in documentation --- doc/api-swimlane-procedures.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'doc') diff --git a/doc/api-swimlane-procedures.markdown b/doc/api-swimlane-procedures.markdown index c58e56c9..d7c1e28f 100644 --- a/doc/api-swimlane-procedures.markdown +++ b/doc/api-swimlane-procedures.markdown @@ -373,7 +373,7 @@ Response example: ## disableSwimlane -- Purpose: **Enable a swimlane** +- Purpose: **Disable a swimlane** - Parameters: - **project_id** (integer, required) - **swimlane_id** (integer, required) -- cgit v1.2.3 From a72ef8cedcdda66fc7fc477b2805acbd58142e07 Mon Sep 17 00:00:00 2001 From: Christopher Geelen Date: Mon, 15 Aug 2016 13:12:38 +0200 Subject: fixed description in doc for this->hook->on to comply with new HookHelper args --- doc/plugin-hooks.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'doc') diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 97816d5f..444b76db 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -105,7 +105,7 @@ class Plugin extends Base { public function initialize() { - $this->hook->on('template:layout:css', 'plugins/Css/skin.css'); + $this->hook->on('template:layout:css', array('template' => 'plugins/Css/skin.css')); } } ``` -- cgit v1.2.3