From 0b1dcecc9c3cd188b8f56f4fe6172ee2f34266ad Mon Sep 17 00:00:00 2001 From: ngosang Date: Sun, 19 Jun 2016 19:29:15 +0200 Subject: Update es_ES translation --- app/Locale/es_ES/translations.php | 388 +++++++++++++++++++------------------- 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index a646e4ef..39757f74 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -20,16 +20,16 @@ return array( 'Orange' => 'Naranja', 'Grey' => 'Gris', 'Brown' => 'Marrón', - 'Deep Orange' => 'Naranja Oscuro', - 'Dark Grey' => 'Gris Oscuro', + 'Deep Orange' => 'Naranja oscuro', + 'Dark Grey' => 'Gris oscuro', 'Pink' => 'Rosa', - 'Teal' => 'Verde Azulado', - 'Cyan' => 'Cián', + 'Teal' => 'Verde azulado', + 'Cyan' => 'Cian', 'Lime' => 'Lima', - 'Light Green' => 'Verde Claro', + 'Light Green' => 'Verde claro', 'Amber' => 'Ámbar', 'Save' => 'Guardar', - 'Login' => 'Iniciar sesión (Ingresar)', + 'Login' => 'Iniciar sesión (ingresar)', 'Official website:' => 'Página web oficial:', 'Unassigned' => 'No asignado', 'View this task' => 'Ver esta tarea', @@ -80,7 +80,7 @@ return array( 'Settings' => 'Preferencias', 'Application settings' => 'Preferencias de la aplicación', 'Language' => 'Idioma', - 'Webhook token:' => 'Token de los disparadores Web (webhooks):', + 'Webhook token:' => 'Token de los disparadores web (webhooks):', 'API token:' => 'Token de la API:', 'Database size:' => 'Tamaño de la base de datos:', 'Download the database' => 'Descargar la base de datos', @@ -175,7 +175,7 @@ return array( 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.', 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', 'Remove an action' => 'Eliminar una acción', - 'Unable to remove this action.' => 'No se puede eliminar esta accción.', + 'Unable to remove this action.' => 'No se puede eliminar esta acción.', 'Action removed successfully.' => 'La acción ha sido eliminada correctamente.', 'Automatic actions for the project "%s"' => 'Acciones automatizadas para el proyecto «%s»', 'Add an action' => 'Añadir una acción', @@ -315,9 +315,9 @@ return array( 'Project cloned successfully.' => 'Proyecto clonado correctamente.', 'Unable to clone this project.' => 'No se puede clonar este proyecto.', 'Enable email notifications' => 'Habilitar notificaciones por correo electrónico', - 'Task position:' => 'Posición de la tarea', - 'The task #%d have been opened.' => 'La tarea #%d ha sido abierta', - 'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada', + 'Task position:' => 'Posición de la tarea:', + 'The task #%d have been opened.' => 'La tarea #%d ha sido abierta.', + 'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada.', 'Sub-task updated' => 'Subtarea actualizada', 'Title:' => 'Título:', 'Status:' => 'Estado:', @@ -362,7 +362,7 @@ return array( 'Account type:' => 'Tipo de cuenta:', 'Edit profile' => 'Modificar perfil', 'Change password' => 'Cambiar contraseña', - 'Password modification' => 'Modificacion de contraseña', + 'Password modification' => 'Modificación de contraseña', 'External authentications' => 'Autenticación externa', 'Never connected.' => 'Nunca se ha conectado.', 'No external authentication enabled.' => 'Sin autenticación externa activa.', @@ -411,13 +411,13 @@ return array( 'About' => 'Acerca de', 'Database driver:' => 'Controlador de la base de datos (driver):', 'Board settings' => 'Preferencias del tablero', - 'Webhook settings' => 'Preferencias del disparador web (Webhook)', + 'Webhook settings' => 'Preferencias del disparador web (webhook)', 'Reset token' => 'Limpiar token', 'API endpoint:' => 'Endpoint del API:', 'Refresh interval for private board' => 'Intervalo de refresco del tablero privado', 'Refresh interval for public board' => 'Intervalo de refresco del tablero público', 'Task highlight period' => 'Periodo de realce de la tarea', - 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (en segundos) para considerar que una tarea fué modificada recientemente (0 para deshabilitar, 2 días por defecto)', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (en segundos) para considerar que una tarea fue modificada recientemente (0 para deshabilitar, 2 días por defecto)', 'Frequency in second (60 seconds by default)' => 'Frecuencia en segundos (60 segundos por defecto)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frecuencia en segundos (0 para deshabilitar esta característica, 10 segundos por defecto)', 'Application URL' => 'URL de la aplicación', @@ -435,7 +435,7 @@ return array( 'Dashboard' => 'Tablero', 'Confirmation' => 'Confirmación', 'Allow everybody to access to this project' => 'Permitir a cualquiera acceder a este proyecto', - 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto', + 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto.', 'Webhooks' => 'Disparadores web (webhooks)', 'API' => 'API', 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', @@ -504,8 +504,8 @@ return array( 'Task Title' => 'Título de la tarea', 'Untitled' => 'Sin título', 'Application default' => 'Predefinido por la aplicación', - 'Language:' => 'Idioma', - 'Timezone:' => 'Zona horaria', + 'Language:' => 'Idioma:', + 'Timezone:' => 'Zona horaria:', 'All columns' => 'Todas las columnas', 'Calendar' => 'Calendario', 'Next' => 'Siguiente', @@ -564,7 +564,7 @@ return array( '%dh' => '%dh', 'Expand tasks' => 'Expandir tareas', 'Collapse tasks' => 'Colapsar tareas', - 'Expand/collapse tasks' => 'Expande/colapasa tareas', + 'Expand/collapse tasks' => 'Expande/colapsa tareas', 'Close dialog box' => 'Cerrar caja de diálogo', 'Submit a form' => 'Enviar formulario', 'Board view' => 'Vista de tablero', @@ -597,14 +597,14 @@ return array( 'Executer' => 'Ejecutor', 'Time spent in the column' => 'Tiempo transcurrido en la columna', 'Task transitions' => 'Transiciones de tarea', - 'Task transitions export' => 'Eportar transiciones de tarea', - 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este informe contiene todos los movimientos de columna para cada tarea con la fecha, el usuario y el tiempo transcurrido en cada trasición.', + 'Task transitions export' => 'Exportar transiciones de tarea', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este informe contiene todos los movimientos de columna para cada tarea con la fecha, el usuario y el tiempo transcurrido en cada transición.', 'Currency rates' => 'Cambio de monedas', 'Rate' => 'Cambio', 'Change reference currency' => 'Cambiar moneda de referencia', 'Add a new currency rate' => 'Añadir nuevo cambio de moneda', 'Reference currency' => 'Moneda de referencia', - 'The currency rate have been added successfully.' => 'Se ha añadido el cambio de moneda correctamente.', + 'The currency rate have been added successfully.' => 'El cambio de moneda se ha añadido correctamente.', 'Unable to add this currency rate.' => 'No se puede añadir este cambio de moneda.', 'Webhook URL' => 'URL del disparador web (webhook)', '%s remove the assignee of the task %s' => '%s quita el responsable de la tarea %s', @@ -623,67 +623,67 @@ return array( '%s via Kanboard' => '%s vía Kanboard', 'Burndown chart for "%s"' => 'Trabajo pendiente para «%s»', 'Burndown chart' => 'Trabajo pendiente', - 'This chart show the task complexity over the time (Work Remaining).' => 'Este diagrama mestra la complejidad de la tarea a lo largo del tiempo (trabajo restante).', + 'This chart show the task complexity over the time (Work Remaining).' => 'Este diagrama muestra la complejidad de la tarea a lo largo del tiempo (trabajo restante).', 'Screenshot taken %s' => 'Pantallazo tomado el %s', 'Add a screenshot' => 'Añadir un pantallazo', 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Capture un patallazo y pulse CTRL+V o ⌘+V para pegar aquí.', 'Screenshot uploaded successfully.' => 'Pantallazo cargado correctamente.', 'SEK - Swedish Krona' => 'SEK - Corona sueca', 'Identifier' => 'Identificador', - 'Disable two factor authentication' => 'Desactivar la autenticación de dos factores', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmentes quiere desactuvar la autenticación de dos factores para este usuario: "%s?"', + 'Disable two factor authentication' => 'Desactivar la autenticación en dos pasos', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmente desea desactivar la autenticación en dos pasos para este usuario: «%s»?', 'Edit link' => 'Modificar enlace', 'Start to type task title...' => 'Empiece a escribir el título de la tarea...', - 'A task cannot be linked to itself' => 'Una tarea no puede se enlazada con sigo misma', + 'A task cannot be linked to itself' => 'Una tarea no se puede enlazar con sigo misma', 'The exact same link already exists' => 'El mismo enlace ya existe', 'Recurrent task is scheduled to be generated' => 'Tarea recurrente programada para ser generada', 'Score' => 'Puntuación', 'The identifier must be unique' => 'El identificador debe ser único', - 'This linked task id doesn\'t exists' => 'El id de tarea no existe', + 'This linked task id doesn\'t exists' => 'El identificador de tarea no existe', 'This value must be alphanumeric' => 'Este valor debe ser alfanumérico', 'Edit recurrence' => 'Modificar repetición', - 'Generate recurrent task' => 'Generar tarea recurrente', - 'Trigger to generate recurrent task' => 'Disparador para generar tarea recurrente', + 'Generate recurrent task' => 'Generar una tarea recurrente', + 'Trigger to generate recurrent task' => 'Disparador para generar una tarea recurrente', 'Factor to calculate new due date' => 'Factor para calcular la nueva fecha de entrega', 'Timeframe to calculate new due date' => 'Calendario para calcular la nueva fecha de entrega', 'Base date to calculate new due date' => 'Fecha base para calcular la nueva fecha de entrega', 'Action date' => 'Fecha de la acción', - 'Base date to calculate new due date: ' => 'Fecha base para calcular la nueva fecha de entrega:', - 'This task has created this child task: ' => 'Esta tarea ha cerado esta tarea hija:', + 'Base date to calculate new due date: ' => 'Fecha base para calcular la nueva fecha de entrega: ', + 'This task has created this child task: ' => 'Esta tarea ha creado esta tarea hija: ', 'Day(s)' => 'Día(s)', 'Existing due date' => 'Fecha de entrega existente', - 'Factor to calculate new due date: ' => 'Factor para calcular la nueva fecha de entrega:', + 'Factor to calculate new due date: ' => 'Factor para calcular la nueva fecha de entrega: ', 'Month(s)' => 'Mes(es)', 'Recurrence' => 'Repetición', 'This task has been created by: ' => 'Esta tarea ha sido creada por: ', 'Recurrent task has been generated:' => 'Tarea recurrente generada:', - 'Timeframe to calculate new due date: ' => 'Calendario para calcular la nueva fecha de entrega:', - 'Trigger to generate recurrent task: ' => 'Disparador para generar tarea recurrente', + 'Timeframe to calculate new due date: ' => 'Calendario para calcular la nueva fecha de entrega: ', + 'Trigger to generate recurrent task: ' => 'Disparador para generar una tarea recurrente: ', 'When task is closed' => 'Cuando la tarea es cerrada', 'When task is moved from first column' => 'Cuando la tarea es movida desde la primera columna', 'When task is moved to last column' => 'Cuando la tarea es movida a la última columna', 'Year(s)' => 'Año(s)', - 'Calendar settings' => 'Parámetros del Calendario', - 'Project calendar view' => 'Vista de Calendario para el Proyecto', - 'Project settings' => 'Parámetros del Proyecto', - 'Show subtasks based on the time tracking' => 'Mostrar subtareas en base al seguimiento de tiempo', + 'Calendar settings' => 'Preferencias del calendario', + 'Project calendar view' => 'Vista de calendario del proyecto', + 'Project settings' => 'Preferencias del proyecto', + 'Show subtasks based on the time tracking' => 'Mostrar subtareas en base al seguimiento temporal', 'Show tasks based on the creation date' => 'Mostrar tareas en base a la fecha de creación', - 'Show tasks based on the start date' => 'Mostrar tareas en base a la fecha de comienzo', + 'Show tasks based on the start date' => 'Mostrar tareas en base a la fecha de inicio', 'Subtasks time tracking' => 'Seguimiento de tiempo en subtareas', - 'User calendar view' => 'Vista de Calendario para el Usuario', - 'Automatically update the start date' => 'Actualizar automáticamente la fecha de comienzo', + 'User calendar view' => 'Vista de calendario del usuario', + 'Automatically update the start date' => 'Actualizar automáticamente la fecha de inicio', 'iCal feed' => 'Fuente iCal', 'Preferences' => 'Preferencias', 'Security' => 'Seguridad', - 'Two factor authentication disabled' => 'Autenticación de dos factores deshabilitada', - 'Two factor authentication enabled' => 'Autenticación de dos factores habilitada', + 'Two factor authentication disabled' => 'Autenticación en dos pasos deshabilitada', + 'Two factor authentication enabled' => 'Autenticación en dos pasos habilitada', 'Unable to update this user.' => 'No se puede actualizar este usuario.', 'There is no user management for private projects.' => 'No hay gestión de usuarios para proyectos privados.', 'User that will receive the email' => 'Usuario que recibirá el correo', 'Email subject' => 'Asunto del correo', 'Date' => 'Fecha', 'Add a comment log when moving the task between columns' => 'Añadir un comentario al mover la tarea entre columnas', - 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambia la categoría', + 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambie la categoría', 'Send a task by email to someone' => 'Enviar una tarea a alguien por correo', 'Reopen a task' => 'Reabrir tarea', 'Column change' => 'Cambio de columna', @@ -698,12 +698,12 @@ return array( 'Gravatar' => 'Gravatar', '%s moved the task %s to the first swimlane' => '%s movió la tarea %s a la primera calle', '%s moved the task %s to the swimlane "%s"' => '%s movió la tarea %s a la calle «%s»', - 'This report contains all subtasks information for the given date range.' => 'Este informe contiene todas la información de las subtareas para el rango proporcionado de fechas.', - 'This report contains all tasks information for the given date range.' => 'Este informe contiene todas la información de las tareas para el rango proporcionado de fechas.', + 'This report contains all subtasks information for the given date range.' => 'Este informe contiene toda la información de las subtareas para el rango de fechas proporcionado.', + 'This report contains all tasks information for the given date range.' => 'Este informe contiene toda la información de las tareas para el rango de fechas proporcionado.', 'Project activities for %s' => 'Actividades del proyecto para %s', 'view the board on Kanboard' => 'ver el tablero en Kanboard', 'The task have been moved to the first swimlane' => 'Se ha movido la tarea a la primera calle', - 'The task have been moved to another swimlane:' => 'Se ha movido la tarea a otra calle', + 'The task have been moved to another swimlane:' => 'Se ha movido la tarea a otra calle:', 'New title: %s' => 'Nuevo título: %s', 'The task is not assigned anymore' => 'La tarea ya no está asignada', 'New assignee: %s' => 'Nuevo responsable: %s', @@ -717,46 +717,46 @@ return array( 'Time spent changed: %sh' => 'Se ha cambiado el tiempo empleado: %sh', 'Time estimated changed: %sh' => 'Se ha cambiado el tiempo estimado: %sh', 'The field "%s" have been updated' => 'Se ha actualizado el campo «%s»', - 'The description has been modified:' => 'Se ha modificado la descripción', + 'The description has been modified:' => 'Se ha modificado la descripción:', 'Do you really want to close the task "%s" as well as all subtasks?' => '¿Realmente desea cerrar la tarea «%s» así como todas las subtareas?', 'I want to receive notifications for:' => 'Deseo recibir notificaciones para:', 'All tasks' => 'Todas las tareas', 'Only for tasks assigned to me' => 'Sólo para las tareas que me han sido asignadas', - 'Only for tasks created by me' => 'Sólo para las taread creadas por mí', - 'Only for tasks created by me and assigned to me' => 'Sólo para las tareas credas por mí y que me han sido asignadas', + 'Only for tasks created by me' => 'Sólo para las tareas creadas por mí', + 'Only for tasks created by me and assigned to me' => 'Sólo para las tareas creadas por mí y que me han sido asignadas', '%%Y-%%m-%%d' => '%%d/%%M/%%Y', 'Total for all columns' => 'Total para todas las columnas', 'You need at least 2 days of data to show the chart.' => 'Necesitas al menos 2 días de datos para mostrar el gráfico.', '<15m' => '<15m', '<30m' => '<30m', 'Stop timer' => 'Parar temporizador', - 'Start timer' => 'Arrancar temporizador', + 'Start timer' => 'Iniciar temporizador', 'Add project member' => 'Añadir miembro al proyecto', 'My activity stream' => 'Mi flujo de actividad', 'My calendar' => 'Mi calendario', 'Search tasks' => 'Buscar tareas', 'Reset filters' => 'Limpiar filtros', 'My tasks due tomorrow' => 'Mis tareas a entregar mañana', - 'Tasks due today' => 'Tareas a antregar hoy', - 'Tasks due tomorrow' => 'Taraes a entregar mañana', + 'Tasks due today' => 'Tareas a entregar hoy', + 'Tasks due tomorrow' => 'Tareas a entregar mañana', 'Tasks due yesterday' => 'Tareas a entregar ayer', 'Closed tasks' => 'Tareas cerradas', 'Open tasks' => 'Tareas abiertas', 'Not assigned' => 'No asignada', - 'View advanced search syntax' => 'Ver sintáxis avanzada de búsqueda', + 'View advanced search syntax' => 'Ver sintaxis de búsqueda avanzada', 'Overview' => 'Resumen', - 'Board/Calendar/List view' => 'Vista de Tablero/Calendario/Lista', + 'Board/Calendar/List view' => 'Vista de tablero/calendario/lista', 'Switch to the board view' => 'Cambiar a vista de tablero', 'Switch to the calendar view' => 'Cambiar a vista de calendario', 'Switch to the list view' => 'Cambiar a vista de lista', 'Go to the search/filter box' => 'Ir a caja de buscar/filtrar', 'There is no activity yet.' => 'Aún no hay actividades.', - 'No tasks found.' => 'No se ha hallado tarea alguna.', + 'No tasks found.' => 'No se ha encontrado ninguna tarea.', 'Keyboard shortcut: "%s"' => 'Atajo de teclado: %s', 'List' => 'Lista', 'Filter' => 'Filtro', 'Advanced search' => 'Búsqueda avanzada', - 'Example of query: ' => 'Ejemplo de query: ', + 'Example of query: ' => 'Ejemplo de consulta: ', 'Search by project: ' => 'Buscar por proyecto: ', 'Search by column: ' => 'Buscar por columna: ', 'Search by assignee: ' => 'Buscar por responsable: ', @@ -764,36 +764,36 @@ return array( 'Search by category: ' => 'Buscar por categoría: ', 'Search by description: ' => 'Buscar por descripción: ', 'Search by due date: ' => 'Buscar por fecha de entrega: ', - 'Lead and Cycle time for "%s"' => 'Plazo de Entrega y Ciclo para «%s»', + 'Lead and Cycle time for "%s"' => 'Plazo de entrega y ciclo para «%s»', 'Average time spent into each column for "%s"' => 'Tiempo medio empleado en cada columna para «%s»', 'Average time spent into each column' => 'Tiempo medio empleado en cada columna', 'Average time spent' => 'Tiempo medio empleado', 'This chart show the average time spent into each column for the last %d tasks.' => 'Esta gráfica muestra el tiempo medio empleado en cada columna para las últimas %d tareas.', - 'Average Lead and Cycle time' => 'Plazo Medio de Entrega y de Ciclo', - 'Average lead time: ' => 'Plazo Medio de entrega: ', - 'Average cycle time: ' => 'Tiempo Medio de Ciclo: ', - 'Cycle Time' => 'Tiempo de Ciclo', - 'Lead Time' => 'Plazo de Entrega', - 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Esta gráfica muestra el plazo medio de entrega y de ciclo para las %d últimas tareas transcurridas.', + 'Average Lead and Cycle time' => 'Plazo medio de entrega y de ciclo', + 'Average lead time: ' => 'Plazo medio de entrega: ', + 'Average cycle time: ' => 'Tiempo medio de ciclo: ', + 'Cycle Time' => 'Tiempo de ciclo', + 'Lead Time' => 'Plazo de entrega', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Esta gráfica muestra el plazo medio de entrega y de ciclo para las %d últimas tareas.', 'Average time into each column' => 'Tiempo medio en cada columna', 'Lead and cycle time' => 'Plazo de entrega y de ciclo', 'Lead time: ' => 'Plazo de entrega: ', - 'Cycle time: ' => 'Tiempo de Ciclo: ', + 'Cycle time: ' => 'Tiempo de ciclo: ', 'Time spent into each column' => 'Tiempo empleado en cada columna', - 'The lead time is the duration between the task creation and the completion.' => 'El plazo de entrega es la duración entre la creación de la tarea su terminación.', - 'The cycle time is the duration between the start date and the completion.' => 'El tiempo de ciclo es la duración entre la fecha de inicio y su terminación.', - 'If the task is not closed the current time is used instead of the completion date.' => 'Si la tarea no se cierra, se usa la fecha actual en lugar de la de terminación.', + 'The lead time is the duration between the task creation and the completion.' => 'El plazo de entrega es la duración entre la creación de la tarea su finalización.', + 'The cycle time is the duration between the start date and the completion.' => 'El tiempo de ciclo es la duración entre la fecha de inicio y su finalización.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Si la tarea no se cierra, se usa la fecha actual en lugar de la de finalización.', 'Set automatically the start date' => 'Poner la fecha de inicio de forma automática', 'Edit Authentication' => 'Modificar autenticación', 'Remote user' => 'Usuario remoto', - 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github', - 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la caja de edición "Desactivar formulario de ingreso", se ignoran las credenciales entradas en el formulario de ingreso.', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la opción "Desactivar formulario de ingreso", se ignoran las credenciales introducidas en el formulario de ingreso.', 'New remote user' => 'Nuevo usuario remoto', 'New local user' => 'Nuevo usuario local', - 'Default task color' => 'Color por defecto de tarea', - 'This feature does not work with all browsers.' => 'Esta característica no funciona con todos los navegadores', - 'There is no destination project available.' => 'No está disponible proyecto destino', - 'Trigger automatically subtask time tracking' => 'Disparar de forma automática seguimiento temporal de subtarea', + 'Default task color' => 'Color de la tarea por defecto', + 'This feature does not work with all browsers.' => 'Esta característica no funciona en todos los navegadores.', + 'There is no destination project available.' => 'No está disponible proyecto de destino.', + 'Trigger automatically subtask time tracking' => 'Disparar de forma automática el seguimiento temporal de subtarea', 'Include closed tasks in the cumulative flow diagram' => 'Incluir tareas cerradas en el diagrama de flujo acumulado', 'Current swimlane: %s' => 'Calle en curso: %s', 'Current column: %s' => 'Columna en curso: %s', @@ -805,20 +805,20 @@ return array( 'contributors' => 'contribuyentes', 'License:' => 'Licencia:', 'License' => 'Licencia', - 'Enter the text below' => 'Digita el texto de abajo', + 'Enter the text below' => 'Introduzca el texto a continuación', 'Gantt chart for %s' => 'Diagrama de Gantt para %s', - 'Sort by position' => 'Clasificado mediante posición', - 'Sort by date' => 'Clasificado mediante fecha', + 'Sort by position' => 'Ordenar por posición', + 'Sort by date' => 'Ordenar por fecha', 'Add task' => 'Añadir tarea', 'Start date:' => 'Fecha de inicio:', 'Due date:' => 'Fecha de entrega:', 'There is no start date or due date for this task.' => 'No hay fecha de inicio o de entrega para esta tarea.', - 'Moving or resizing a task will change the start and due date of the task.' => 'El mover o redimensionar una tarea cambiará la fecha inicio y de entrega de la misma.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Mover o redimensionar una tarea cambiará la fecha inicio y de entrega de la misma.', 'There is no task in your project.' => 'No hay tareas en su proyecto.', - 'Gantt chart' => 'Digrama de Gantt', - 'People who are project managers' => 'Usuarios que son administradores de proyecto', - 'People who are project members' => 'Usuarios que son miembros de proyecto', - 'NOK - Norwegian Krone' => 'NOK - Coronoa Noruega', + 'Gantt chart' => 'Diagrama de Gantt', + 'People who are project managers' => 'Usuarios que son administradores del proyecto', + 'People who are project members' => 'Usuarios que son miembros del proyecto', + 'NOK - Norwegian Krone' => 'NOK - Corona Noruega', 'Show this column' => 'Mostrar esta columna', 'Hide this column' => 'Ocultar esta columna', 'open file' => 'abrir fichero', @@ -826,80 +826,80 @@ return array( 'Users overview' => 'Resumen de usuarios', 'Members' => 'Miembros', 'Shared project' => 'Proyecto compartido', - 'Project managers' => 'Administradores de proyecto', + 'Project managers' => 'Administradores del proyecto', 'Gantt chart for all projects' => 'Diagrama de Gantt para todos los proyectos', 'Projects list' => 'Lista de proyectos', 'Gantt chart for this project' => 'Diagrama de Gantt para este proyecto', 'Project board' => 'Tablero del proyecto', - 'End date:' => 'Fecha final', + 'End date:' => 'Fecha de fin:', 'There is no start date or end date for this project.' => 'No existe fecha de inicio o de fin para este proyecto.', - 'Projects Gantt chart' => 'Diagramas de Gantt de los proyectos', - 'Change task color when using a specific task link' => 'Cambiar colo de la tarea al usar un enlace específico a tarea', - 'Task link creation or modification' => 'Creación o modificación de enlace a tarea', + 'Projects Gantt chart' => 'Diagrama de Gantt de los proyectos', + 'Change task color when using a specific task link' => 'Cambiar el color de la tarea al usar un enlace específico a tarea', + 'Task link creation or modification' => 'Creación o modificación del enlace a tarea', 'Milestone' => 'Hito', 'Documentation: %s' => 'Documentación: %s', - 'Switch to the Gantt chart view' => 'Conmutar a vista de diagrama de Gantt', - 'Reset the search/filter box' => 'Limpiar la caja del filtro de búsqueda', + 'Switch to the Gantt chart view' => 'Cambiar a vista de diagrama de Gantt', + 'Reset the search/filter box' => 'Limpiar el filtro de búsqueda', 'Documentation' => 'Documentación', 'Table of contents' => 'Tabla de contenido', 'Gantt' => 'Gantt', 'Author' => 'Autor', 'Version' => 'Versión', 'Plugins' => 'Plugins', - 'There is no plugin loaded.' => 'No hay ningún plugin cargado', + 'There is no plugin loaded.' => 'No hay ningún plugin cargado.', 'Set maximum column height' => 'Establecer altura máxima de la columna', 'Remove maximum column height' => 'Eliminar altura máxima de la columna', 'My notifications' => 'Mis notificaciones', 'Custom filters' => 'Filtros personalizados', 'Your custom filter have been created successfully.' => 'Tus filtros personalizados han sido creados correctamente.', 'Unable to create your custom filter.' => 'No se ha podido crear tu filtro personalizado.', - 'Custom filter removed successfully.' => 'Filtro personalizado ha sido eliminado correctamente.', - 'Unable to remove this custom filter.' => 'No se ha podido eliminar tu filtro personalizado', + 'Custom filter removed successfully.' => 'El filtro personalizado ha sido eliminado correctamente.', + 'Unable to remove this custom filter.' => 'No se ha podido eliminar tu filtro personalizado.', 'Edit custom filter' => 'Modificar filtro personalizado', 'Your custom filter have been updated successfully.' => 'Tu filtro personalizado ha sido actualizado correctamente.', - 'Unable to update custom filter.' => 'No se ha podido actualizar tu filtro personalizado', + 'Unable to update custom filter.' => 'No se ha podido actualizar tu filtro personalizado.', 'Web' => 'Web', 'New attachment on task #%d: %s' => 'Nuevo adjunto en la tarea #%d: %s', 'New comment on task #%d' => 'Nuevo comentario en la tarea #%d', 'Comment updated on task #%d' => 'Comentario actualizado en la tarea #%d', 'New subtask on task #%d' => 'Nueva subtarea en la tarea #%d', - 'Subtask updated on task #%d' => 'La subtarea en la tarea #%d ha sido actualizada', + 'Subtask updated on task #%d' => 'Subtarea actualizada en la tarea #%d', 'New task #%d: %s' => 'Nueva tarea #%d: %s', 'Task updated #%d' => 'Tarea actualizada #%d', - 'Task #%d closed' => 'Tarea #%d ha sido cerrada', - 'Task #%d opened' => 'Tarea #%d ha sido abierta', - 'Column changed for task #%d' => 'Columna para tarea #%d ha sido cambiada', + 'Task #%d closed' => 'Tarea #%d cerrada', + 'Task #%d opened' => 'Tarea #%d abierta', + 'Column changed for task #%d' => 'Columna cambiada para la tarea #%d', 'New position for task #%d' => 'Nueva posición para tarea #%d', - 'Swimlane changed for task #%d' => 'Se cambió el swimlane de la tarea #%d', - 'Assignee changed on task #%d' => 'Se cambió el asignado de la tarea #%d', + 'Swimlane changed for task #%d' => 'Se cambió la calle de la tarea #%d', + 'Assignee changed on task #%d' => 'Se cambió el responsable de la tarea #%d', '%d overdue tasks' => '%d tareas atrasadas', 'Task #%d is overdue' => 'La tarea #%d está atrasada', - 'No new notifications.' => 'No hay nuevas notificaciones', + 'No new notifications.' => 'No hay nuevas notificaciones.', 'Mark all as read' => 'Marcar todo como leído', 'Mark as read' => 'Marcar como leído', - 'Total number of tasks in this column across all swimlanes' => 'Número total de tareas en esta columna por todas las swimlanes', - 'Collapse swimlane' => 'Contraer swimlane', - 'Expand swimlane' => 'Ampliar swimlane', + 'Total number of tasks in this column across all swimlanes' => 'Número total de tareas en esta columna a través de todas las calles', + 'Collapse swimlane' => 'Contraer calle', + 'Expand swimlane' => 'Ampliar calle', 'Add a new filter' => 'Añadir nuevo filtro', 'Share with all project members' => 'Compartir con todos los miembros del proyecto', 'Shared' => 'Compartido', - 'Owner' => 'Dueño', + 'Owner' => 'Propietario', 'Unread notifications' => 'Notificaciones sin leer', - 'Notification methods:' => 'Métodos de notificación', + 'Notification methods:' => 'Métodos de notificación:', 'Import tasks from CSV file' => 'Importar tareas desde archivo CSV', 'Unable to read your file' => 'No es posible leer el archivo', - '%d task(s) have been imported successfully.' => '%d tarea(s) han sido importadas correctamente', - 'Nothing have been imported!' => 'No se ha importado nada!', + '%d task(s) have been imported successfully.' => '%d tarea(s) han sido importadas correctamente.', + 'Nothing have been imported!' => '¡No se ha importado nada!', 'Import users from CSV file' => 'Importar usuarios desde archivo CSV', - '%d user(s) have been imported successfully.' => '%d usuario(s) se han importado correctamente', + '%d user(s) have been imported successfully.' => '%d usuario(s) se han importado correctamente.', 'Comma' => 'Coma', 'Semi-colon' => 'Punto y coma', 'Tab' => 'Tabulación', - 'Vertical bar' => 'Pleca', + 'Vertical bar' => 'Barra vertical', 'Double Quote' => 'Comilla doble', - 'Single Quote' => 'Comilla sencilla', - '%s attached a file to the task #%d' => '%s adjuntó un archivo a la tarea #%d', - 'There is no column or swimlane activated in your project!' => 'No hay ninguna columna o swimlane activada en su proyecto!', + 'Single Quote' => 'Comilla simple', + '%s attached a file to the task #%d' => '%s adjuntó un archivo en la tarea #%d', + 'There is no column or swimlane activated in your project!' => '¡No hay ninguna columna o calle activada en su proyecto!', 'Append filter (instead of replacement)' => 'Añadir filtro (en vez de reemplazar)', 'Append/Replace' => 'Añadir/Reemplazar', 'Append' => 'Añadir', @@ -912,63 +912,63 @@ return array( 'CSV File' => 'Archivo CSV', 'Instructions' => 'Indicaciones', 'Your file must use the predefined CSV format' => 'Su archivo debe utilizar el formato CSV predeterminado', - 'Your file must be encoded in UTF-8' => 'Su archivo debe ser codificado en UTF-8', + 'Your file must be encoded in UTF-8' => 'Su archivo debe estar codificado en UTF-8', 'The first row must be the header' => 'La primera fila debe ser el encabezado', 'Duplicates are not verified for you' => 'Los duplicados no serán verificados', 'The due date must use the ISO format: YYYY-MM-DD' => 'La fecha de entrega debe utilizar el formato ISO: AAAA-MM-DD', 'Download CSV template' => 'Descargar plantilla CSV', - 'No external integration registered.' => 'No se ha registrado integración externa', + 'No external integration registered.' => 'No se ha registrado integración externa.', 'Duplicates are not imported' => 'Los duplicados no son importados', - 'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y contener sólo minúsculas', + 'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y en minúsculas', 'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen', - '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s', + '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo en la tarea %s', 'Link type' => 'Tipo de enlace', 'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace', - 'BAM - Konvertible Mark' => 'BAM - marco convertible', + 'BAM - Konvertible Mark' => 'BAM - Marco convertible', 'Assignee Username' => 'Nombre de usuario del responsable', 'Assignee Name' => 'Nombre del responsable', 'Groups' => 'Grupos', 'Members of %s' => 'Miembros de %s', 'New group' => 'Nuevo grupo', - 'Group created successfully.' => 'Grupo creado correctamente', - 'Unable to create your group.' => 'No es posible crear el grupo', + 'Group created successfully.' => 'Grupo creado correctamente.', + 'Unable to create your group.' => 'No es posible crear el grupo.', 'Edit group' => 'Modificar grupo', - 'Group updated successfully.' => 'Grupo actualizado correctamente', - 'Unable to update your group.' => 'No es posible actualizar el grupo', + 'Group updated successfully.' => 'Grupo actualizado correctamente.', + 'Unable to update your group.' => 'No es posible actualizar el grupo.', 'Add group member to "%s"' => 'Añadir un miembro del grupo a «%s»', - 'Group member added successfully.' => 'Miembro del grupo añadido correctamente', - 'Unable to add group member.' => 'No es posible añadir miembro del grupo', + 'Group member added successfully.' => 'Miembro del grupo añadido correctamente.', + 'Unable to add group member.' => 'No es posible añadir el miembro del grupo.', 'Remove user from group "%s"' => 'Eliminar usuario del grupo «%s»', - 'User removed successfully from this group.' => 'Usuario eliminado correctamente del grupo', - 'Unable to remove this user from the group.' => 'No es posible eliminar este usuario del grupo', + 'User removed successfully from this group.' => 'Usuario eliminado correctamente del grupo.', + 'Unable to remove this user from the group.' => 'No es posible eliminar este usuario del grupo.', 'Remove group' => 'Eliminar grupo', - 'Group removed successfully.' => 'Grupo eliminado correctamente', - 'Unable to remove this group.' => 'No es posible eliminar este grupo', + 'Group removed successfully.' => 'Grupo eliminado correctamente.', + 'Unable to remove this group.' => 'No es posible eliminar este grupo.', 'Project Permissions' => 'Permisos del proyecto', 'Manager' => 'Administrador', - 'Project Manager' => 'Administrador de proyecto', + 'Project Manager' => 'Administrador del proyecto', 'Project Member' => 'Miembro del proyecto', - 'Project Viewer' => 'Visor de proyectos', - 'Your account is locked for %d minutes' => 'Tu cuenta ha sido bloqueada por %d minuto(s)', + 'Project Viewer' => 'Observador del proyecto', + 'Your account is locked for %d minutes' => 'Tu cuenta ha sido bloqueada durante %d minutos', 'Invalid captcha' => 'CAPTCHA inválido', 'The name must be unique' => 'El nombre debe ser único', 'View all groups' => 'Ver todos los grupos', 'View group members' => 'Ver miembros del grupo', - 'There is no user available.' => 'No hay usuario disponible', - 'Do you really want to remove the user "%s" from the group "%s"?' => '¿Realmente desea eliminar el usuario "%s" del grupo «%s»?', - 'There is no group.' => 'No hay grupo', - 'External Id' => 'ID externo', - 'Add group member' => 'Añadir un miembro de grupo', + 'There is no user available.' => 'No hay usuario disponible.', + 'Do you really want to remove the user "%s" from the group "%s"?' => '¿Realmente desea eliminar el usuario «%s» del grupo «%s»?', + 'There is no group.' => 'No hay grupo.', + 'External Id' => 'Identificador externo', + 'Add group member' => 'Añadir un miembro al grupo', 'Do you really want to remove this group: "%s"?' => '¿Realmente desea eliminar este grupo: «%s»?', - 'There is no user in this group.' => 'No hay usuario en este grupo', + 'There is no user in this group.' => 'No hay usuario en este grupo.', 'Remove this user' => 'Eliminar este usuario', 'Permissions' => 'Permisos', 'Allowed Users' => 'Usuarios permitidos', - 'No user have been allowed specifically.' => 'Ningun usuario ha sido explícitamente permitido', + 'No user have been allowed specifically.' => 'Ningún usuario ha sido explícitamente permitido.', 'Role' => 'Rol', 'Enter user name...' => 'Ingresa nombre de usuario...', 'Allowed Groups' => 'Grupos permitidos', - 'No group have been allowed specifically.' => 'Ningun grupo ha sido explícitamente permitido', + 'No group have been allowed specifically.' => 'Ningún grupo ha sido explícitamente permitido.', 'Group' => 'Grupo', 'Group Name' => 'Nombre del grupo', 'Enter group name...' => 'Ingresa el nombre del grupo...', @@ -977,27 +977,27 @@ return array( 'Compare hours for "%s"' => 'Compara horas con «%s»', '%s mentioned you in the task #%d' => '%s te mencionó en la tarea #%d', '%s mentioned you in a comment on the task #%d' => '%s te mencionó en un comentario en la tarea #%d', - 'You were mentioned in the task #%d' => 'Te mencionaron en la tarea #%d', - 'You were mentioned in a comment on the task #%d' => 'Te mencionaron en un comentario en la tarea #%d', + 'You were mentioned in the task #%d' => 'Fuiste mencionado en la tarea #%d', + 'You were mentioned in a comment on the task #%d' => 'Fuiste mencionado en un comentario de la tarea #%d', 'Mentioned' => 'Mencionado', - 'Compare Estimated Time vs Actual Time' => 'Comparar Tiempo Estimado vs Tiempo Actual', + 'Compare Estimated Time vs Actual Time' => 'Comparar tiempo estimado vs tiempo actual', 'Estimated hours: ' => 'Horas estimadas: ', 'Actual hours: ' => 'Horas actuales: ', 'Hours Spent' => 'Horas gastadas', - 'Hours Estimated' => 'Hora Estimada', - 'Estimated Time' => 'Tiempo Estimado', - 'Actual Time' => 'Tiempo Actual', + 'Hours Estimated' => 'Horas estimadas', + 'Estimated Time' => 'Tiempo estimado', + 'Actual Time' => 'Tiempo actual', 'Estimated vs actual time' => 'Tiempo estimado vs real', - 'RUB - Russian Ruble' => 'RUB - rublo ruso', - 'Assign the task to the person who does the action when the column is changed' => 'Asignar la tarea a la persona que haga la acción al cambiar de columna', + 'RUB - Russian Ruble' => 'RUB - Rublo ruso', + 'Assign the task to the person who does the action when the column is changed' => 'Asignar la tarea a la persona que hace la acción al cambiar de columna', 'Close a task in a specific column' => 'Cerrar tarea en una columna especifica', 'Time-based One-time Password Algorithm' => 'Algoritmo basado en tiempo de un solo uso', - 'Two-Factor Provider: ' => 'Proveedor de autenticación de dos factores', - 'Disable two-factor authentication' => 'Deshabilitar autenticación de dos factores', - 'Enable two-factor authentication' => 'Habilitar autenticación de dos factorse', - 'There is no integration registered at the moment.' => 'No hay ninguna integración registrada por el momento', + 'Two-Factor Provider: ' => 'Proveedor de autenticación en dos pasos: ', + 'Disable two-factor authentication' => 'Deshabilitar autenticación en dos pasos', + 'Enable two-factor authentication' => 'Habilitar autenticación en dos pasos', + 'There is no integration registered at the moment.' => 'No hay ninguna integración registrada por el momento.', 'Password Reset for Kanboard' => 'Restablecimiento de contraseña para Kanboard', - 'Forgot password?' => '¿Olvidó contraseña?', + 'Forgot password?' => '¿Olvidó la contraseña?', 'Enable "Forget Password"' => 'Habilitar "olvidar contraseña"', 'Password Reset' => 'Restablecer contraseña', 'New password' => 'Nueva contraseña', @@ -1008,19 +1008,19 @@ return array( 'Creation' => 'Creación', 'Expiration' => 'Vencimiento', 'Password reset history' => 'Historial de restablecimiento de contraseña', - 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas las tareas de la columna "%s" y el swimlane «%s» se han cerrado correctamente', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas las tareas de la columna "%s" y la calle «%s» se han cerrado correctamente.', 'Do you really want to close all tasks of this column?' => '¿Realmente desea cerrar todas las tareas de esta columna?', - '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarea(s) en la columna "%s" y el swimlane «%s» será(n) cerrada(s)', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarea(s) en la columna "%s" y en la calle «%s» será(n) cerrada(s).', 'Close all tasks of this column' => 'Cerrar todas las tareas de esta columna', - 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Ningún plugin ha registrado un método de notificación para el proyecto. Aún puedes configurar notificaciones individuales en tu perfil de usuario', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Ningún plugin ha registrado un método de notificación para el proyecto. Aún puedes configurar notificaciones individuales en tu perfil de usuario.', 'My dashboard' => 'Mi tablero', 'My profile' => 'Mi perfil', - 'Project owner: ' => 'Dueño del proyecto', - 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'El identificador de proyecto es opcional y debe ser alfanumérico. Ejemplo: MIPROYECTO', - 'Project owner' => 'Dueño del proyecto', - 'Those dates are useful for the project Gantt chart.' => 'Esas fechas son útiles para el diagrama de Gantt', - 'Private projects do not have users and groups management.' => 'Proyectos privados no cuentan con gestión de usuarios y grupos', - 'There is no project member.' => 'No existe miembro del proyecto', + 'Project owner: ' => 'Propietario del proyecto: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'El identificador del proyecto es opcional y debe ser alfanumérico. Ejemplo: MIPROYECTO', + 'Project owner' => 'Propietario del proyecto', + 'Those dates are useful for the project Gantt chart.' => 'Esas fechas son útiles para el diagrama de Gantt.', + 'Private projects do not have users and groups management.' => 'Los proyectos privados no cuentan con gestión de usuarios y grupos.', + 'There is no project member.' => 'No existe miembro del proyecto.', 'Priority' => 'Prioridad', 'Task priority' => 'Prioridad de la tarea', 'General' => 'General', @@ -1028,16 +1028,16 @@ return array( 'Default priority' => 'Prioridad predeterminada', 'Lowest priority' => 'Prioridad más baja', 'Highest priority' => 'Prioridad más alta', - 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si estableces la prioridad más baja y alta como cero esta función será deshabilitada', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si estableces la prioridad más baja y más alta a cero esta función será deshabilitada.', 'Close a task when there is no activity' => 'Cerrar tarea cuando no haya actividad', 'Duration in days' => 'Duración en días', 'Send email when there is no activity on a task' => 'Enviar correo cuando no haya actividad en una tarea', - 'Unable to fetch link information.' => 'No es posible obtener información sobre el enlace', - 'Daily background job for tasks' => 'Tarea de fondo diaria para las tareas', + 'Unable to fetch link information.' => 'No es posible obtener información del enlace.', + 'Daily background job for tasks' => 'Trabajo en segundo plano diario para las tareas', 'Auto' => 'Automático', 'Related' => 'Relacionado', 'Attachment' => 'Adjunto', - 'Title not found' => 'Título no ha sido encontrado', + 'Title not found' => 'No se ha encontrado el título', 'Web Link' => 'Enlace web', 'External links' => 'Enlaces externos', 'Add external link' => 'Añadir enlace externo', @@ -1050,50 +1050,50 @@ return array( 'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...', 'URL' => 'URL', 'Internal links' => 'Enlaces internos', - 'Assign to me' => 'Asignar a mí', + 'Assign to me' => 'Asignarme a mí', 'Me' => 'Yo', 'Do not duplicate anything' => 'No duplicar nada', 'Projects management' => 'Administración de proyectos', 'Users management' => 'Administración de usuarios', 'Groups management' => 'Administración de grupos', - 'Create from another project' => 'Crear de otro proyecto', + 'Create from another project' => 'Crear a partir de otro proyecto', 'open' => 'abierto', 'closed' => 'cerrado', - 'Priority:' => 'Prioridad', - 'Reference:' => 'Referencia', + 'Priority:' => 'Prioridad:', + 'Reference:' => 'Referencia:', 'Complexity:' => 'Complejidad:', - 'Swimlane:' => 'Swimlane:', + 'Swimlane:' => 'Calle:', 'Column:' => 'Columna:', 'Position:' => 'Posición:', 'Creator:' => 'Creador:', 'Time estimated:' => 'Tiempo estimado:', '%s hours' => '%s horas', 'Time spent:' => 'Tiempo gastado:', - 'Created:' => 'Creado', - 'Modified:' => 'Modificado', - 'Completed:' => 'Terminado', - 'Started:' => 'Iniciado', - 'Moved:' => 'Movido', + 'Created:' => 'Creado:', + 'Modified:' => 'Modificado:', + 'Completed:' => 'Finalizado:', + 'Started:' => 'Iniciado:', + 'Moved:' => 'Movido:', 'Task #%d' => 'Tarea #%d', - 'Date and time format' => 'Formato de hora y fecha', + 'Date and time format' => 'Formato de fecha y hora', 'Time format' => 'Formato de hora', - 'Start date: ' => 'Fecha de inicio', - 'End date: ' => 'Fecha de terminación', - 'New due date: ' => 'Nueva fecha de entrega', - 'Start date changed: ' => 'Fecha de inicio cambiada', + 'Start date: ' => 'Fecha de inicio: ', + 'End date: ' => 'Fecha de finalización: ', + 'New due date: ' => 'Nueva fecha de entrega: ', + 'Start date changed: ' => 'Fecha de inicio cambiada: ', 'Disable private projects' => 'Deshabilitar proyectos privados', 'Do you really want to remove this custom filter: "%s"?' => '¿Realmente desea eliminar este filtro personalizado: «%s»?', - 'Remove a custom filter' => 'Eliminar filtro personalizado', - 'User activated successfully.' => 'Usuario activado correctamente', - 'Unable to enable this user.' => 'No es posible habilitar este usuario', - 'User disabled successfully.' => 'Usuario deshabilitado correctamente', - 'Unable to disable this user.' => 'No es posible deshabilitar este usuario', - 'All files have been uploaded successfully.' => 'Todos los archivos han sido subidos correctamente', - 'View uploaded files' => 'Ver archivos subidos', - 'The maximum allowed file size is %sB.' => 'El límite de tamaño de archivo permitido para subir es %sB.', - 'Choose files again' => 'Eligir archivos de nuevo', + 'Remove a custom filter' => 'Eliminar el filtro personalizado', + 'User activated successfully.' => 'Usuario activado correctamente.', + 'Unable to enable this user.' => 'No es posible habilitar este usuario.', + 'User disabled successfully.' => 'Usuario deshabilitado correctamente.', + 'Unable to disable this user.' => 'No es posible deshabilitar este usuario.', + 'All files have been uploaded successfully.' => 'Todos los archivos han sido cargados correctamente.', + 'View uploaded files' => 'Ver archivos cargados', + 'The maximum allowed file size is %sB.' => 'El tamaño máximo de archivo es %sB.', + 'Choose files again' => 'Elegir archivos de nuevo', 'Drag and drop your files here' => 'Arrastra y suelta tus archivos aquí', - 'choose files' => 'Elegir archivos', + 'choose files' => 'elegir archivos', 'View profile' => 'Ver perfil', 'Two Factor' => 'Dos factores', 'Disable user' => 'Deshabilitar usuario', @@ -1113,16 +1113,16 @@ return array( 'Change column position' => 'Cambiar posición de la columna', 'Switch to the project overview' => 'Cambiar a vista general del proyecto', 'User filters' => 'Usar filtros', - 'Category filters' => 'Categoría y filtros', - 'Upload a file' => 'Subir archivo', + 'Category filters' => 'Filtros de categoría', + 'Upload a file' => 'Cargar archivo', 'View file' => 'Ver archivo', 'Last activity' => 'Última actividad', 'Change subtask position' => 'Cambiar posición de la subtarea', 'This value must be greater than %d' => 'Este valor debe ser mayor que %d', - 'Another swimlane with the same name exists in the project' => 'Ya existe otro swimlane con el mismo nombre en el proyecto', - 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Ejemplo: http://ejemplo.kanboard.net/ (Usado para generar URLs absolutas)', - 'Actions duplicated successfully.' => 'Acción duplicada con exito.', - 'Unable to duplicate actions.' => 'No se ha podido duplicar la acción.', + 'Another swimlane with the same name exists in the project' => 'Ya existe otra calle con el mismo nombre en el proyecto', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado para generar URLs absolutas)', + 'Actions duplicated successfully.' => 'Acciones duplicadas con éxito.', + 'Unable to duplicate actions.' => 'No se han podido duplicar las acciones.', 'Add a new action' => 'Añadir una nueva acción', 'Import from another project' => 'Importar de otro proyecto', 'There is no action at the moment.' => 'No hay ninguna acción en este momento.', -- cgit v1.2.3 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 --- CONTRIBUTORS.md | 1 + doc/es_ES/calendar-configuration.markdown | 43 +++++++++++ doc/es_ES/email-configuration.markdown | 115 ++++++++++++++++++++++++++++ doc/es_ES/kanban-vs-todo-and-scrum.markdown | 38 +++++++++ 4 files changed, 197 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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 73837402..01ac3f2b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,6 +53,7 @@ Contributors: - [Jesusaplsoft](https://github.com/jesusaplsoft) - [Jesús Marín](https://github.com/alu0100502114) - [Jules Verhaeren](https://github.com/julesverhaeren) +- [JunglaCODE]https://github.com/junglaCODE) - [Karol J](https://github.com/dzudek) - [Kiswa](https://github.com/kiswa) - [Kralo](https://github.com/kralo) 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 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 95751f391f336faf82ee2402a559247aef668e72 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 23 Jun 2016 15:43:37 -0400 Subject: Fixed broken CSV export --- ChangeLog | 4 ++++ app/Controller/ExportController.php | 33 +++++++++++++++++---------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/ChangeLog b/ChangeLog index 20ffbca1..0277af69 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,10 @@ Improvements: * Make embedded documentation available in multiple languages +Bug fixes: + +* Fixed broken CSV exports + Version 1.0.30 -------------- diff --git a/app/Controller/ExportController.php b/app/Controller/ExportController.php index b2fe0ebd..27046c76 100644 --- a/app/Controller/ExportController.php +++ b/app/Controller/ExportController.php @@ -31,22 +31,23 @@ class ExportController extends BaseController $data = $this->$model->$method($project['id'], $from, $to); $this->response->withFileDownload($filename.'.csv'); $this->response->csv($data); - } + } else { - $this->response->html($this->helper->layout->project('export/'.$action, array( - 'values' => array( - 'controller' => 'ExportController', - 'action' => $action, - 'project_id' => $project['id'], - 'from' => $from, - 'to' => $to, - ), - 'errors' => array(), - 'date_format' => $this->configModel->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), - 'project' => $project, - 'title' => $page_title, - ), 'export/sidebar')); + $this->response->html($this->helper->layout->project('export/'.$action, array( + 'values' => array( + 'controller' => 'ExportController', + 'action' => $action, + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'date_format' => $this->configModel->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), + 'project' => $project, + 'title' => $page_title, + ), 'export/sidebar')); + } } /** @@ -76,7 +77,7 @@ class ExportController extends BaseController */ public function summary() { - $this->common('projectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + $this->common('projectDailyColumnStatsModel', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); } /** -- cgit v1.2.3 From d560f84b374fa1b3345dc582eddd6bb7b9138674 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 23 Jun 2016 20:26:19 -0400 Subject: Added models for tags --- app/Core/Base.php | 2 + app/Model/TagModel.php | 139 +++++++++++++++++++++++++++++++++ app/Model/TaskTagModel.php | 125 +++++++++++++++++++++++++++++ app/Schema/Mysql.php | 25 +++++- app/Schema/Postgres.php | 24 +++++- app/Schema/Sqlite.php | 24 +++++- app/ServiceProvider/ClassProvider.php | 2 + tests/units/Model/TagModelTest.php | 120 ++++++++++++++++++++++++++++ tests/units/Model/TaskTagModelTest.php | 67 ++++++++++++++++ 9 files changed, 525 insertions(+), 3 deletions(-) create mode 100644 app/Model/TagModel.php create mode 100644 app/Model/TaskTagModel.php create mode 100644 tests/units/Model/TagModelTest.php create mode 100644 tests/units/Model/TaskTagModelTest.php diff --git a/app/Core/Base.php b/app/Core/Base.php index 7b4462e2..6712cbce 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -86,6 +86,7 @@ use Pimple\Container; * @property \Kanboard\Model\SubtaskModel $subtaskModel * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel * @property \Kanboard\Model\SwimlaneModel $swimlaneModel + * @property \Kanboard\Model\TagModel $tagModel * @property \Kanboard\Model\TaskModel $taskModel * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel * @property \Kanboard\Model\TaskCreationModel $taskCreationModel @@ -96,6 +97,7 @@ use Pimple\Container; * @property \Kanboard\Model\TaskModificationModel $taskModificationModel * @property \Kanboard\Model\TaskPositionModel $taskPositionModel * @property \Kanboard\Model\TaskStatusModel $taskStatusModel + * @property \Kanboard\Model\TaskTagModel $taskTagModel * @property \Kanboard\Model\TaskMetadataModel $taskMetadataModel * @property \Kanboard\Model\TimezoneModel $timezoneModel * @property \Kanboard\Model\TransitionModel $transitionModel diff --git a/app/Model/TagModel.php b/app/Model/TagModel.php new file mode 100644 index 00000000..1be05a66 --- /dev/null +++ b/app/Model/TagModel.php @@ -0,0 +1,139 @@ +db->table(self::TABLE)->asc('name')->findAll(); + } + + /** + * Get all tags by project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAllByProject($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('name')->findAll(); + } + + /** + * Get one tag + * + * @access public + * @param integer $tag_id + * @return array|null + */ + public function getById($tag_id) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->findOne(); + } + + /** + * Get tag id from tag name + * + * @access public + * @param int $project_id + * @param string $tag + * @return integer + */ + public function getIdByName($project_id, $tag) + { + return $this->db + ->table(self::TABLE) + ->beginOr() + ->eq('project_id', 0) + ->eq('project_id', $project_id) + ->closeOr() + ->ilike('name', $tag) + ->asc('project_id') + ->findOneColumn('id'); + } + + /** + * Return tag id and create a new tag if necessary + * + * @access public + * @param int $project_id + * @param string $tag + * @return bool|int + */ + public function findOrCreateTag($project_id, $tag) + { + $tag_id = $this->getIdByName($project_id, $tag); + + if (empty($tag_id)) { + $tag_id = $this->create($project_id, $tag); + } + + return $tag_id; + } + + /** + * Add a new tag + * + * @access public + * @param int $project_id + * @param string $tag + * @return bool|int + */ + public function create($project_id, $tag) + { + return $this->db->table(self::TABLE)->persist(array( + 'project_id' => $project_id, + 'name' => $tag, + )); + } + + /** + * Update a tag + * + * @access public + * @param integer $tag_id + * @param string $tag + * @return bool + */ + public function update($tag_id, $tag) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->update(array( + 'name' => $tag, + )); + } + + /** + * Remove a tag + * + * @access public + * @param integer $tag_id + * @return bool + */ + public function remove($tag_id) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->remove(); + } +} diff --git a/app/Model/TaskTagModel.php b/app/Model/TaskTagModel.php new file mode 100644 index 00000000..74d82539 --- /dev/null +++ b/app/Model/TaskTagModel.php @@ -0,0 +1,125 @@ +db->table(TagModel::TABLE) + ->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name') + ->eq(self::TABLE.'.task_id', $task_id) + ->join(self::TABLE, 'tag_id', 'id') + ->findAll(); + } + + /** + * Get dictionary of tags + * + * @access public + * @param integer $task_id + * @return array + */ + public function getList($task_id) + { + $tags = $this->getAll($task_id); + return array_column($tags, 'name', 'id'); + } + + /** + * Add or update a list of tags to a task + * + * @access public + * @param integer $project_id + * @param integer $task_id + * @param string[] $tags + * @return boolean + */ + public function save($project_id, $task_id, array $tags) + { + $task_tags = $this->getList($task_id); + + return $this->addTags($project_id, $task_id, $task_tags, $tags) && + $this->removeTags($task_id, $task_tags, $tags); + } + + /** + * Associate a tag to a task + * + * @access public + * @param integer $task_id + * @param integer $tag_id + * @return boolean + */ + public function associate($task_id, $tag_id) + { + return $this->db->table(self::TABLE)->insert(array( + 'task_id' => $task_id, + 'tag_id' => $tag_id, + )); + } + + /** + * Dissociate a tag from a task + * + * @access public + * @param integer $task_id + * @param integer $tag_id + * @return boolean + */ + public function dissociate($task_id, $tag_id) + { + return $this->db->table(self::TABLE) + ->eq('task_id', $task_id) + ->eq('tag_id', $tag_id) + ->remove(); + } + + private function addTags($project_id, $task_id, $task_tags, $tags) + { + foreach ($tags as $tag) { + $tag_id = $this->tagModel->findOrCreateTag($project_id, $tag); + + if (! isset($task_tags[$tag_id]) && ! $this->associate($task_id, $tag_id)) { + return false; + } + } + + return true; + } + + private function removeTags($task_id, $task_tags, $tags) + { + foreach ($task_tags as $tag_id => $tag) { + if (! in_array($tag, $tags)) { + if (! $this->dissociate($task_id, $tag_id)) { + return false; + } + } + } + + return true; + } +} diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 934b063f..82ccb8c8 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,30 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 110; +const VERSION = 111; + +function version_111(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + project_id INT NOT NULL, + UNIQUE(project_id, name), + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INT NOT NULL, + tag_id INT NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_110(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 3ef49498..229cbd25 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,29 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 89; +const VERSION = 90; + +function version_90(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + project_id INTEGER NOT NULL, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) + "); +} function version_89(PDO $pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 9ded7ed9..dac348d4 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,29 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 101; +const VERSION = 102; + +function version_102(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + project_id INTEGER NOT NULL, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) + "); +} function version_101(PDO $pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 3e6efb02..778b4f9e 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -60,6 +60,7 @@ class ClassProvider implements ServiceProviderInterface 'SubtaskModel', 'SubtaskTimeTrackingModel', 'SwimlaneModel', + 'TagModel', 'TaskModel', 'TaskAnalyticModel', 'TaskCreationModel', @@ -71,6 +72,7 @@ class ClassProvider implements ServiceProviderInterface 'TaskModificationModel', 'TaskPositionModel', 'TaskStatusModel', + 'TaskTagModel', 'TaskMetadataModel', 'TimezoneModel', 'TransitionModel', diff --git a/tests/units/Model/TagModelTest.php b/tests/units/Model/TagModelTest.php new file mode 100644 index 00000000..f090ab4a --- /dev/null +++ b/tests/units/Model/TagModelTest.php @@ -0,0 +1,120 @@ +container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + $this->assertEquals(2, $tagModel->create(1, 'Tag 1')); + $this->assertEquals(3, $tagModel->create(1, 'Tag 2')); + $this->assertFalse($tagModel->create(0, 'Tag 1')); + $this->assertFalse($tagModel->create(1, 'Tag 2')); + } + + public function testGetById() + { + $tagModel = new TagModel($this->container); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + + $tag = $tagModel->getById(1); + $this->assertEquals(0, $tag['project_id']); + $this->assertEquals('Tag 1', $tag['name']); + + $tag = $tagModel->getById(3); + $this->assertEmpty($tag); + } + + public function testGetAll() + { + $tagModel = new TagModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + $this->assertEquals(2, $tagModel->create(1, 'Tag 2')); + + $tags = $tagModel->getAll(); + $this->assertCount(2, $tags); + $this->assertEquals(0, $tags[0]['project_id']); + $this->assertEquals('Tag 1', $tags[0]['name']); + + $this->assertEquals(1, $tags[1]['project_id']); + $this->assertEquals('Tag 2', $tags[1]['name']); + } + + public function testGetAllByProjectId() + { + $tagModel = new TagModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + $this->assertEquals(2, $tagModel->create(1, 'B')); + $this->assertEquals(3, $tagModel->create(1, 'A')); + + $tags = $tagModel->getAllByProject(1); + $this->assertCount(2, $tags); + $this->assertEquals(1, $tags[0]['project_id']); + $this->assertEquals('A', $tags[0]['name']); + + $this->assertEquals(1, $tags[1]['project_id']); + $this->assertEquals('B', $tags[1]['name']); + } + + public function testGetIdByName() + { + $tagModel = new TagModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + $this->assertEquals(2, $tagModel->create(1, 'Tag 1')); + $this->assertEquals(3, $tagModel->create(1, 'Tag 3')); + + $this->assertEquals(1, $tagModel->getIdByName(1, 'tag 1')); + $this->assertEquals(1, $tagModel->getIdByName(0, 'tag 1')); + $this->assertEquals(3, $tagModel->getIdByName(1, 'TaG 3')); + } + + public function testFindOrCreateTag() + { + $tagModel = new TagModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + + $this->assertEquals(2, $tagModel->findOrCreateTag(1, 'Tag 2')); + $this->assertEquals(2, $tagModel->findOrCreateTag(1, 'Tag 2')); + $this->assertEquals(1, $tagModel->findOrCreateTag(1, 'Tag 1')); + } + + public function testRemove() + { + $tagModel = new TagModel($this->container); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + + $this->assertTrue($tagModel->remove(1)); + $this->assertFalse($tagModel->remove(1)); + } + + public function testUpdate() + { + $tagModel = new TagModel($this->container); + $this->assertEquals(1, $tagModel->create(0, 'Tag 1')); + $this->assertTrue($tagModel->update(1, 'Tag Updated')); + + $tag = $tagModel->getById(1); + $this->assertEquals(0, $tag['project_id']); + $this->assertEquals('Tag Updated', $tag['name']); + } +} diff --git a/tests/units/Model/TaskTagModelTest.php b/tests/units/Model/TaskTagModelTest.php new file mode 100644 index 00000000..c08b571f --- /dev/null +++ b/tests/units/Model/TaskTagModelTest.php @@ -0,0 +1,67 @@ +container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskTagModel = new TaskTagModel($this->container); + $tagModel = new TagModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertEquals(1, $tagModel->create(0, 'My tag 1')); + $this->assertEquals(2, $tagModel->create(0, 'My tag 2')); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); + + $tags = $taskTagModel->getAll(1); + $this->assertCount(3, $tags); + + $this->assertEquals(1, $tags[0]['id']); + $this->assertEquals('My tag 1', $tags[0]['name']); + + $this->assertEquals(2, $tags[1]['id']); + $this->assertEquals('My tag 2', $tags[1]['name']); + + $this->assertEquals(3, $tags[2]['id']); + $this->assertEquals('My tag 3', $tags[2]['name']); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 3', 'My tag 1', 'My tag 4'))); + + $tags = $taskTagModel->getAll(1); + $this->assertCount(3, $tags); + + $this->assertEquals(1, $tags[0]['id']); + $this->assertEquals('My tag 1', $tags[0]['name']); + + $this->assertEquals(3, $tags[1]['id']); + $this->assertEquals('My tag 3', $tags[1]['name']); + + $this->assertEquals(4, $tags[2]['id']); + $this->assertEquals('My tag 4', $tags[2]['name']); + + $tags = $tagModel->getAll(); + $this->assertCount(4, $tags); + $this->assertEquals('My tag 1', $tags[0]['name']); + $this->assertEquals(0, $tags[0]['project_id']); + + $this->assertEquals('My tag 2', $tags[1]['name']); + $this->assertEquals(0, $tags[1]['project_id']); + + $this->assertEquals('My tag 3', $tags[2]['name']); + $this->assertEquals(1, $tags[2]['project_id']); + + $this->assertEquals('My tag 4', $tags[3]['name']); + $this->assertEquals(1, $tags[3]['project_id']); + } +} -- cgit v1.2.3 From 9e278a9370e3b651a4a545c0c0c0c256088ed187 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 24 Jun 2016 08:50:57 -0400 Subject: Use BoardFormatter to generate the board --- app/Api/BoardApi.php | 8 +- app/Controller/BoardAjaxController.php | 2 +- app/Controller/BoardViewController.php | 8 +- app/Filter/BaseFilter.php | 3 +- app/Formatter/BaseFormatter.php | 17 +- app/Formatter/BoardColumnFormatter.php | 79 +++++++ app/Formatter/BoardFormatter.php | 19 +- app/Formatter/BoardSwimlaneFormatter.php | 105 +++++++++ app/Formatter/BoardTaskFormatter.php | 80 +++++++ app/Model/BoardModel.php | 60 ------ app/Model/TaskFinderModel.php | 20 -- app/Template/board/table_column.php | 4 +- app/functions.php | 26 +++ tests/units/Formatter/BoardFormatterTest.php | 311 +++++++++++++++++++++++++++ tests/units/FunctionTest.php | 21 ++ tests/units/Model/BoardTest.php | 121 ----------- 16 files changed, 667 insertions(+), 217 deletions(-) create mode 100644 app/Formatter/BoardColumnFormatter.php create mode 100644 app/Formatter/BoardSwimlaneFormatter.php create mode 100644 app/Formatter/BoardTaskFormatter.php create mode 100644 tests/units/Formatter/BoardFormatterTest.php create mode 100644 tests/units/FunctionTest.php diff --git a/app/Api/BoardApi.php b/app/Api/BoardApi.php index aa5942af..70f21c0e 100644 --- a/app/Api/BoardApi.php +++ b/app/Api/BoardApi.php @@ -2,6 +2,8 @@ namespace Kanboard\Api; +use Kanboard\Formatter\BoardFormatter; + /** * Board API controller * @@ -13,6 +15,10 @@ class BoardApi extends BaseApi public function getBoard($project_id) { $this->checkProjectPermission($project_id); - return $this->boardModel->getBoard($project_id); + + return BoardFormatter::getInstance($this->container) + ->withProjectId($project_id) + ->withQuery($this->taskFinderModel->getExtendedQuery()) + ->format(); } } diff --git a/app/Controller/BoardAjaxController.php b/app/Controller/BoardAjaxController.php index 24914671..9b721f06 100644 --- a/app/Controller/BoardAjaxController.php +++ b/app/Controller/BoardAjaxController.php @@ -134,7 +134,7 @@ class BoardAjaxController extends BaseController 'board_highlight_period' => $this->configModel->get('board_highlight_period'), 'swimlanes' => $this->taskLexer ->build($this->userSession->getFilters($project_id)) - ->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id)) + ->format(BoardFormatter::getInstance($this->container)->withProjectId($project_id)) )); } } diff --git a/app/Controller/BoardViewController.php b/app/Controller/BoardViewController.php index 496fa995..97c99d11 100644 --- a/app/Controller/BoardViewController.php +++ b/app/Controller/BoardViewController.php @@ -30,7 +30,11 @@ class BoardViewController extends BaseController $this->response->html($this->helper->layout->app('board/view_public', array( 'project' => $project, - 'swimlanes' => $this->boardModel->getBoard($project['id']), + 'swimlanes' => BoardFormatter::getInstance($this->container) + ->withProjectId($project['id']) + ->withQuery($this->taskFinderModel->getExtendedQuery()) + ->format() + , 'title' => $project['name'], 'description' => $project['description'], 'no_layout' => true, @@ -59,7 +63,7 @@ class BoardViewController extends BaseController 'board_highlight_period' => $this->configModel->get('board_highlight_period'), 'swimlanes' => $this->taskLexer ->build($search) - ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id'])) + ->format(BoardFormatter::getInstance($this->container)->withProjectId($project['id'])) ))); } } diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php index 79a664be..e029f4e1 100644 --- a/app/Filter/BaseFilter.php +++ b/app/Filter/BaseFilter.php @@ -43,8 +43,7 @@ abstract class BaseFilter */ public static function getInstance($value = null) { - $self = new static($value); - return $self; + return new static($value); } /** diff --git a/app/Formatter/BaseFormatter.php b/app/Formatter/BaseFormatter.php index a9f0ad15..89c48437 100644 --- a/app/Formatter/BaseFormatter.php +++ b/app/Formatter/BaseFormatter.php @@ -3,8 +3,8 @@ namespace Kanboard\Formatter; use Kanboard\Core\Base; -use Kanboard\Core\Filter\FormatterInterface; use PicoDb\Table; +use Pimple\Container; /** * Class BaseFormatter @@ -22,12 +22,25 @@ abstract class BaseFormatter extends Base */ protected $query; + /** + * Get object instance + * + * @static + * @access public + * @param Container $container + * @return static + */ + public static function getInstance(Container $container) + { + return new static($container); + } + /** * Set query * * @access public * @param Table $query - * @return FormatterInterface + * @return $this */ public function withQuery(Table $query) { diff --git a/app/Formatter/BoardColumnFormatter.php b/app/Formatter/BoardColumnFormatter.php new file mode 100644 index 00000000..3d8f6e67 --- /dev/null +++ b/app/Formatter/BoardColumnFormatter.php @@ -0,0 +1,79 @@ +swimlaneId = $swimlaneId; + return $this; + } + + /** + * Set columns + * + * @access public + * @param array $columns + * @return $this + */ + public function withColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Set tasks + * + * @access public + * @param array $tasks + * @return $this + */ + public function withTasks(array $tasks) + { + $this->tasks = $tasks; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + foreach ($this->columns as &$column) { + $column['tasks'] = BoardTaskFormatter::getInstance($this->container) + ->withTasks($this->tasks) + ->withSwimlaneId($this->swimlaneId) + ->withColumnId($column['id']) + ->format(); + + $column['nb_tasks'] = count($column['tasks']); + $column['score'] = (int) array_column_sum($column['tasks'], 'score'); + } + + return $this->columns; + } +} diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php index dbc7cf21..562a97bc 100644 --- a/app/Formatter/BoardFormatter.php +++ b/app/Formatter/BoardFormatter.php @@ -28,7 +28,7 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface * @param integer $projectId * @return $this */ - public function setProjectId($projectId) + public function withProjectId($projectId) { $this->projectId = $projectId; return $this; @@ -42,15 +42,22 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface */ public function format() { + $swimlanes = $this->swimlaneModel->getSwimlanes($this->projectId); + $columns = $this->columnModel->getAll($this->projectId); + $tasks = $this->query ->eq(TaskModel::TABLE.'.project_id', $this->projectId) ->asc(TaskModel::TABLE.'.position') ->findAll(); - return $this->boardModel->getBoard($this->projectId, function ($project_id, $column_id, $swimlane_id) use ($tasks) { - return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) { - return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id; - }); - }); + if (empty($swimlanes) || empty($columns)) { + return array(); + } + + return BoardSwimlaneFormatter::getInstance($this->container) + ->withSwimlanes($swimlanes) + ->withColumns($columns) + ->withTasks($tasks) + ->format(); } } diff --git a/app/Formatter/BoardSwimlaneFormatter.php b/app/Formatter/BoardSwimlaneFormatter.php new file mode 100644 index 00000000..91b4bfd7 --- /dev/null +++ b/app/Formatter/BoardSwimlaneFormatter.php @@ -0,0 +1,105 @@ +swimlanes = $swimlanes; + return $this; + } + + /** + * Set columns + * + * @access public + * @param array $columns + * @return $this + */ + public function withColumns($columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Set tasks + * + * @access public + * @param array $tasks + * @return $this + */ + public function withTasks(array $tasks) + { + $this->tasks = $tasks; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $nb_swimlanes = count($this->swimlanes); + $nb_columns = count($this->columns); + + foreach ($this->swimlanes as &$swimlane) { + $swimlane['columns'] = BoardColumnFormatter::getInstance($this->container) + ->withSwimlaneId($swimlane['id']) + ->withColumns($this->columns) + ->withTasks($this->tasks) + ->format(); + + $swimlane['nb_swimlanes'] = $nb_swimlanes; + $swimlane['nb_columns'] = $nb_columns; + $swimlane['nb_tasks'] = array_column_sum($swimlane['columns'], 'nb_tasks'); + $swimlane['score'] = array_column_sum($swimlane['columns'], 'score'); + + $this->calculateStatsByColumnAcrossSwimlanes($swimlane['columns']); + } + + return $this->swimlanes; + } + + /** + * Calculate stats for each column acrosss all swimlanes + * + * @access protected + * @param array $columns + */ + protected function calculateStatsByColumnAcrossSwimlanes(array $columns) + { + foreach ($columns as $columnIndex => $column) { + if (! isset($this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'])) { + $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'] = 0; + $this->swimlanes[0]['columns'][$columnIndex]['column_score'] = 0; + } + + $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'] += $column['nb_tasks']; + $this->swimlanes[0]['columns'][$columnIndex]['column_score'] += $column['score']; + } + } +} diff --git a/app/Formatter/BoardTaskFormatter.php b/app/Formatter/BoardTaskFormatter.php new file mode 100644 index 00000000..d9500710 --- /dev/null +++ b/app/Formatter/BoardTaskFormatter.php @@ -0,0 +1,80 @@ +tasks = $tasks; + return $this; + } + + /** + * Set columnId + * + * @access public + * @param integer $columnId + * @return $this + */ + public function withColumnId($columnId) + { + $this->columnId = $columnId; + return $this; + } + + /** + * Set swimlaneId + * + * @access public + * @param integer $swimlaneId + * @return $this + */ + public function withSwimlaneId($swimlaneId) + { + $this->swimlaneId = $swimlaneId; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + return array_values(array_filter($this->tasks, array($this, 'filterTasks'))); + } + + /** + * Keep only tasks of the given column and swimlane + * + * @access public + * @param array $task + * @return bool + */ + public function filterTasks(array $task) + { + return $task['column_id'] == $this->columnId && $task['swimlane_id'] == $this->swimlaneId; + } +} diff --git a/app/Model/BoardModel.php b/app/Model/BoardModel.php index d2718b47..4d559936 100644 --- a/app/Model/BoardModel.php +++ b/app/Model/BoardModel.php @@ -93,66 +93,6 @@ class BoardModel extends Base return $this->boardModel->create($project_to, $columns); } - /** - * Get all tasks sorted by columns and swimlanes - * - * @access public - * @param integer $project_id - * @param callable $callback - * @return array - */ - public function getBoard($project_id, $callback = null) - { - $swimlanes = $this->swimlaneModel->getSwimlanes($project_id); - $columns = $this->columnModel->getAll($project_id); - $nb_columns = count($columns); - - for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { - $swimlanes[$i]['columns'] = $columns; - $swimlanes[$i]['nb_columns'] = $nb_columns; - $swimlanes[$i]['nb_tasks'] = 0; - $swimlanes[$i]['nb_swimlanes'] = $ilen; - - for ($j = 0; $j < $nb_columns; $j++) { - $column_id = $columns[$j]['id']; - $swimlane_id = $swimlanes[$i]['id']; - - if (! isset($swimlanes[0]['columns'][$j]['nb_column_tasks'])) { - $swimlanes[0]['columns'][$j]['nb_column_tasks'] = 0; - $swimlanes[0]['columns'][$j]['total_score'] = 0; - } - - $swimlanes[$i]['columns'][$j]['tasks'] = $callback === null ? $this->taskFinderModel->getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id) : $callback($project_id, $column_id, $swimlane_id); - $swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']); - $swimlanes[$i]['columns'][$j]['score'] = $this->getColumnSum($swimlanes[$i]['columns'][$j]['tasks'], 'score'); - $swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks']; - $swimlanes[0]['columns'][$j]['nb_column_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks']; - $swimlanes[0]['columns'][$j]['total_score'] += $swimlanes[$i]['columns'][$j]['score']; - } - } - - return $swimlanes; - } - - /** - * Calculate the sum of the defined field for a list of tasks - * - * @access public - * @param array $tasks - * @param string $field - * @return integer - */ - public function getColumnSum(array &$tasks, $field) - { - $sum = 0; - - foreach ($tasks as $task) { - $sum += $task[$field]; - } - - return $sum; - } - /** * Get the total of tasks per column * diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php index 8b636e28..0e99c407 100644 --- a/app/Model/TaskFinderModel.php +++ b/app/Model/TaskFinderModel.php @@ -152,26 +152,6 @@ class TaskFinderModel extends Base ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE); } - /** - * Get all tasks shown on the board (sorted by position) - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @param integer $swimlane_id Swimlane id - * @return array - */ - public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) - { - return $this->getExtendedQuery() - ->eq(TaskModel::TABLE.'.project_id', $project_id) - ->eq(TaskModel::TABLE.'.column_id', $column_id) - ->eq(TaskModel::TABLE.'.swimlane_id', $swimlane_id) - ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN) - ->asc(TaskModel::TABLE.'.position') - ->findAll(); - } - /** * Get all tasks for a given project and status * diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php index f7a9f6ad..6336234a 100644 --- a/app/Template/board/table_column.php +++ b/app/Template/board/table_column.php @@ -18,9 +18,9 @@ - 1 && ! empty($column['nb_column_tasks'])): ?> + 1 && ! empty($column['column_nb_tasks'])): ?> - () + () diff --git a/app/functions.php b/app/functions.php index b759763f..99431d9e 100644 --- a/app/functions.php +++ b/app/functions.php @@ -2,6 +2,32 @@ use Kanboard\Core\Translator; +/** + * Sum all values from a single column in the input array + * + * $input = [ + * ['column' => 2'], ['column' => 3'] + * ] + * + * array_column_sum($input, 'column') returns 5 + * + * @param array $input + * @param string $column + * @return double + */ +function array_column_sum(array &$input, $column) +{ + $sum = 0.0; + + foreach ($input as &$row) { + if (isset($row[$column])) { + $sum += (float) $row[$column]; + } + } + + return $sum; +} + /** * Build version number from git-archive output * diff --git a/tests/units/Formatter/BoardFormatterTest.php b/tests/units/Formatter/BoardFormatterTest.php new file mode 100644 index 00000000..02b0b518 --- /dev/null +++ b/tests/units/Formatter/BoardFormatterTest.php @@ -0,0 +1,311 @@ +container); + $swimlaneModel = new SwimlaneModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $swimlaneModel->create(array('name' => 'Swimlane 1', 'project_id' => 1))); + $this->assertEquals(2, $swimlaneModel->create(array('name' => 'Swimlane 2', 'project_id' => 1))); + + // 2 task within the same column but no score + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task 1', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task 2', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 1))); + + // 2 tasks in the same column with score + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task 3', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 1, 'score' => 4))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task 4', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 1, 'score' => 5))); + + // 1 task in 2nd column + $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task 5', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 2))); + + // tasks in same column but different swimlanes + $this->assertEquals(6, $taskCreationModel->create(array('title' => 'Task 6', 'project_id' => 1, 'swimlane_id' => 0, 'column_id' => 3, 'score' => 1))); + $this->assertEquals(7, $taskCreationModel->create(array('title' => 'Task 7', 'project_id' => 1, 'swimlane_id' => 1, 'column_id' => 3, 'score' => 2))); + $this->assertEquals(8, $taskCreationModel->create(array('title' => 'Task 8', 'project_id' => 1, 'swimlane_id' => 2, 'column_id' => 3, 'score' => 3))); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(3, $board); + + $this->assertEquals('Default swimlane', $board[0]['name']); + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals(3, $board[0]['nb_swimlanes']); + $this->assertEquals(4, $board[0]['nb_columns']); + $this->assertEquals(6, $board[0]['nb_tasks']); + $this->assertEquals(10, $board[0]['score']); + + $this->assertEquals(4, $board[0]['columns'][0]['column_nb_tasks']); + $this->assertEquals(1, $board[0]['columns'][1]['column_nb_tasks']); + $this->assertEquals(3, $board[0]['columns'][2]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][3]['column_nb_tasks']); + + $this->assertEquals(9, $board[0]['columns'][0]['column_score']); + $this->assertEquals(0, $board[0]['columns'][1]['column_score']); + $this->assertEquals(6, $board[0]['columns'][2]['column_score']); + $this->assertEquals(0, $board[0]['columns'][3]['column_score']); + + $this->assertSame(9, $board[0]['columns'][0]['score']); + $this->assertSame(0, $board[0]['columns'][1]['score']); + $this->assertSame(1, $board[0]['columns'][2]['score']); + $this->assertSame(0, $board[0]['columns'][3]['score']); + + $this->assertSame(4, $board[0]['columns'][0]['nb_tasks']); + $this->assertSame(1, $board[0]['columns'][1]['nb_tasks']); + $this->assertSame(1, $board[0]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][3]['nb_tasks']); + + $this->assertEquals('Task 1', $board[0]['columns'][0]['tasks'][0]['title']); + $this->assertEquals('Task 2', $board[0]['columns'][0]['tasks'][1]['title']); + $this->assertEquals('Task 3', $board[0]['columns'][0]['tasks'][2]['title']); + $this->assertEquals('Task 4', $board[0]['columns'][0]['tasks'][3]['title']); + $this->assertEquals('Task 5', $board[0]['columns'][1]['tasks'][0]['title']); + $this->assertEquals('Task 6', $board[0]['columns'][2]['tasks'][0]['title']); + + $this->assertEquals('Swimlane 1', $board[1]['name']); + $this->assertCount(4, $board[1]['columns']); + $this->assertEquals(3, $board[1]['nb_swimlanes']); + $this->assertEquals(4, $board[1]['nb_columns']); + $this->assertEquals(1, $board[1]['nb_tasks']); + $this->assertEquals(2, $board[1]['score']); + + $this->assertSame(0, $board[1]['columns'][0]['score']); + $this->assertSame(0, $board[1]['columns'][1]['score']); + $this->assertSame(2, $board[1]['columns'][2]['score']); + $this->assertSame(0, $board[1]['columns'][3]['score']); + + $this->assertSame(0, $board[1]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][1]['nb_tasks']); + $this->assertSame(1, $board[1]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][3]['nb_tasks']); + + $this->assertEquals('Task 7', $board[1]['columns'][2]['tasks'][0]['title']); + + $this->assertEquals('Swimlane 2', $board[2]['name']); + $this->assertCount(4, $board[2]['columns']); + $this->assertEquals(3, $board[2]['nb_swimlanes']); + $this->assertEquals(4, $board[2]['nb_columns']); + $this->assertEquals(1, $board[2]['nb_tasks']); + $this->assertEquals(3, $board[2]['score']); + + $this->assertSame(0, $board[2]['columns'][0]['score']); + $this->assertSame(0, $board[2]['columns'][1]['score']); + $this->assertSame(3, $board[2]['columns'][2]['score']); + $this->assertSame(0, $board[2]['columns'][3]['score']); + + $this->assertSame(0, $board[2]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[2]['columns'][1]['nb_tasks']); + $this->assertSame(1, $board[2]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[2]['columns'][3]['nb_tasks']); + + $this->assertEquals('Task 8', $board[2]['columns'][2]['tasks'][0]['title']); + } + + public function testFormatWithoutDefaultSwimlane() + { + $projectModel = new ProjectModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertTrue($swimlaneModel->disableDefault(1)); + $this->assertEquals(1, $swimlaneModel->create(array('name' => 'Swimlane 1', 'project_id' => 1))); + $this->assertEquals(2, $swimlaneModel->create(array('name' => 'Swimlane 2', 'project_id' => 1))); + + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task 1', 'project_id' => 1, 'swimlane_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task 2', 'project_id' => 1, 'swimlane_id' => 2, 'column_id' => 2))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task 3', 'project_id' => 1, 'swimlane_id' => 1, 'column_id' => 2, 'score' => 1))); + $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task 4', 'project_id' => 1, 'swimlane_id' => 2, 'column_id' => 1))); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(2, $board); + + $this->assertEquals('Swimlane 1', $board[0]['name']); + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals(2, $board[0]['nb_swimlanes']); + $this->assertEquals(4, $board[0]['nb_columns']); + $this->assertEquals(2, $board[0]['nb_tasks']); + $this->assertEquals(1, $board[0]['score']); + + $this->assertEquals(2, $board[0]['columns'][0]['column_nb_tasks']); + $this->assertEquals(2, $board[0]['columns'][1]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][2]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][3]['column_nb_tasks']); + + $this->assertEquals(0, $board[0]['columns'][0]['column_score']); + $this->assertEquals(1, $board[0]['columns'][1]['column_score']); + $this->assertEquals(0, $board[0]['columns'][2]['column_score']); + $this->assertEquals(0, $board[0]['columns'][3]['column_score']); + + $this->assertSame(0, $board[0]['columns'][0]['score']); + $this->assertSame(1, $board[0]['columns'][1]['score']); + $this->assertSame(0, $board[0]['columns'][2]['score']); + $this->assertSame(0, $board[0]['columns'][3]['score']); + + $this->assertSame(1, $board[0]['columns'][0]['nb_tasks']); + $this->assertSame(1, $board[0]['columns'][1]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][3]['nb_tasks']); + + $this->assertEquals('Task 1', $board[0]['columns'][0]['tasks'][0]['title']); + $this->assertEquals('Task 3', $board[0]['columns'][1]['tasks'][0]['title']); + + $this->assertEquals('Swimlane 2', $board[1]['name']); + $this->assertCount(4, $board[1]['columns']); + $this->assertEquals(2, $board[1]['nb_swimlanes']); + $this->assertEquals(4, $board[1]['nb_columns']); + $this->assertEquals(2, $board[1]['nb_tasks']); + $this->assertEquals(0, $board[1]['score']); + + $this->assertSame(0, $board[1]['columns'][0]['score']); + $this->assertSame(0, $board[1]['columns'][1]['score']); + $this->assertSame(0, $board[1]['columns'][2]['score']); + $this->assertSame(0, $board[1]['columns'][3]['score']); + + $this->assertSame(1, $board[1]['columns'][0]['nb_tasks']); + $this->assertSame(1, $board[1]['columns'][1]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][3]['nb_tasks']); + + $this->assertEquals('Task 4', $board[1]['columns'][0]['tasks'][0]['title']); + $this->assertEquals('Task 2', $board[1]['columns'][1]['tasks'][0]['title']); + } + + public function testFormatWithoutSwimlane() + { + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertTrue($swimlaneModel->disableDefault(1)); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(0, $board); + } + + public function testFormatWithoutColumn() + { + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $columnModel = new ColumnModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertTrue($columnModel->remove(1)); + $this->assertTrue($columnModel->remove(2)); + $this->assertTrue($columnModel->remove(3)); + $this->assertTrue($columnModel->remove(4)); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(0, $board); + } + + public function testFormatWithoutTask() + { + $projectModel = new ProjectModel($this->container); + $swimlaneModel = new SwimlaneModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $swimlaneModel->create(array('name' => 'Swimlane 1', 'project_id' => 1))); + $this->assertEquals(2, $swimlaneModel->create(array('name' => 'Swimlane 2', 'project_id' => 1))); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(3, $board); + + $this->assertEquals('Default swimlane', $board[0]['name']); + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals(3, $board[0]['nb_swimlanes']); + $this->assertEquals(4, $board[0]['nb_columns']); + $this->assertEquals(0, $board[0]['nb_tasks']); + $this->assertEquals(0, $board[0]['score']); + + $this->assertEquals(0, $board[0]['columns'][0]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][1]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][2]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][3]['column_nb_tasks']); + + $this->assertEquals(0, $board[0]['columns'][0]['column_score']); + $this->assertEquals(0, $board[0]['columns'][1]['column_score']); + $this->assertEquals(0, $board[0]['columns'][2]['column_score']); + $this->assertEquals(0, $board[0]['columns'][3]['column_score']); + + $this->assertSame(0, $board[0]['columns'][0]['score']); + $this->assertSame(0, $board[0]['columns'][1]['score']); + $this->assertSame(0, $board[0]['columns'][2]['score']); + $this->assertSame(0, $board[0]['columns'][3]['score']); + + $this->assertSame(0, $board[0]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][1]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][3]['nb_tasks']); + + $this->assertEquals('Swimlane 1', $board[1]['name']); + $this->assertCount(4, $board[1]['columns']); + $this->assertEquals(3, $board[1]['nb_swimlanes']); + $this->assertEquals(4, $board[1]['nb_columns']); + $this->assertEquals(0, $board[1]['nb_tasks']); + $this->assertEquals(0, $board[1]['score']); + + $this->assertSame(0, $board[1]['columns'][0]['score']); + $this->assertSame(0, $board[1]['columns'][1]['score']); + $this->assertSame(0, $board[1]['columns'][2]['score']); + $this->assertSame(0, $board[1]['columns'][3]['score']); + + $this->assertSame(0, $board[1]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][1]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[1]['columns'][3]['nb_tasks']); + + $this->assertEquals('Swimlane 2', $board[2]['name']); + $this->assertCount(4, $board[2]['columns']); + $this->assertEquals(3, $board[2]['nb_swimlanes']); + $this->assertEquals(4, $board[2]['nb_columns']); + $this->assertEquals(0, $board[2]['nb_tasks']); + $this->assertEquals(0, $board[2]['score']); + + $this->assertSame(0, $board[2]['columns'][0]['score']); + $this->assertSame(0, $board[2]['columns'][1]['score']); + $this->assertSame(0, $board[2]['columns'][2]['score']); + $this->assertSame(0, $board[2]['columns'][3]['score']); + + $this->assertSame(0, $board[2]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[2]['columns'][1]['nb_tasks']); + $this->assertSame(0, $board[2]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[2]['columns'][3]['nb_tasks']); + } +} diff --git a/tests/units/FunctionTest.php b/tests/units/FunctionTest.php new file mode 100644 index 00000000..72895845 --- /dev/null +++ b/tests/units/FunctionTest.php @@ -0,0 +1,21 @@ + 123 + ), + array( + 'my_column' => 456.7 + ), + array() + ); + + $this->assertSame(579.7, array_column_sum($input, 'my_column')); + } +} diff --git a/tests/units/Model/BoardTest.php b/tests/units/Model/BoardTest.php index 80587d89..9f540c63 100644 --- a/tests/units/Model/BoardTest.php +++ b/tests/units/Model/BoardTest.php @@ -3,12 +3,8 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\ProjectModel; -use Kanboard\Model\BoardModel; use Kanboard\Model\ColumnModel; use Kanboard\Model\ConfigModel; -use Kanboard\Model\TaskCreationModel; -use Kanboard\Model\TaskFinderModel; -use Kanboard\Model\SwimlaneModel; class BoardTest extends Base { @@ -45,121 +41,4 @@ class BoardTest extends Base $this->assertEquals('column #1', $columns[5]); $this->assertEquals('column #2', $columns[6]); } - - public function testGetBoard() - { - $p = new ProjectModel($this->container); - $b = new BoardModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - - $board = $b->getBoard(1); - $this->assertNotEmpty($board); - $this->assertEquals(1, count($board)); - $this->assertEquals(6, count($board[0])); - $this->assertArrayHasKey('name', $board[0]); - $this->assertArrayHasKey('nb_tasks', $board[0]); - $this->assertArrayHasKey('columns', $board[0]); - $this->assertArrayHasKey('tasks', $board[0]['columns'][2]); - $this->assertArrayHasKey('nb_tasks', $board[0]['columns'][2]); - $this->assertArrayHasKey('title', $board[0]['columns'][2]); - $this->assertArrayHasKey('nb_column_tasks', $board[0]['columns'][0]); - $this->assertArrayHasKey('total_score', $board[0]['columns'][0]); - } - - public function testGetBoardWithSwimlane() - { - $b = new BoardModel($this->container); - $tc = new TaskCreationModel($this->container); - $tf = new TaskFinderModel($this->container); - $p = new ProjectModel($this->container); - $s = new SwimlaneModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'test 1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 3))); - $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1))); - $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 3))); - $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 4, 'score' => 2))); - $this->assertEquals(6, $tc->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 4, 'score' => 3, 'swimlane_id' => 1))); - - $board = $b->getBoard(1); - $this->assertNotEmpty($board); - $this->assertEquals(2, count($board)); - $this->assertEquals(6, count($board[0])); - $this->assertArrayHasKey('name', $board[0]); - $this->assertArrayHasKey('nb_tasks', $board[0]); - $this->assertArrayHasKey('columns', $board[0]); - $this->assertArrayHasKey('tasks', $board[0]['columns'][2]); - $this->assertArrayHasKey('nb_tasks', $board[0]['columns'][2]); - $this->assertArrayHasKey('title', $board[0]['columns'][2]); - $this->assertArrayHasKey('nb_column_tasks', $board[0]['columns'][0]); - $this->assertArrayNotHasKey('nb_column_tasks', $board[1]['columns'][0]); - $this->assertArrayNotHasKey('total_score', $board[1]['columns'][0]); - $this->assertArrayHasKey('score', $board[0]['columns'][3]); - $this->assertArrayHasKey('total_score', $board[0]['columns'][3]); - $this->assertEquals(2, $board[0]['columns'][3]['score']); - $this->assertEquals(5, $board[0]['columns'][3]['total_score']); - - $task = $tf->getById(1); - $this->assertEquals(1, $task['id']); - $this->assertEquals(1, $task['column_id']); - $this->assertEquals(1, $task['position']); - $this->assertEquals(0, $task['swimlane_id']); - $this->assertEquals(1, $board[0]['columns'][0]['tasks'][0]['id']); - $this->assertEquals(1, $board[0]['columns'][0]['tasks'][0]['column_id']); - $this->assertEquals(1, $board[0]['columns'][0]['tasks'][0]['position']); - $this->assertEquals(0, $board[0]['columns'][0]['tasks'][0]['swimlane_id']); - - $task = $tf->getById(2); - $this->assertEquals(2, $task['id']); - $this->assertEquals(3, $task['column_id']); - $this->assertEquals(1, $task['position']); - $this->assertEquals(0, $task['swimlane_id']); - $this->assertEquals(2, $board[0]['columns'][2]['tasks'][0]['id']); - $this->assertEquals(3, $board[0]['columns'][2]['tasks'][0]['column_id']); - $this->assertEquals(1, $board[0]['columns'][2]['tasks'][0]['position']); - $this->assertEquals(0, $board[0]['columns'][2]['tasks'][0]['swimlane_id']); - - $task = $tf->getById(3); - $this->assertEquals(3, $task['id']); - $this->assertEquals(2, $task['column_id']); - $this->assertEquals(1, $task['position']); - $this->assertEquals(1, $task['swimlane_id']); - $this->assertEquals(3, $board[1]['columns'][1]['tasks'][0]['id']); - $this->assertEquals(2, $board[1]['columns'][1]['tasks'][0]['column_id']); - $this->assertEquals(1, $board[1]['columns'][1]['tasks'][0]['position']); - $this->assertEquals(1, $board[1]['columns'][1]['tasks'][0]['swimlane_id']); - - $task = $tf->getById(4); - $this->assertEquals(4, $task['id']); - $this->assertEquals(3, $task['column_id']); - $this->assertEquals(2, $task['position']); - $this->assertEquals(0, $task['swimlane_id']); - $this->assertEquals(4, $board[0]['columns'][2]['tasks'][1]['id']); - $this->assertEquals(3, $board[0]['columns'][2]['tasks'][1]['column_id']); - $this->assertEquals(2, $board[0]['columns'][2]['tasks'][1]['position']); - $this->assertEquals(0, $board[0]['columns'][2]['tasks'][1]['swimlane_id']); - - $task = $tf->getById(5); - $this->assertEquals(5, $task['id']); - $this->assertEquals(4, $task['column_id']); - $this->assertEquals(1, $task['position']); - $this->assertEquals(0, $task['swimlane_id']); - $this->assertEquals(5, $board[0]['columns'][3]['tasks'][0]['id']); - $this->assertEquals(4, $board[0]['columns'][3]['tasks'][0]['column_id']); - $this->assertEquals(1, $board[0]['columns'][3]['tasks'][0]['position']); - $this->assertEquals(0, $board[0]['columns'][3]['tasks'][0]['swimlane_id']); - - $task = $tf->getById(6); - $this->assertEquals(6, $task['id']); - $this->assertEquals(4, $task['column_id']); - $this->assertEquals(1, $task['position']); - $this->assertEquals(1, $task['swimlane_id']); - $this->assertEquals(6, $board[1]['columns'][3]['tasks'][0]['id']); - $this->assertEquals(4, $board[1]['columns'][3]['tasks'][0]['column_id']); - $this->assertEquals(1, $board[1]['columns'][3]['tasks'][0]['position']); - $this->assertEquals(1, $board[1]['columns'][3]['tasks'][0]['swimlane_id']); - } } -- cgit v1.2.3 From 700b4e8f0265e4eabd7a7c0eb6a06088d50554fe Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 24 Jun 2016 10:05:45 -0400 Subject: Associate tags to tasks in BoardFormatter --- app/Formatter/BoardColumnFormatter.php | 15 ++++ app/Formatter/BoardFormatter.php | 5 +- app/Formatter/BoardSwimlaneFormatter.php | 15 ++++ app/Formatter/BoardTaskFormatter.php | 22 +++++- app/Model/TaskTagModel.php | 61 +++++++++++++--- app/functions.php | 52 +++++++++++++- tests/units/Formatter/BoardFormatterTest.php | 81 +++++++++++++++++++++ tests/units/FunctionTest.php | 102 +++++++++++++++++++++++++++ tests/units/Model/TaskTagModelTest.php | 50 ++++++++++++- 9 files changed, 386 insertions(+), 17 deletions(-) diff --git a/app/Formatter/BoardColumnFormatter.php b/app/Formatter/BoardColumnFormatter.php index 3d8f6e67..d49a577a 100644 --- a/app/Formatter/BoardColumnFormatter.php +++ b/app/Formatter/BoardColumnFormatter.php @@ -15,6 +15,7 @@ class BoardColumnFormatter extends BaseFormatter implements FormatterInterface protected $swimlaneId = 0; protected $columns = array(); protected $tasks = array(); + protected $tags = array(); /** * Set swimlaneId @@ -55,6 +56,19 @@ class BoardColumnFormatter extends BaseFormatter implements FormatterInterface return $this; } + /** + * Set tags + * + * @access public + * @param array $tags + * @return $this + */ + public function withTags(array $tags) + { + $this->tags = $tags; + return $this; + } + /** * Apply formatter * @@ -66,6 +80,7 @@ class BoardColumnFormatter extends BaseFormatter implements FormatterInterface foreach ($this->columns as &$column) { $column['tasks'] = BoardTaskFormatter::getInstance($this->container) ->withTasks($this->tasks) + ->withTags($this->tags) ->withSwimlaneId($this->swimlaneId) ->withColumnId($column['id']) ->format(); diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php index 562a97bc..350dde6c 100644 --- a/app/Formatter/BoardFormatter.php +++ b/app/Formatter/BoardFormatter.php @@ -44,12 +44,14 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface { $swimlanes = $this->swimlaneModel->getSwimlanes($this->projectId); $columns = $this->columnModel->getAll($this->projectId); - $tasks = $this->query ->eq(TaskModel::TABLE.'.project_id', $this->projectId) ->asc(TaskModel::TABLE.'.position') ->findAll(); + $task_ids = array_column($tasks, 'id'); + $tags = $this->taskTagModel->getTagsByTasks($task_ids); + if (empty($swimlanes) || empty($columns)) { return array(); } @@ -58,6 +60,7 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface ->withSwimlanes($swimlanes) ->withColumns($columns) ->withTasks($tasks) + ->withTags($tags) ->format(); } } diff --git a/app/Formatter/BoardSwimlaneFormatter.php b/app/Formatter/BoardSwimlaneFormatter.php index 91b4bfd7..c2abb444 100644 --- a/app/Formatter/BoardSwimlaneFormatter.php +++ b/app/Formatter/BoardSwimlaneFormatter.php @@ -15,6 +15,7 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface protected $swimlanes = array(); protected $columns = array(); protected $tasks = array(); + protected $tags = array(); /** * Set swimlanes @@ -55,6 +56,19 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface return $this; } + /** + * Set tags + * + * @access public + * @param array $tags + * @return $this + */ + public function withTags(array $tags) + { + $this->tags = $tags; + return $this; + } + /** * Apply formatter * @@ -71,6 +85,7 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface ->withSwimlaneId($swimlane['id']) ->withColumns($this->columns) ->withTasks($this->tasks) + ->withTags($this->tags) ->format(); $swimlane['nb_swimlanes'] = $nb_swimlanes; diff --git a/app/Formatter/BoardTaskFormatter.php b/app/Formatter/BoardTaskFormatter.php index d9500710..3bf171b1 100644 --- a/app/Formatter/BoardTaskFormatter.php +++ b/app/Formatter/BoardTaskFormatter.php @@ -13,9 +13,23 @@ use Kanboard\Core\Filter\FormatterInterface; class BoardTaskFormatter extends BaseFormatter implements FormatterInterface { protected $tasks = array(); + protected $tags = array(); protected $columnId = 0; protected $swimlaneId = 0; + /** + * Set tags + * + * @access public + * @param array $tags + * @return $this + */ + public function withTags(array $tags) + { + $this->tags = $tags; + return $this; + } + /** * Set tasks * @@ -63,17 +77,19 @@ class BoardTaskFormatter extends BaseFormatter implements FormatterInterface */ public function format() { - return array_values(array_filter($this->tasks, array($this, 'filterTasks'))); + $tasks = array_values(array_filter($this->tasks, array($this, 'filterTasks'))); + array_merge_relation($tasks, $this->tags, 'tags', 'id'); + return $tasks; } /** * Keep only tasks of the given column and swimlane * - * @access public + * @access protected * @param array $task * @return bool */ - public function filterTasks(array $task) + protected function filterTasks(array $task) { return $task['column_id'] == $this->columnId && $task['swimlane_id'] == $this->swimlaneId; } diff --git a/app/Model/TaskTagModel.php b/app/Model/TaskTagModel.php index 74d82539..3dd1dd88 100644 --- a/app/Model/TaskTagModel.php +++ b/app/Model/TaskTagModel.php @@ -26,7 +26,7 @@ class TaskTagModel extends Base * @param integer $task_id * @return array */ - public function getAll($task_id) + public function getTagsByTask($task_id) { return $this->db->table(TagModel::TABLE) ->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name') @@ -35,6 +35,28 @@ class TaskTagModel extends Base ->findAll(); } + /** + * Get all tags associated to a list of tasks + * + * @access public + * @param integer[] $task_ids + * @return array + */ + public function getTagsByTasks($task_ids) + { + if (empty($task_ids)) { + return array(); + } + + $tags = $this->db->table(TagModel::TABLE) + ->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name', self::TABLE.'.task_id') + ->in(self::TABLE.'.task_id', $task_ids) + ->join(self::TABLE, 'tag_id', 'id') + ->findAll(); + + return array_column_index($tags, 'task_id'); + } + /** * Get dictionary of tags * @@ -44,7 +66,7 @@ class TaskTagModel extends Base */ public function getList($task_id) { - $tags = $this->getAll($task_id); + $tags = $this->getTagsByTask($task_id); return array_column($tags, 'name', 'id'); } @@ -61,8 +83,8 @@ class TaskTagModel extends Base { $task_tags = $this->getList($task_id); - return $this->addTags($project_id, $task_id, $task_tags, $tags) && - $this->removeTags($task_id, $task_tags, $tags); + return $this->associateTags($project_id, $task_id, $task_tags, $tags) && + $this->dissociateTags($task_id, $task_tags, $tags); } /** @@ -73,7 +95,7 @@ class TaskTagModel extends Base * @param integer $tag_id * @return boolean */ - public function associate($task_id, $tag_id) + public function associateTag($task_id, $tag_id) { return $this->db->table(self::TABLE)->insert(array( 'task_id' => $task_id, @@ -89,7 +111,7 @@ class TaskTagModel extends Base * @param integer $tag_id * @return boolean */ - public function dissociate($task_id, $tag_id) + public function dissociateTag($task_id, $tag_id) { return $this->db->table(self::TABLE) ->eq('task_id', $task_id) @@ -97,12 +119,22 @@ class TaskTagModel extends Base ->remove(); } - private function addTags($project_id, $task_id, $task_tags, $tags) + /** + * Associate missing tags + * + * @access protected + * @param integer $project_id + * @param integer $task_id + * @param array $task_tags + * @param array $tags + * @return bool + */ + protected function associateTags($project_id, $task_id, $task_tags, $tags) { foreach ($tags as $tag) { $tag_id = $this->tagModel->findOrCreateTag($project_id, $tag); - if (! isset($task_tags[$tag_id]) && ! $this->associate($task_id, $tag_id)) { + if (! isset($task_tags[$tag_id]) && ! $this->associateTag($task_id, $tag_id)) { return false; } } @@ -110,11 +142,20 @@ class TaskTagModel extends Base return true; } - private function removeTags($task_id, $task_tags, $tags) + /** + * Dissociate removed tags + * + * @access protected + * @param integer $task_id + * @param array $task_tags + * @param array $tags + * @return bool + */ + protected function dissociateTags($task_id, $task_tags, $tags) { foreach ($task_tags as $tag_id => $tag) { if (! in_array($tag, $tags)) { - if (! $this->dissociate($task_id, $tag_id)) { + if (! $this->dissociateTag($task_id, $tag_id)) { return false; } } diff --git a/app/functions.php b/app/functions.php index 99431d9e..eaf33a52 100644 --- a/app/functions.php +++ b/app/functions.php @@ -2,11 +2,61 @@ use Kanboard\Core\Translator; +/** + * Associate another dict to a dict based on a common key + * + * @param array $input + * @param array $relations + * @param string $relation + * @param string $column + */ +function array_merge_relation(array &$input, array &$relations, $relation, $column) +{ + foreach ($input as &$row) { + if (isset($row[$column]) && isset($relations[$row[$column]])) { + $row[$relation] = $relations[$row[$column]]; + } else { + $row[$relation] = array(); + } + } +} + +/** + * Create indexed array from a list of dict + * + * $input = [ + * ['k1' => 1, 'k2' => 2], ['k1' => 3, 'k2' => 4], ['k1' => 2, 'k2' => 5] + * ] + * + * array_column_index($input, 'k1') will returns: + * + * [ + * 1 => [['k1' => 1, 'k2' => 2], ['k1' => 2, 'k2' => 5]], + * 3 => [['k1' => 3, 'k2' => 4]], + * ] + * + * @param array $input + * @param string $column + * @return array + */ +function array_column_index(array &$input, $column) +{ + $result = array(); + + foreach ($input as &$row) { + if (isset($row[$column])) { + $result[$row[$column]][] = $row; + } + } + + return $result; +} + /** * Sum all values from a single column in the input array * * $input = [ - * ['column' => 2'], ['column' => 3'] + * ['column' => 2], ['column' => 3] * ] * * array_column_sum($input, 'column') returns 5 diff --git a/tests/units/Formatter/BoardFormatterTest.php b/tests/units/Formatter/BoardFormatterTest.php index 02b0b518..c107eaf5 100644 --- a/tests/units/Formatter/BoardFormatterTest.php +++ b/tests/units/Formatter/BoardFormatterTest.php @@ -6,6 +6,7 @@ use Kanboard\Model\ProjectModel; use Kanboard\Model\SwimlaneModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; +use Kanboard\Model\TaskTagModel; require_once __DIR__.'/../Base.php'; @@ -308,4 +309,84 @@ class BoardFormatterTest extends Base $this->assertSame(0, $board[2]['columns'][2]['nb_tasks']); $this->assertSame(0, $board[2]['columns'][3]['nb_tasks']); } + + public function testFormatWithTags() + { + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($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->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test2', 'column_id' => 3))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test3'))); + + $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2'))); + $this->assertTrue($taskTagModel->save(1, 2, array('My tag 3'))); + + $board = BoardFormatter::getInstance($this->container) + ->withQuery($taskFinderModel->getExtendedQuery()) + ->withProjectId(1) + ->format(); + + $this->assertCount(1, $board); + + $this->assertEquals('Default swimlane', $board[0]['name']); + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals(1, $board[0]['nb_swimlanes']); + $this->assertEquals(4, $board[0]['nb_columns']); + $this->assertEquals(3, $board[0]['nb_tasks']); + $this->assertEquals(0, $board[0]['score']); + + $this->assertEquals(2, $board[0]['columns'][0]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][1]['column_nb_tasks']); + $this->assertEquals(1, $board[0]['columns'][2]['column_nb_tasks']); + $this->assertEquals(0, $board[0]['columns'][3]['column_nb_tasks']); + + $this->assertEquals(0, $board[0]['columns'][0]['column_score']); + $this->assertEquals(0, $board[0]['columns'][1]['column_score']); + $this->assertEquals(0, $board[0]['columns'][2]['column_score']); + $this->assertEquals(0, $board[0]['columns'][3]['column_score']); + + $this->assertSame(0, $board[0]['columns'][0]['score']); + $this->assertSame(0, $board[0]['columns'][1]['score']); + $this->assertSame(0, $board[0]['columns'][2]['score']); + $this->assertSame(0, $board[0]['columns'][3]['score']); + + $this->assertSame(2, $board[0]['columns'][0]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][1]['nb_tasks']); + $this->assertSame(1, $board[0]['columns'][2]['nb_tasks']); + $this->assertSame(0, $board[0]['columns'][3]['nb_tasks']); + + $this->assertEquals('test1', $board[0]['columns'][0]['tasks'][0]['title']); + $this->assertEquals('test3', $board[0]['columns'][0]['tasks'][1]['title']); + $this->assertEquals('test2', $board[0]['columns'][2]['tasks'][0]['title']); + + $expected = array( + array( + 'id' => 1, + 'name' => 'My tag 1', + 'task_id' => 1, + ), + array( + 'id' => 2, + 'name' => 'My tag 2', + 'task_id' => 1, + ), + ); + + $this->assertEquals($expected, $board[0]['columns'][0]['tasks'][0]['tags']); + $this->assertEquals(array(), $board[0]['columns'][0]['tasks'][1]['tags']); + + $expected = array( + array( + 'id' => 3, + 'name' => 'My tag 3', + 'task_id' => 2, + ), + ); + + $this->assertEquals($expected, $board[0]['columns'][2]['tasks'][0]['tags']); + } } diff --git a/tests/units/FunctionTest.php b/tests/units/FunctionTest.php index 72895845..1c5f971d 100644 --- a/tests/units/FunctionTest.php +++ b/tests/units/FunctionTest.php @@ -18,4 +18,106 @@ class FunctionTest extends Base $this->assertSame(579.7, array_column_sum($input, 'my_column')); } + + public function testArrayColumnIndex() + { + $input = array( + array( + 'k1' => 11, + 'k2' => 22, + ), + array( + 'k1' => 11, + 'k2' => 55, + ), + array( + 'k1' => 33, + 'k2' => 44, + ), + array() + ); + + $expected = array( + 11 => array( + array( + 'k1' => 11, + 'k2' => 22, + ), + array( + 'k1' => 11, + 'k2' => 55, + ) + ), + 33 => array( + array( + 'k1' => 33, + 'k2' => 44, + ) + ) + ); + + $this->assertSame($expected, array_column_index($input, 'k1')); + } + + public function testArrayMergeRelation() + { + $relations = array( + 88 => array( + 'id' => 123, + 'value' => 'test1', + ), + 99 => array( + 'id' => 456, + 'value' => 'test2', + ), + 55 => array() + ); + + $input = array( + array(), + array( + 'task_id' => 88, + 'title' => 'task1' + ), + array( + 'task_id' => 99, + 'title' => 'task2' + ), + array( + 'task_id' => 11, + 'title' => 'task3' + ) + ); + + $expected = array( + array( + 'my_relation' => array(), + ), + array( + 'task_id' => 88, + 'title' => 'task1', + 'my_relation' => array( + 'id' => 123, + 'value' => 'test1', + ), + ), + array( + 'task_id' => 99, + 'title' => 'task2', + 'my_relation' => array( + 'id' => 456, + 'value' => 'test2', + ), + ), + array( + 'task_id' => 11, + 'title' => 'task3', + 'my_relation' => array(), + ) + ); + + array_merge_relation($input, $relations, 'my_relation', 'task_id'); + + $this->assertSame($expected, $input); + } } diff --git a/tests/units/Model/TaskTagModelTest.php b/tests/units/Model/TaskTagModelTest.php index c08b571f..819f55b8 100644 --- a/tests/units/Model/TaskTagModelTest.php +++ b/tests/units/Model/TaskTagModelTest.php @@ -24,7 +24,7 @@ class TaskTagModelTest extends Base $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3'))); - $tags = $taskTagModel->getAll(1); + $tags = $taskTagModel->getTagsByTask(1); $this->assertCount(3, $tags); $this->assertEquals(1, $tags[0]['id']); @@ -38,7 +38,7 @@ class TaskTagModelTest extends Base $this->assertTrue($taskTagModel->save(1, 1, array('My tag 3', 'My tag 1', 'My tag 4'))); - $tags = $taskTagModel->getAll(1); + $tags = $taskTagModel->getTagsByTask(1); $this->assertCount(3, $tags); $this->assertEquals(1, $tags[0]['id']); @@ -64,4 +64,50 @@ class TaskTagModelTest extends Base $this->assertEquals('My tag 4', $tags[3]['name']); $this->assertEquals(1, $tags[3]['project_id']); } + + public function testGetTagsForTasks() + { + $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->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'))); + + $tags = $taskTagModel->getTagsByTasks(array(1, 2, 3)); + + $expected = array( + 1 => array( + array( + 'id' => 1, + 'name' => 'My tag 1', + 'task_id' => 1 + ), + array( + 'id' => 2, + 'name' => 'My tag 2', + 'task_id' => 1 + ), + array( + 'id' => 3, + 'name' => 'My tag 3', + 'task_id' => 1 + ), + ), + 2 => array( + array( + 'id' => 3, + 'name' => 'My tag 3', + 'task_id' => 2, + ) + ) + ); + + $this->assertEquals($expected, $tags); + } } -- 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 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 18cb7ad0a4a96be63030f5207b74a195c8b6cd6c Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 24 Jun 2016 15:43:34 -0400 Subject: Expose tags to the user interface (first prototype) --- app/Controller/TaskModificationController.php | 1 + app/Controller/TaskViewController.php | 2 + app/Helper/TaskHelper.php | 28 +++ app/Model/TagModel.php | 18 ++ app/Model/TaskCreationModel.php | 14 +- app/Model/TaskModificationModel.php | 5 + app/Template/board/task_footer.php | 10 + app/Template/task/details.php | 259 ++++++++++++++------------ app/Template/task/public.php | 7 +- app/Template/task/show.php | 1 + app/Template/task_creation/show.php | 4 +- app/Template/task_gantt_creation/show.php | 5 +- app/Template/task_modification/edit_task.php | 4 +- assets/css/app.min.css | 2 +- assets/css/print.min.css | 2 +- assets/css/src/board.css | 13 -- assets/css/src/form.css | 10 +- assets/css/src/task.css | 31 +++ assets/css/vendor.min.css | 2 + assets/js/app.min.js | 4 +- assets/js/src/App.js | 7 + assets/js/src/Popover.js | 1 + assets/js/vendor.min.js | 3 + bower.json | 3 +- gulpfile.js | 2 + tests/units/Filter/TaskTagFilterTest.php | 5 +- 26 files changed, 286 insertions(+), 157 deletions(-) diff --git a/app/Controller/TaskModificationController.php b/app/Controller/TaskModificationController.php index f9c63c12..d55a7193 100644 --- a/app/Controller/TaskModificationController.php +++ b/app/Controller/TaskModificationController.php @@ -98,6 +98,7 @@ class TaskModificationController extends BaseController 'values' => $values, 'errors' => $errors, 'task' => $task, + 'tags' => $this->taskTagModel->getList($task['id']), 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']), 'colors_list' => $this->colorModel->getList(), 'categories_list' => $this->categoryModel->getList($task['project_id']), diff --git a/app/Controller/TaskViewController.php b/app/Controller/TaskViewController.php index bd1e86ae..f40f8bea 100644 --- a/app/Controller/TaskViewController.php +++ b/app/Controller/TaskViewController.php @@ -45,6 +45,7 @@ class TaskViewController extends BaseController 'task' => $task, 'columns_list' => $this->columnModel->getList($task['project_id']), 'colors_list' => $this->colorModel->getList(), + 'tags' => $this->taskTagModel->getList($task['id']), 'title' => $task['title'], 'no_layout' => true, 'auto_refresh' => true, @@ -82,6 +83,7 @@ class TaskViewController extends BaseController 'internal_links' => $this->taskLinkModel->getAllGroupedByLabel($task['id']), 'external_links' => $this->taskExternalLinkModel->getAll($task['id']), 'link_label_list' => $this->linkModel->getList(0, false), + 'tags' => $this->taskTagModel->getList($task['id']), ))); } diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php index e33438d6..f272059d 100644 --- a/app/Helper/TaskHelper.php +++ b/app/Helper/TaskHelper.php @@ -40,6 +40,34 @@ class TaskHelper extends Base return $this->taskModel->getRecurrenceBasedateList(); } + public function selectTitle(array $values, array $errors) + { + $html = $this->helper->form->label(t('Title'), 'title'); + $html .= $this->helper->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large'); + return $html; + } + + public function selectTags(array $project, array $tags = array()) + { + $options = $this->tagModel->getAssignableList($project['id']); + + $html = $this->helper->form->label(t('Tags'), 'tags[]'); + $html .= ''; + + return $html; + } + public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array()) { $attributes = array_merge(array('tabindex="3"'), $attributes); diff --git a/app/Model/TagModel.php b/app/Model/TagModel.php index 1be05a66..8eb5e5ba 100644 --- a/app/Model/TagModel.php +++ b/app/Model/TagModel.php @@ -42,6 +42,24 @@ class TagModel extends Base return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('name')->findAll(); } + /** + * Get assignable tags for a project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAssignableList($project_id) + { + return $this->db->hashtable(self::TABLE) + ->beginOr() + ->eq('project_id', $project_id) + ->eq('project_id', 0) + ->closeOr() + ->asc('name') + ->getAll('id', 'name'); + } + /** * Get one tag * diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index 3800f831..fa2d32c6 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -22,11 +22,13 @@ class TaskCreationModel extends Base */ public function create(array $values) { - if (! $this->projectModel->exists($values['project_id'])) { - return 0; - } - $position = empty($values['position']) ? 0 : $values['position']; + $tags = array(); + + if (isset($values['tags'])) { + $tags = $values['tags']; + unset($values['tags']); + } $this->prepare($values); $task_id = $this->db->table(TaskModel::TABLE)->persist($values); @@ -36,6 +38,10 @@ class TaskCreationModel extends Base $this->taskPositionModel->movePosition($values['project_id'], $task_id, $values['column_id'], $position, $values['swimlane_id'], false); } + if (! empty($tags)) { + $this->taskTagModel->save($values['project_id'], $task_id, $tags); + } + $this->fireEvents($task_id, $values); } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index 762af2c5..0fc3617e 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -85,6 +85,11 @@ class TaskModificationModel extends Base */ public function prepare(array &$values) { + if (isset($values['tags'])) { + $this->taskTagModel->save($values['project_id'], $values['id'], $values['tags']); + unset($values['tags']); + } + $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php index f6cbff70..37d13605 100644 --- a/app/Template/board/task_footer.php +++ b/app/Template/board/task_footer.php @@ -18,6 +18,16 @@ + +
+
    + +
  • text->e($tag['name']) ?>
  • + +
+
+ +
diff --git a/app/Template/task/details.php b/app/Template/task/details.php index fe2bba67..695957f9 100644 --- a/app/Template/task/details.php +++ b/app/Template/task/details.php @@ -4,146 +4,157 @@ hook->render('template:task:details:top', array('task' => $task)) ?>
-
-
    -
  • - - - - - - - - -
  • -
  • - -
  • - +
    +
    +
    • - text->e($task['reference']) ?> + + + + + + + +
    • - -
    • - text->e($task['score']) ?> +
    • - - -
    • - - url->link(t('Public link'), 'TaskViewController', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> -
    • - - -
    • - - url->link(t('Back to the board'), 'BoardViewController', 'readonly', array('token' => $project['token'])) ?> -
    • - -
    • + +
    • + text->e($task['reference']) ?> +
    • + + +
    • + text->e($task['score']) ?> +
    • + + +
    • + + url->link(t('Public link'), 'TaskViewController', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> +
    • + + +
    • + + url->link(t('Back to the board'), 'BoardViewController', 'readonly', array('token' => $project['token'])) ?> +
    • + +
    • - hook->render('template:task:details:first-column', array('task' => $task)) ?> -
    -
    -
    -
      - + hook->render('template:task:details:first-column', array('task' => $task)) ?> +
    +
    +
    +
      + +
    • + + text->e($task['category_name']) ?> +
    • + + +
    • + + text->e($task['swimlane_name']) ?> +
    • +
    • - - text->e($task['category_name']) ?> + + text->e($task['column_title']) ?>
    • - -
    • - - text->e($task['swimlane_name']) ?> + +
    • - -
    • - - text->e($task['column_title']) ?> -
    • -
    • - - -
    • - hook->render('template:task:details:second-column', array('task' => $task)) ?> -
    -
    -
    -
      -
    • - - - - text->e($task['assignee_name'] ?: $task['assignee_username']) ?> - - + hook->render('template:task:details:second-column', array('task' => $task)) ?> +
    +
    +
    +
      +
    • + + + + text->e($task['assignee_name'] ?: $task['assignee_username']) ?> + + + + +
    • + +
    • + + text->e($task['creator_name'] ?: $task['creator_username']) ?> +
    • + + +
    • + + dt->date($task['date_due']) ?> +
    • + + +
    • + + +
    • - - - +
    • - - text->e($task['creator_name'] ?: $task['creator_username']) ?> + +
    • - - -
    • - - dt->date($task['date_due']) ?> -
    • - - -
    • - - -
    • - - -
    • - - -
    • - + - hook->render('template:task:details:third-column', array('task' => $task)) ?> -
    -
    -
    -
      -
    • - - dt->datetime($task['date_creation']) ?> -
    • -
    • - - dt->datetime($task['date_modification']) ?> -
    • - -
    • - - dt->datetime($task['date_completed']) ?> -
    • - - -
    • - - dt->datetime($task['date_started']) ?> -
    • - - -
    • - - dt->datetime($task['date_moved']) ?> -
    • - + hook->render('template:task:details:third-column', array('task' => $task)) ?> +
    +
    +
    +
      +
    • + + dt->datetime($task['date_creation']) ?> +
    • +
    • + + dt->datetime($task['date_modification']) ?> +
    • + +
    • + + dt->datetime($task['date_completed']) ?> +
    • + + +
    • + + dt->datetime($task['date_started']) ?> +
    • + + +
    • + + dt->datetime($task['date_moved']) ?> +
    • + - hook->render('template:task:details:fourth-column', array('task' => $task)) ?> -
    + hook->render('template:task:details:fourth-column', array('task' => $task)) ?> +
+
+ +
+
    + +
  • text->e($tag) ?>
  • + +
+
+
diff --git a/app/Template/task/public.php b/app/Template/task/public.php index 94782163..b8405ff7 100644 --- a/app/Template/task/public.php +++ b/app/Template/task/public.php @@ -1,5 +1,10 @@
- render('task/details', array('task' => $task, 'project' => $project, 'editable' => false)) ?> + render('task/details', array( + 'task' => $task, + 'tags' => $tags, + 'project' => $project, + 'editable' => false, + )) ?> render('task/description', array( 'task' => $task, diff --git a/app/Template/task/show.php b/app/Template/task/show.php index 2b54eea8..80786715 100644 --- a/app/Template/task/show.php +++ b/app/Template/task/show.php @@ -2,6 +2,7 @@ render('task/details', array( 'task' => $task, + 'tags' => $tags, 'project' => $project, 'editable' => $this->user->hasProjectAccess('TaskModificationController', 'edit', $project['id']), )) ?> diff --git a/app/Template/task_creation/show.php b/app/Template/task_creation/show.php index 7bebbfe9..cd752eba 100644 --- a/app/Template/task_creation/show.php +++ b/app/Template/task_creation/show.php @@ -7,8 +7,7 @@ form->csrf() ?>
- form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> + task->selectTitle($values, $errors) ?> form->label(t('Description'), 'description') ?> form->textarea( @@ -23,6 +22,7 @@ 'markdown-editor' ) ?> + task->selectTags($project) ?> render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> diff --git a/app/Template/task_gantt_creation/show.php b/app/Template/task_gantt_creation/show.php index 683bc8c8..d1bfa67c 100644 --- a/app/Template/task_gantt_creation/show.php +++ b/app/Template/task_gantt_creation/show.php @@ -8,12 +8,11 @@ form->hidden('position', $values) ?>
- form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> + task->selectTitle($values, $errors) ?> form->label(t('Description'), 'description') ?> form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"'), 'markdown-editor') ?> - + task->selectTags($project) ?> render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?>
diff --git a/app/Template/task_modification/edit_task.php b/app/Template/task_modification/edit_task.php index 0707fd9a..d8f18743 100644 --- a/app/Template/task_modification/edit_task.php +++ b/app/Template/task_modification/edit_task.php @@ -7,8 +7,8 @@ form->hidden('project_id', $values) ?>
- form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?> + task->selectTitle($values, $errors) ?> + task->selectTags($project, $tags) ?> task->selectAssignee($users_list, $values, $errors) ?> task->selectCategory($categories_list, $values, $errors) ?> task->selectPriority($project, $values) ?> diff --git a/assets/css/app.min.css b/assets/css/app.min.css index 7d093952..49630a00 100644 --- a/assets/css/app.min.css +++ b/assets/css/app.min.css @@ -1 +1 @@ -a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.btn,.color-square,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.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,.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}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;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:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.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-date,input.form-datetime{width:150px}input.form-input-large{width:400px}.form-column{float:left;margin-right:3%;max-width:50%;min-width:40%}.form-column ul{margin-top:15px}.form-clear{clear:both;padding-top:20px;padding-bottom:10px}.form-login{width:350px;margin:8% auto 0}.form-column li,.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.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-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.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;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.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-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:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px;display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,.9)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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%}#main .confirm{max-width:700px;font-size:1.1em}.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;font-size:.95em;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;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:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#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:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}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:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;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:#666}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 silver;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:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.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{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.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,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;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}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file +a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.btn,.color-square,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.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,.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}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:3px;margin-bottom:10px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;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:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.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-date,input.form-datetime{width:150px}input.form-input-large{width:400px}.form-column{float:left;margin-right:3%;max-width:50%;min-width:40%}.form-column ul{margin-top:15px}.form-clear{clear:both;padding-top:20px;padding-bottom:10px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.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-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.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;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.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-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:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,.9)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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%}#main .confirm{max-width:700px;font-size:1.1em}.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;font-size:.95em;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;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:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#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:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}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:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;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:#666}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 silver;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:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.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:flex;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{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.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,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;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}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file diff --git a/assets/css/print.min.css b/assets/css/print.min.css index 5041beb8..a5b4407e 100644 --- a/assets/css/print.min.css +++ b/assets/css/print.min.css @@ -1 +1 @@ -a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.color-square,.task-board-change-assignee{cursor:pointer}.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,.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}.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-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}td.board-column-task-collapsed{font-weight:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;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}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-board-avatars{text-align:right;float:right}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px;display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,.9)}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file +a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.color-square,.task-board-change-assignee{cursor:pointer}.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,.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}.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-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}td.board-column-task-collapsed{font-weight:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;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}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.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{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{min-height:35px}.color-square{display:inline-block;width:30px;height:30px;margin-right:5px;margin-bottom:5px;border:1px solid #000}.color-square:hover{border-style:dotted}div.color-square-selected{border-width:2px;width:28px;height:28px;box-shadow:3px 2px 10px 0 rgba(180,180,180,.9)}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file diff --git a/assets/css/src/board.css b/assets/css/src/board.css index 586093b8..95e04108 100644 --- a/assets/css/src/board.css +++ b/assets/css/src/board.css @@ -157,16 +157,3 @@ div.draggable-item-selected { float: left; padding-right: 5px; } - -/* board saving state */ -.task-board-saving-state { - opacity: 0.3; -} - -.task-board-saving-icon { - position: absolute; - margin: auto; - width: 100%; - text-align: center; - color: #000; -} diff --git a/assets/css/src/form.css b/assets/css/src/form.css index fd778c88..27c19835 100644 --- a/assets/css/src/form.css +++ b/assets/css/src/form.css @@ -61,6 +61,15 @@ select:focus { outline: 0; } +.tag-autocomplete { + width: 400px; +} + +span.select2-container { + margin-top: 3px; + margin-bottom: 10px; +} + ::-webkit-input-placeholder { color: #ddd; padding-top: 2px; @@ -169,7 +178,6 @@ input.form-input-large { margin-top: 8%; } -.form-column li, .form-login li { margin-left: 25px; line-height: 25px; diff --git a/assets/css/src/task.css b/assets/css/src/task.css index 2a5f1e97..c134809c 100644 --- a/assets/css/src/task.css +++ b/assets/css/src/task.css @@ -53,6 +53,19 @@ div.task-board-status-closed { text-overflow: ellipsis; } +/* board saving state */ +.task-board-saving-state { + opacity: 0.3; +} + +.task-board-saving-icon { + position: absolute; + margin: auto; + width: 100%; + text-align: center; + color: #000; +} + /* title one the card */ .task-board-title { font-size: 1.15em; @@ -83,6 +96,21 @@ div.task-board-status-closed { opacity: 0.6; } +/* tags list */ +.task-tags li { + display: inline; + margin: 0; + margin-right: 4px; + padding: 2px; + color: #666; + border: 1px solid #666; + border-radius: 2px; +} + +.task-summary-container .task-tags { + margin-top: 10px; +} + /* avatars on the card */ .task-board-avatars { text-align: right; @@ -181,6 +209,9 @@ span.task-board-age-column { border: 2px solid #000; border-radius: 8px; padding: 15px; +} + +.task-summary-columns { display: -webkit-flex; display: flex; -webkit-flex-direction: row; diff --git a/assets/css/vendor.min.css b/assets/css/vendor.min.css index d5b238de..edd97352 100644 --- a/assets/css/vendor.min.css +++ b/assets/css/vendor.min.css @@ -459,6 +459,8 @@ This file is generated by `grunt build`, do not edit it by hand. } /* @end */ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} + /*! * FullCalendar v2.7.1 Stylesheet * Docs & License: http://fullcalendar.io/ diff --git a/assets/js/app.min.js b/assets/js/app.min.js index 075b6489..7f894359 100644 --- a/assets/js/app.min.js +++ b/assets/js/app.min.js @@ -1,2 +1,2 @@ -"use strict";var Kanboard={};Kanboard.Accordion=function(t){this.app=t},Kanboard.Accordion.prototype.listen=function(){$(document).on("click",".accordion-toggle",function(t){var e=$(this).parents(".accordion-section");t.preventDefault(),e.hasClass("accordion-collapsed")?(e.find(".accordion-content").show(),e.removeClass("accordion-collapsed")):(e.find(".accordion-content").hide(),e.addClass("accordion-collapsed"))})},Kanboard.App=function(){this.controllers={}},Kanboard.App.prototype.get=function(t){return this.controllers[t]},Kanboard.App.prototype.execute=function(){for(var t in Kanboard)if("App"!==t){var e=new Kanboard[t](this);this.controllers[t]=e,"function"==typeof e.execute&&e.execute(),"function"==typeof e.listen&&e.listen(),"function"==typeof e.focus&&e.focus(),"function"==typeof e.keyboardShortcuts&&e.keyboardShortcuts()}this.focus(),this.chosen(),this.keyboardShortcuts(),this.datePicker(),this.autoComplete()},Kanboard.App.prototype.keyboardShortcuts=function(){var t=this;Mousetrap.bindGlobal("mod+enter",function(){var e=$("form");1==e.length?e.submit():e.length>1&&("INPUT"===document.activeElement.tagName||"TEXTAREA"===document.activeElement.tagName?$(document.activeElement).parents("form").submit():t.get("Popover").isOpen()&&$("#popover-container form").submit())}),Mousetrap.bind("b",function(t){t.preventDefault(),$("#board-selector").trigger("chosen:open")}),Mousetrap.bindGlobal("esc",function(){t.get("Popover").close(),t.get("Dropdown").close()}),Mousetrap.bind("?",function(){t.get("Popover").open($("body").data("keyboard-shortcut-url"))})},Kanboard.App.prototype.focus=function(){$(document).on("focus",".auto-select",function(){$(this).select()}),$(document).on("mouseup",".auto-select",function(t){t.preventDefault()})},Kanboard.App.prototype.chosen=function(){$(".chosen-select").each(function(){var t=$(this).data("search-threshold");void 0===t&&(t=10),$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:t})}),$(".select-auto-redirect").change(function(){var t=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(t,$(this).val())})},Kanboard.App.prototype.datePicker=function(){var t=$("body"),e=t.data("js-date-format"),a=t.data("js-time-format"),o=t.data("js-lang");$.datepicker.setDefaults($.datepicker.regional[o]),$.timepicker.setDefaults($.timepicker.regional[o]),$(".form-date").datepicker({showOtherMonths:!0,selectOtherMonths:!0,dateFormat:e,constrainInput:!1}),$(".form-datetime").datetimepicker({dateFormat:e,timeFormat:a,constrainInput:!1})},Kanboard.App.prototype.autoComplete=function(){$(".autocomplete").each(function(){var t=$(this),e=t.data("dst-field"),a=t.data("dst-extra-field");""==$("#form-"+e).val()&&t.parent().find("button[type=submit]").attr("disabled","disabled"),t.autocomplete({source:t.data("search-url"),minLength:1,select:function(o,n){$("input[name="+e+"]").val(n.item.id),a&&$("input[name="+a+"]").val(n.item[a]),t.parent().find("button[type=submit]").removeAttr("disabled")}})})},Kanboard.App.prototype.hasId=function(t){return!!document.getElementById(t)},Kanboard.App.prototype.showLoadingIcon=function(){$("body").append(' ')},Kanboard.App.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()},Kanboard.App.prototype.formatDuration=function(t){return t>=86400?Math.round(t/86400)+"d":t>=3600?Math.round(t/3600)+"h":t>=60?Math.round(t/60)+"m":t+"s"},Kanboard.App.prototype.isVisible=function(){var t="";return"undefined"!=typeof document.hidden?t="visibilityState":"undefined"!=typeof document.mozHidden?t="mozVisibilityState":"undefined"!=typeof document.msHidden?t="msVisibilityState":"undefined"!=typeof document.webkitHidden&&(t="webkitVisibilityState"),""!=t?"visible"==document[t]:!0},Kanboard.AvgTimeColumnChart=function(t){this.app=t},Kanboard.AvgTimeColumnChart.prototype.execute=function(){this.app.hasId("analytic-avg-time-column")&&this.show()},Kanboard.AvgTimeColumnChart.prototype.show=function(){var t=$("#chart"),e=t.data("metrics"),a=[t.data("label")],o=[];for(var n in e)a.push(e[n].average),o.push(e[n].title);c3.generate({data:{columns:[a],type:"bar"},bar:{width:{ratio:.5}},axis:{x:{type:"category",categories:o},y:{tick:{format:this.app.formatDuration}}},legend:{show:!1}})},Kanboard.BoardCollapsedMode=function(t){this.app=t},Kanboard.BoardCollapsedMode.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("s",function(){t.toggle()})},Kanboard.BoardCollapsedMode.prototype.toggle=function(){var t=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(e){$(".filter-display-mode").toggle(),t.app.get("BoardDragAndDrop").refresh(e)}})},Kanboard.BoardColumnScrolling=function(t){this.app=t},Kanboard.BoardColumnScrolling.prototype.execute=function(){this.app.hasId("board")&&(this.render(),$(window).on("load",this.render),$(window).resize(this.render))},Kanboard.BoardColumnScrolling.prototype.listen=function(){var t=this;$(document).on("click",".filter-toggle-height",function(e){e.preventDefault(),t.toggle()})},Kanboard.BoardColumnScrolling.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardColumnScrolling.prototype.toggle=function(){var t=localStorage.getItem("column_scroll");void 0==t&&(t=1),localStorage.setItem("column_scroll",0==t?1:0),this.render()},Kanboard.BoardColumnScrolling.prototype.render=function(){var t=$(".board-task-list"),e=$(".board-rotation-wrapper"),a=$(".filter-max-height"),o=$(".filter-min-height");if(0==localStorage.getItem("column_scroll")){var n=80;a.show(),o.hide(),e.css("min-height",""),t.each(function(){var t=$(this).height();t>n&&(n=t)}),t.css("min-height",n),t.css("height","")}else if(a.hide(),o.show(),$(".board-swimlane").length>1)t.each(function(){$(this).height()>500?$(this).css("height",500):($(this).css("min-height",320),e.css("min-height",320))});else{var n=$(window).height()-170;t.css("height",n),e.css("min-height",n)}},Kanboard.BoardColumnView=function(t){this.app=t},Kanboard.BoardColumnView.prototype.execute=function(){this.app.hasId("board")&&this.render()},Kanboard.BoardColumnView.prototype.listen=function(){var t=this;$(document).on("click",".board-toggle-column-view",function(){t.toggle($(this).data("column-id"))})},Kanboard.BoardColumnView.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardColumnView.prototype.render=function(){var t=this;$(".board-column-header").each(function(){var e=$(this).data("column-id");localStorage.getItem("hidden_column_"+e)&&t.hideColumn(e)})},Kanboard.BoardColumnView.prototype.toggle=function(t){localStorage.getItem("hidden_column_"+t)?this.showColumn(t):this.hideColumn(t)},Kanboard.BoardColumnView.prototype.hideColumn=function(t){$(".board-column-"+t+" .board-column-expanded").hide(),$(".board-column-"+t+" .board-column-collapsed").show(),$(".board-column-header-"+t+" .board-column-expanded").hide(),$(".board-column-header-"+t+" .board-column-collapsed").show(),$(".board-column-header-"+t).each(function(){$(this).removeClass("board-column-compact"),$(this).addClass("board-column-header-collapsed")}),$(".board-column-"+t).each(function(){$(this).addClass("board-column-task-collapsed")}),$(".board-column-"+t+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+t).height())}),localStorage.setItem("hidden_column_"+t,1)},Kanboard.BoardColumnView.prototype.showColumn=function(t){$(".board-column-"+t+" .board-column-expanded").show(),$(".board-column-"+t+" .board-column-collapsed").hide(),$(".board-column-header-"+t+" .board-column-expanded").show(),$(".board-column-header-"+t+" .board-column-collapsed").hide(),$(".board-column-header-"+t).removeClass("board-column-header-collapsed"),$(".board-column-"+t).removeClass("board-column-task-collapsed"),0==localStorage.getItem("horizontal_scroll")&&$(".board-column-header-"+t).addClass("board-column-compact"),localStorage.removeItem("hidden_column_"+t)},Kanboard.BoardHorizontalScrolling=function(t){this.app=t},Kanboard.BoardHorizontalScrolling.prototype.execute=function(){this.app.hasId("board")&&this.render()},Kanboard.BoardHorizontalScrolling.prototype.listen=function(){var t=this;$(document).on("click",".filter-toggle-scrolling",function(e){e.preventDefault(),t.toggle()})},Kanboard.BoardHorizontalScrolling.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("c",function(){t.toggle()})},Kanboard.BoardHorizontalScrolling.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardHorizontalScrolling.prototype.toggle=function(){var t=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",0==t?1:0),this.render()},Kanboard.BoardHorizontalScrolling.prototype.render=function(){0==localStorage.getItem("horizontal_scroll")?($(".filter-wide").show(),$(".filter-compact").hide(),$("#board-container").addClass("board-container-compact"),$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")):($(".filter-wide").hide(),$(".filter-compact").show(),$("#board-container").removeClass("board-container-compact"),$("#board th").removeClass("board-column-compact"))},Kanboard.BoardPolling=function(t){this.app=t},Kanboard.BoardPolling.prototype.execute=function(){if(this.app.hasId("board")){var t=parseInt($("#board").attr("data-check-interval"));t>0&&window.setInterval(this.check.bind(this),1e3*t)}},Kanboard.BoardPolling.prototype.check=function(){if(this.app.isVisible()&&!this.app.get("BoardDragAndDrop").savingInProgress){var t=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:$("#board").data("check-url"),statusCode:{200:function(e){t.app.get("BoardDragAndDrop").refresh(e)},304:function(){t.app.hideLoadingIcon()}}})}},Kanboard.BoardTask=function(t){this.app=t},Kanboard.BoardTask.prototype.listen=function(){var t=this;$(document).on("click",".task-board-change-assignee",function(e){e.preventDefault(),e.stopPropagation(),t.app.get("Popover").open($(this).data("url"))}),$(document).on("click",".task-board",function(t){"A"!=t.target.tagName&&"IMG"!=t.target.tagName&&(window.location=$(this).data("task-url"))})},Kanboard.BoardTask.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("n",function(){t.app.get("Popover").open($("#board").data("task-creation-url"))})},Kanboard.BurndownChart=function(t){this.app=t},Kanboard.BurndownChart.prototype.execute=function(){this.app.hasId("analytic-burndown")&&this.show()},Kanboard.BurndownChart.prototype.show=function(){for(var t=$("#chart"),e=t.data("metrics"),a=[[t.data("label-total")]],o=[],n=d3.time.format("%Y-%m-%d"),r=d3.time.format(t.data("date-format")),i=0;i0&&(void 0==a[0][i]&&a[0].push(0),a[0][i]+=e[i][s]),0==s&&o.push(r(n.parse(e[i][s]))));c3.generate({data:{columns:a},axis:{x:{type:"category",categories:o}}})},Kanboard.Calendar=function(t){this.app=t},Kanboard.Calendar.prototype.execute=function(){var t=$("#calendar");1==t.length&&this.show(t)},Kanboard.Calendar.prototype.show=function(t){t.fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(e){$.ajax({cache:!1,url:t.data("save-url"),contentType:"application/json",type:"POST",processData:!1,data:JSON.stringify({task_id:e.id,date_due:e.start.format()})})},viewRender:function(){var e=t.data("check-url"),a={start:t.fullCalendar("getView").start.format(),end:t.fullCalendar("getView").end.format()};for(var o in a)e+="&"+o+"="+a[o];$.getJSON(e,function(e){t.fullCalendar("removeEvents"),t.fullCalendar("addEventSource",e),t.fullCalendar("rerenderEvents")})}})},Kanboard.Column=function(t){this.app=t},Kanboard.Column.prototype.listen=function(){this.dragAndDrop()},Kanboard.Column.prototype.dragAndDrop=function(){var t=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")}),$(".columns-table tbody").sortable({forcePlaceholderSize:!0,handle:"td:first i",helper:function(t,e){return e.children().each(function(){$(this).width($(this).width())}),e},stop:function(e,a){var o=a.item;o.removeClass("draggable-item-selected"),t.savePosition(o.data("column-id"),o.index()+1)},start:function(t,e){e.item.addClass("draggable-item-selected")}}).disableSelection()},Kanboard.Column.prototype.savePosition=function(t,e){var a=$(".columns-table").data("save-position-url"),o=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:a,contentType:"application/json",type:"POST",processData:!1,data:JSON.stringify({column_id:t,position:e}),complete:function(){o.app.hideLoadingIcon()}})},Kanboard.CompareHoursColumnChart=function(t){this.app=t},Kanboard.CompareHoursColumnChart.prototype.execute=function(){this.app.hasId("analytic-compare-hours")&&this.show()},Kanboard.CompareHoursColumnChart.prototype.show=function(){var t=$("#chart"),e=t.data("metrics"),a=t.data("label-open"),o=t.data("label-closed"),n=[t.data("label-spent")],r=[t.data("label-estimated")],i=[];for(var s in e)n.push(parseFloat(e[s].time_spent)),r.push(parseFloat(e[s].time_estimated)),i.push("open"==s?a:o);c3.generate({data:{columns:[n,r],type:"bar"},bar:{width:{ratio:.2}},axis:{x:{type:"category",categories:i}},legend:{show:!0}})},Kanboard.CumulativeFlowDiagram=function(t){this.app=t},Kanboard.CumulativeFlowDiagram.prototype.execute=function(){this.app.hasId("analytic-cfd")&&this.show()},Kanboard.CumulativeFlowDiagram.prototype.show=function(){for(var t=$("#chart"),e=t.data("metrics"),a=[],o=[],n=[],r=d3.time.format("%Y-%m-%d"),i=d3.time.format(t.data("date-format")),s=0;s0&&o.push(e[s][d])):(a[d].push(e[s][d]),0==d&&n.push(i(r.parse(e[s][d]))));c3.generate({data:{columns:a,type:"area-spline",groups:[o]},axis:{x:{type:"category",categories:n}}})},Kanboard.Dropdown=function(t){this.app=t},Kanboard.Dropdown.prototype.listen=function(){var t=this;$(document).on("click",function(){t.close()}),$(document).on("click",".dropdown-menu",function(e){e.preventDefault(),e.stopImmediatePropagation(),t.close();var a=$(this).next("ul"),o=$(this).offset();$("body").append(jQuery("
",{id:"dropdown"})),a.clone().appendTo("#dropdown");var n=$("#dropdown ul");n.addClass("dropdown-submenu-open");var r=n.outerHeight(),i=n.outerWidth();o.top+r-$(window).scrollTop()<$(window).height()||$(window).scrollTop()+o.top$(window).width()?n.css("left",o.left-i+$(this).outerWidth()):n.css("left",o.left)}),$(document).on("click",".dropdown-submenu-open li",function(t){$(t.target).is("li")&&$(this).find("a:visible")[0].click()})},Kanboard.Dropdown.prototype.close=function(){$("#dropdown").remove()},Kanboard.Dropdown.prototype.onPopoverOpened=function(){this.close()},Kanboard.FileUpload=function(t){this.app=t,this.files=[],this.currentFile=0},Kanboard.FileUpload.prototype.onPopoverOpened=function(){var t=document.getElementById("file-dropzone"),e=this;t&&(t.ondragover=t.ondragenter=function(t){t.stopPropagation(),t.preventDefault()},t.ondrop=function(t){t.stopPropagation(),t.preventDefault(),e.files=t.dataTransfer.files,e.show(),$("#file-error-max-size").hide()},$(document).on("click","#file-browser",function(t){t.preventDefault(),$("#file-form-element").get(0).click()}),$(document).on("click","#file-upload-button",function(t){t.preventDefault(),e.currentFile=0,e.checkFiles()}),$("#file-form-element").change(function(){e.files=document.getElementById("file-form-element").files,e.show(),$("#file-error-max-size").hide()}))},Kanboard.FileUpload.prototype.show=function(){if($("#file-list").remove(),this.files.length>0){$("#file-upload-button").prop("disabled",!1),$("#file-dropzone-inner").hide();for(var t=jQuery("
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 b6119e7dee84869a619dedccd9c80df4422a4f5b Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 14:05:15 -0400 Subject: Added internal task links to activity stream --- ChangeLog | 1 + app/Action/TaskAssignCategoryLink.php | 13 +- app/Action/TaskAssignColorLink.php | 10 +- app/Core/Base.php | 1 + app/EventBuilder/TaskLinkEventBuilder.php | 89 +++++++++++ app/Helper/HookHelper.php | 2 +- app/Job/TaskLinkEventJob.php | 45 ++++++ app/Model/NotificationModel.php | 39 ++--- app/Model/TaskLinkModel.php | 173 ++++++++++++--------- app/ServiceProvider/JobProvider.php | 5 + app/Subscriber/NotificationSubscriber.php | 3 + .../event/task_internal_link_create_update.php | 16 ++ app/Template/event/task_internal_link_delete.php | 16 ++ app/Template/notification/task_file_create.php | 2 +- .../task_internal_link_create_update.php | 11 ++ .../notification/task_internal_link_delete.php | 11 ++ tests/units/Action/TaskAssignCategoryLinkTest.php | 51 +++--- tests/units/Action/TaskAssignColorLinkTest.php | 45 ++++-- .../EventBuilder/TaskLinkEventBuilderTest.php | 70 +++++++++ tests/units/Job/TaskLinkEventJobTest.php | 65 ++++++++ tests/units/Model/NotificationModelTest.php | 39 ++--- tests/units/Model/TaskLinkModelTest.php | 28 ++++ tests/units/Notification/MailNotificationTest.php | 117 ++++++++++++++ tests/units/Notification/MailTest.php | 117 -------------- .../units/Notification/WebhookNotificationTest.php | 29 ++++ tests/units/Notification/WebhookTest.php | 29 ---- 26 files changed, 705 insertions(+), 322 deletions(-) create mode 100644 app/EventBuilder/TaskLinkEventBuilder.php create mode 100644 app/Job/TaskLinkEventJob.php create mode 100644 app/Template/event/task_internal_link_create_update.php create mode 100644 app/Template/event/task_internal_link_delete.php create mode 100644 app/Template/notification/task_internal_link_create_update.php create mode 100644 app/Template/notification/task_internal_link_delete.php create mode 100644 tests/units/EventBuilder/TaskLinkEventBuilderTest.php create mode 100644 tests/units/Job/TaskLinkEventJobTest.php create mode 100644 tests/units/Notification/MailNotificationTest.php delete mode 100644 tests/units/Notification/MailTest.php create mode 100644 tests/units/Notification/WebhookNotificationTest.php delete mode 100644 tests/units/Notification/WebhookTest.php diff --git a/ChangeLog b/ChangeLog index a1e39436..ee57c86c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,7 @@ Version 1.0.32 (unreleased) New features: * New automated action to close tasks without activity in a specific column +* Added internal task links to activity stream * Added new event for removed comments * Added search filter for task priority * Added the possibility to hide tasks in dashboard for a specific column diff --git a/app/Action/TaskAssignCategoryLink.php b/app/Action/TaskAssignCategoryLink.php index 6937edd1..d4a4c0ec 100644 --- a/app/Action/TaskAssignCategoryLink.php +++ b/app/Action/TaskAssignCategoryLink.php @@ -60,8 +60,10 @@ class TaskAssignCategoryLink extends Base public function getEventRequiredParameters() { return array( - 'task_id', - 'link_id', + 'task_link' => array( + 'task_id', + 'link_id', + ) ); } @@ -75,7 +77,7 @@ class TaskAssignCategoryLink extends Base public function doAction(array $data) { $values = array( - 'id' => $data['task_id'], + 'id' => $data['task_link']['task_id'], 'category_id' => $this->getParam('category_id'), ); @@ -91,9 +93,8 @@ class TaskAssignCategoryLink extends Base */ public function hasRequiredCondition(array $data) { - if ($data['link_id'] == $this->getParam('link_id')) { - $task = $this->taskFinderModel->getById($data['task_id']); - return empty($task['category_id']); + if ($data['task_link']['link_id'] == $this->getParam('link_id')) { + return empty($data['task']['category_id']); } return false; diff --git a/app/Action/TaskAssignColorLink.php b/app/Action/TaskAssignColorLink.php index 9ab5458b..9759f622 100644 --- a/app/Action/TaskAssignColorLink.php +++ b/app/Action/TaskAssignColorLink.php @@ -59,8 +59,10 @@ class TaskAssignColorLink extends Base public function getEventRequiredParameters() { return array( - 'task_id', - 'link_id', + 'task_link' => array( + 'task_id', + 'link_id', + ) ); } @@ -74,7 +76,7 @@ class TaskAssignColorLink extends Base public function doAction(array $data) { $values = array( - 'id' => $data['task_id'], + 'id' => $data['task_link']['task_id'], 'color_id' => $this->getParam('color_id'), ); @@ -90,6 +92,6 @@ class TaskAssignColorLink extends Base */ public function hasRequiredCondition(array $data) { - return $data['link_id'] == $this->getParam('link_id'); + return $data['task_link']['link_id'] == $this->getParam('link_id'); } } diff --git a/app/Core/Base.php b/app/Core/Base.php index 098bd880..20a2d391 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -154,6 +154,7 @@ use Pimple\Container; * @property \Kanboard\Job\SubtaskEventJob $subtaskEventJob * @property \Kanboard\Job\TaskEventJob $taskEventJob * @property \Kanboard\Job\TaskFileEventJob $taskFileEventJob + * @property \Kanboard\Job\TaskLinkEventJob $taskLinkEventJob * @property \Kanboard\Job\ProjectFileEventJob $projectFileEventJob * @property \Kanboard\Job\NotificationJob $notificationJob * @property \Psr\Log\LoggerInterface $logger diff --git a/app/EventBuilder/TaskLinkEventBuilder.php b/app/EventBuilder/TaskLinkEventBuilder.php new file mode 100644 index 00000000..8be5299f --- /dev/null +++ b/app/EventBuilder/TaskLinkEventBuilder.php @@ -0,0 +1,89 @@ +taskLinkId = $taskLinkId; + return $this; + } + + /** + * Build event data + * + * @access public + * @return TaskLinkEvent|null + */ + public function build() + { + $taskLink = $this->taskLinkModel->getById($this->taskLinkId); + + if (empty($taskLink)) { + $this->logger->debug(__METHOD__.': TaskLink not found'); + return null; + } + + return new TaskLinkEvent(array( + 'task_link' => $taskLink, + 'task' => $this->taskFinderModel->getDetails($taskLink['task_id']), + )); + } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + if ($eventName === TaskLinkModel::EVENT_CREATE_UPDATE) { + return e('%s set a new internal link for the task #%d', $author, $eventData['task']['id']); + } elseif ($eventName === TaskLinkModel::EVENT_DELETE) { + return e('%s removed an internal link for the task #%d', $author, $eventData['task']['id']); + } + + return ''; + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + if ($eventName === TaskLinkModel::EVENT_CREATE_UPDATE) { + return e('A new internal link for the task #%d have been defined', $eventData['task']['id']); + } elseif ($eventName === TaskLinkModel::EVENT_DELETE) { + return e('Internal link removed for the task #%d', $eventData['task']['id']); + } + + return ''; + } +} diff --git a/app/Helper/HookHelper.php b/app/Helper/HookHelper.php index 2d13ebcc..cb4dc1ef 100644 --- a/app/Helper/HookHelper.php +++ b/app/Helper/HookHelper.php @@ -56,7 +56,7 @@ class HookHelper extends Base * @access public * @param string $hook * @param string $template - * @return \Kanboard\Helper\Hook + * @return $this */ public function attach($hook, $template) { diff --git a/app/Job/TaskLinkEventJob.php b/app/Job/TaskLinkEventJob.php new file mode 100644 index 00000000..669608ad --- /dev/null +++ b/app/Job/TaskLinkEventJob.php @@ -0,0 +1,45 @@ +jobParams = array($taskLinkId, $eventName); + return $this; + } + + /** + * Execute job + * + * @param int $taskLinkId + * @param string $eventName + * @return $this + */ + public function execute($taskLinkId, $eventName) + { + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId($taskLinkId) + ->build(); + + if ($event !== null) { + $this->dispatcher->dispatch($eventName, $event); + } + } +} diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 925d646e..39c1f581 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Kanboard\Core\Base; +use Kanboard\EventBuilder\TaskLinkEventBuilder; /** * Notification @@ -85,7 +86,9 @@ class NotificationModel extends Base case CommentModel::EVENT_USER_MENTION: return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); default: - return e('Notification'); + return TaskLinkEventBuilder::getInstance($this->container) + ->buildTitleWithAuthor($event_author, $event_name, $event_data) ?: + e('Notification'); } } @@ -138,7 +141,9 @@ class NotificationModel extends Base case CommentModel::EVENT_USER_MENTION: return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); default: - return e('Notification'); + return TaskLinkEventBuilder::getInstance($this->container) + ->buildTitleWithoutAuthor($event_name, $event_data) ?: + e('Notification'); } } @@ -152,32 +157,10 @@ class NotificationModel extends Base */ public function getTaskIdFromEvent($event_name, array $event_data) { - switch ($event_name) { - case TaskFileModel::EVENT_CREATE: - return $event_data['file']['task_id']; - case CommentModel::EVENT_CREATE: - case CommentModel::EVENT_UPDATE: - case CommentModel::EVENT_DELETE: - return $event_data['comment']['task_id']; - case SubtaskModel::EVENT_CREATE: - case SubtaskModel::EVENT_UPDATE: - case SubtaskModel::EVENT_DELETE: - return $event_data['subtask']['task_id']; - case TaskModel::EVENT_CREATE: - case TaskModel::EVENT_UPDATE: - case TaskModel::EVENT_CLOSE: - case TaskModel::EVENT_OPEN: - case TaskModel::EVENT_MOVE_COLUMN: - case TaskModel::EVENT_MOVE_POSITION: - case TaskModel::EVENT_MOVE_SWIMLANE: - case TaskModel::EVENT_ASSIGNEE_CHANGE: - case CommentModel::EVENT_USER_MENTION: - case TaskModel::EVENT_USER_MENTION: - return $event_data['task']['id']; - case TaskModel::EVENT_OVERDUE: - return $event_data['tasks'][0]['id']; - default: - return 0; + if ($event_name === TaskModel::EVENT_OVERDUE) { + return $event_data['tasks'][0]['id']; } + + return isset($event_data['task']['id']) ? $event_data['task']['id'] : 0; } } diff --git a/app/Model/TaskLinkModel.php b/app/Model/TaskLinkModel.php index 09978eae..e8d3c5df 100644 --- a/app/Model/TaskLinkModel.php +++ b/app/Model/TaskLinkModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskLinkEvent; /** * TaskLink model @@ -26,7 +25,8 @@ class TaskLinkModel extends Base * * @var string */ - const EVENT_CREATE_UPDATE = 'tasklink.create_update'; + const EVENT_CREATE_UPDATE = 'task_internal_link.create_update'; + const EVENT_DELETE = 'task_internal_link.delete'; /** * Get projectId from $task_link_id @@ -53,7 +53,19 @@ class TaskLinkModel extends Base */ public function getById($task_link_id) { - return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne(); + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.opposite_task_id', + self::TABLE.'.task_id', + self::TABLE.'.link_id', + LinkModel::TABLE.'.label', + LinkModel::TABLE.'.opposite_id AS opposite_link_id' + ) + ->eq(self::TABLE.'.id', $task_link_id) + ->join(LinkModel::TABLE, 'id', 'link_id') + ->findOne(); } /** @@ -139,20 +151,6 @@ class TaskLinkModel extends Base return $result; } - /** - * Publish events - * - * @access private - * @param array $events - */ - private function fireEvents(array $events) - { - foreach ($events as $event) { - $event['project_id'] = $this->taskFinderModel->getProjectId($event['task_id']); - $this->container['dispatcher']->dispatch(self::EVENT_CREATE_UPDATE, new TaskLinkEvent($event)); - } - } - /** * Create a new link * @@ -160,42 +158,25 @@ class TaskLinkModel extends Base * @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 + * @return integer|boolean */ public function create($task_id, $opposite_task_id, $link_id) { - $events = array(); $this->db->startTransaction(); - // Get opposite link $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); + $task_link_id1 = $this->createTaskLink($task_id, $opposite_task_id, $link_id); + $task_link_id2 = $this->createTaskLink($opposite_task_id, $task_id, $opposite_link_id); - $values = array( - 'task_id' => $task_id, - 'opposite_task_id' => $opposite_task_id, - 'link_id' => $link_id, - ); - - // Create the original task link - $this->db->table(self::TABLE)->insert($values); - $task_link_id = $this->db->getLastId(); - $events[] = $values; - - // Create the opposite task link - $values = array( - 'task_id' => $opposite_task_id, - 'opposite_task_id' => $task_id, - 'link_id' => $opposite_link_id, - ); - - $this->db->table(self::TABLE)->insert($values); - $events[] = $values; + if ($task_link_id1 === false || $task_link_id2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); + $this->fireEvents(array($task_link_id1, $task_link_id2), self::EVENT_CREATE_UPDATE); - $this->fireEvents($events); - - return (int) $task_link_id; + return $task_link_id1; } /** @@ -210,46 +191,24 @@ class TaskLinkModel extends Base */ public function update($task_link_id, $task_id, $opposite_task_id, $link_id) { - $events = array(); $this->db->startTransaction(); - // Get original task link $task_link = $this->getById($task_link_id); - - // Find opposite task link $opposite_task_link = $this->getOppositeTaskLink($task_link); - - // Get opposite link $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); - // Update the original task link - $values = array( - 'task_id' => $task_id, - 'opposite_task_id' => $opposite_task_id, - 'link_id' => $link_id, - ); - - $rs1 = $this->db->table(self::TABLE)->eq('id', $task_link_id)->update($values); - $events[] = $values; + $result1 = $this->updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id); + $result2 = $this->updateTaskLink($opposite_task_link['id'], $opposite_task_id, $task_id, $opposite_link_id); - // Update the opposite link - $values = array( - 'task_id' => $opposite_task_id, - 'opposite_task_id' => $task_id, - 'link_id' => $opposite_link_id, - ); - - $rs2 = $this->db->table(self::TABLE)->eq('id', $opposite_task_link['id'])->update($values); - $events[] = $values; + if ($result1 === false || $result2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); + $this->fireEvents(array($task_link_id, $opposite_task_link['id']), self::EVENT_CREATE_UPDATE); - if ($rs1 && $rs2) { - $this->fireEvents($events); - return true; - } - - return false; + return true; } /** @@ -261,21 +220,83 @@ class TaskLinkModel extends Base */ public function remove($task_link_id) { + $this->taskLinkEventJob->execute($task_link_id, self::EVENT_DELETE); + $this->db->startTransaction(); $link = $this->getById($task_link_id); $link_id = $this->linkModel->getOppositeLinkId($link['link_id']); - $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); + $result1 = $this->db + ->table(self::TABLE) + ->eq('id', $task_link_id) + ->remove(); - $this->db + $result2 = $this->db ->table(self::TABLE) ->eq('opposite_task_id', $link['task_id']) ->eq('task_id', $link['opposite_task_id']) - ->eq('link_id', $link_id)->remove(); + ->eq('link_id', $link_id) + ->remove(); + + if ($result1 === false || $result2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); return true; } + + /** + * Publish events + * + * @access protected + * @param integer[] $task_link_ids + * @param string $eventName + */ + protected function fireEvents(array $task_link_ids, $eventName) + { + foreach ($task_link_ids as $task_link_id) { + $this->queueManager->push($this->taskLinkEventJob->withParams($task_link_id, $eventName)); + } + } + + /** + * Create task link + * + * @access protected + * @param integer $task_id + * @param integer $opposite_task_id + * @param integer $link_id + * @return integer|boolean + */ + protected function createTaskLink($task_id, $opposite_task_id, $link_id) + { + return $this->db->table(self::TABLE)->persist(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + } + + /** + * Update task link + * + * @access protected + * @param integer $task_link_id + * @param integer $task_id + * @param integer $opposite_task_id + * @param integer $link_id + * @return boolean + */ + protected function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_link_id)->update(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + } } diff --git a/app/ServiceProvider/JobProvider.php b/app/ServiceProvider/JobProvider.php index c7f323f1..5b42794b 100644 --- a/app/ServiceProvider/JobProvider.php +++ b/app/ServiceProvider/JobProvider.php @@ -8,6 +8,7 @@ use Kanboard\Job\ProjectFileEventJob; use Kanboard\Job\SubtaskEventJob; use Kanboard\Job\TaskEventJob; use Kanboard\Job\TaskFileEventJob; +use Kanboard\Job\TaskLinkEventJob; use Pimple\Container; use Pimple\ServiceProviderInterface; @@ -44,6 +45,10 @@ class JobProvider implements ServiceProviderInterface return new TaskFileEventJob($c); }); + $container['taskLinkEventJob'] = $container->factory(function ($c) { + return new TaskLinkEventJob($c); + }); + $container['projectFileEventJob'] = $container->factory(function ($c) { return new ProjectFileEventJob($c); }); diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php index 7de24e49..7cc68b26 100644 --- a/app/Subscriber/NotificationSubscriber.php +++ b/app/Subscriber/NotificationSubscriber.php @@ -3,6 +3,7 @@ namespace Kanboard\Subscriber; use Kanboard\Event\GenericEvent; +use Kanboard\Model\TaskLinkModel; use Kanboard\Model\TaskModel; use Kanboard\Model\CommentModel; use Kanboard\Model\SubtaskModel; @@ -31,6 +32,8 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn CommentModel::EVENT_DELETE => 'handleEvent', CommentModel::EVENT_USER_MENTION => 'handleEvent', TaskFileModel::EVENT_CREATE => 'handleEvent', + TaskLinkModel::EVENT_CREATE_UPDATE => 'handleEvent', + TaskLinkModel::EVENT_DELETE => 'handleEvent', ); } diff --git a/app/Template/event/task_internal_link_create_update.php b/app/Template/event/task_internal_link_create_update.php new file mode 100644 index 00000000..de257977 --- /dev/null +++ b/app/Template/event/task_internal_link_create_update.php @@ -0,0 +1,16 @@ +

+ text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + dt->datetime($date_creation) ?> +

+
+

+ url->link(t('#%d', $task_link['opposite_task_id']), 'TaskViewController', 'show', array('task_id' => $task_link['opposite_task_id'])), + $this->text->e($task_link['label']) + ) ?> +

+
diff --git a/app/Template/event/task_internal_link_delete.php b/app/Template/event/task_internal_link_delete.php new file mode 100644 index 00000000..e537bf81 --- /dev/null +++ b/app/Template/event/task_internal_link_delete.php @@ -0,0 +1,16 @@ +

+ text->e($author), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + dt->datetime($date_creation) ?> +

+
+

+ text->e($task_link['label']), + $this->url->link(t('#%d', $task_link['opposite_task_id']), 'TaskViewController', 'show', array('task_id' => $task_link['opposite_task_id'])) + ) ?> +

+
diff --git a/app/Template/notification/task_file_create.php b/app/Template/notification/task_file_create.php index feab8dd2..c19f7279 100644 --- a/app/Template/notification/task_file_create.php +++ b/app/Template/notification/task_file_create.php @@ -2,4 +2,4 @@

-render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/notification/task_internal_link_create_update.php b/app/Template/notification/task_internal_link_create_update.php new file mode 100644 index 00000000..73cad84d --- /dev/null +++ b/app/Template/notification/task_internal_link_create_update.php @@ -0,0 +1,11 @@ +

text->e($task['title']) ?> (#)

+ +

+ url->link(t('#%d', $task_link['opposite_task_id']), 'TaskViewController', 'show', array('task_id' => $task_link['opposite_task_id'])), + $this->text->e($task_link['label']) + ) ?> +

+ +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/app/Template/notification/task_internal_link_delete.php b/app/Template/notification/task_internal_link_delete.php new file mode 100644 index 00000000..bb54e0a7 --- /dev/null +++ b/app/Template/notification/task_internal_link_delete.php @@ -0,0 +1,11 @@ +

text->e($task['title']) ?> (#)

+ +

+ text->e($task_link['label']), + $this->url->link(t('#%d', $task_link['opposite_task_id']), 'TaskViewController', 'show', array('task_id' => $task_link['opposite_task_id'])) + ) ?> +

+ +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> diff --git a/tests/units/Action/TaskAssignCategoryLinkTest.php b/tests/units/Action/TaskAssignCategoryLinkTest.php index d7e68f72..b9d7e9d9 100644 --- a/tests/units/Action/TaskAssignCategoryLinkTest.php +++ b/tests/units/Action/TaskAssignCategoryLinkTest.php @@ -2,12 +2,12 @@ require_once __DIR__.'/../Base.php'; +use Kanboard\EventBuilder\TaskLinkEventBuilder; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; use Kanboard\Model\TaskLinkModel; use Kanboard\Model\CategoryModel; -use Kanboard\Event\TaskLinkEvent; use Kanboard\Action\TaskAssignCategoryLink; class TaskAssignCategoryLinkTest extends Base @@ -18,6 +18,7 @@ class TaskAssignCategoryLinkTest extends Base $taskFinderModel = new TaskFinderModel($this->container); $projectModel = new ProjectModel($this->container); $categoryModel = new CategoryModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); @@ -27,13 +28,12 @@ class TaskAssignCategoryLinkTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); - $event = new TaskLinkEvent(array( - 'project_id' => 1, - 'task_id' => 1, - 'opposite_task_id' => 2, - 'link_id' => 2, - )); + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId(1) + ->build(); $this->assertTrue($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); @@ -44,51 +44,58 @@ class TaskAssignCategoryLinkTest extends Base public function testWhenLinkDontMatch() { $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); $projectModel = new ProjectModel($this->container); $categoryModel = new CategoryModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); $action->setParam('category_id', 1); - $action->setParam('link_id', 1); + $action->setParam('link_id', 2); $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); - $event = new TaskLinkEvent(array( - 'project_id' => 1, - 'task_id' => 1, - 'opposite_task_id' => 2, - 'link_id' => 2, - )); + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId(1) + ->build(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(0, $task['category_id']); } public function testThatExistingCategoryWillNotChange() { $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); $projectModel = new ProjectModel($this->container); $categoryModel = new CategoryModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $action = new TaskAssignCategoryLink($this->container); $action->setProjectId(1); - $action->setParam('category_id', 2); + $action->setParam('category_id', 1); $action->setParam('link_id', 2); $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1))); - $this->assertEquals(2, $categoryModel->create(array('name' => 'C2', 'project_id' => 1))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'category_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); - $event = new TaskLinkEvent(array( - 'project_id' => 1, - 'task_id' => 1, - 'opposite_task_id' => 2, - 'link_id' => 2, - )); + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId(1) + ->build(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(1, $task['category_id']); } } diff --git a/tests/units/Action/TaskAssignColorLinkTest.php b/tests/units/Action/TaskAssignColorLinkTest.php index 07d0969b..27364bc9 100644 --- a/tests/units/Action/TaskAssignColorLinkTest.php +++ b/tests/units/Action/TaskAssignColorLinkTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; +use Kanboard\EventBuilder\TaskLinkEventBuilder; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\ProjectModel; @@ -13,42 +13,55 @@ class TaskAssignColorLinkTest extends Base { public function testChangeColor() { - $projectModel = new ProjectModel($this->container); $taskCreationModel = new TaskCreationModel($this->container); $taskFinderModel = new TaskFinderModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'link_id' => 1)); + $projectModel = new ProjectModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $action = new TaskAssignColorLink($this->container); $action->setProjectId(1); + $action->setParam('link_id', 2); $action->setParam('color_id', 'red'); - $action->setParam('link_id', 1); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 2)); + + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId(1) + ->build(); $this->assertTrue($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); $task = $taskFinderModel->getById(1); - $this->assertNotEmpty($task); $this->assertEquals('red', $task['color_id']); } public function testWithWrongLink() { - $projectModel = new ProjectModel($this->container); $taskCreationModel = new TaskCreationModel($this->container); - - $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); - $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); - - $event = new GenericEvent(array('project_id' => 1, 'task_id' => 1, 'link_id' => 2)); + $taskFinderModel = new TaskFinderModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $action = new TaskAssignColorLink($this->container); $action->setProjectId(1); + $action->setParam('link_id', 2); $action->setParam('color_id', 'red'); - $action->setParam('link_id', 1); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $event = TaskLinkEventBuilder::getInstance($this->container) + ->withTaskLinkId(1) + ->build(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); + + $task = $taskFinderModel->getById(1); + $this->assertEquals('yellow', $task['color_id']); } } diff --git a/tests/units/EventBuilder/TaskLinkEventBuilderTest.php b/tests/units/EventBuilder/TaskLinkEventBuilderTest.php new file mode 100644 index 00000000..7364d651 --- /dev/null +++ b/tests/units/EventBuilder/TaskLinkEventBuilderTest.php @@ -0,0 +1,70 @@ +container); + $taskLinkEventBuilder->withTaskLinkId(42); + $this->assertNull($taskLinkEventBuilder->build()); + } + + public function testBuild() + { + $taskLinkModel = new TaskLinkModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskLinkEventBuilder = new TaskLinkEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'task 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $event = $taskLinkEventBuilder->withTaskLinkId(1)->build(); + + $this->assertInstanceOf('Kanboard\Event\TaskLinkEvent', $event); + $this->assertNotEmpty($event['task_link']); + $this->assertNotEmpty($event['task']); + } + + public function testBuildTitle() + { + $taskLinkModel = new TaskLinkModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskLinkEventBuilder = new TaskLinkEventBuilder($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'task 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $eventData = $taskLinkEventBuilder->withTaskLinkId(1)->build(); + + $title = $taskLinkEventBuilder->buildTitleWithAuthor('Foobar', TaskLinkModel::EVENT_CREATE_UPDATE, $eventData->getAll()); + $this->assertEquals('Foobar set a new internal link for the task #1', $title); + + $title = $taskLinkEventBuilder->buildTitleWithAuthor('Foobar', TaskLinkModel::EVENT_DELETE, $eventData->getAll()); + $this->assertEquals('Foobar removed an internal link for the task #1', $title); + + $title = $taskLinkEventBuilder->buildTitleWithAuthor('Foobar', 'not found', $eventData->getAll()); + $this->assertSame('', $title); + + $title = $taskLinkEventBuilder->buildTitleWithoutAuthor(TaskLinkModel::EVENT_CREATE_UPDATE, $eventData->getAll()); + $this->assertEquals('A new internal link for the task #1 have been defined', $title); + + $title = $taskLinkEventBuilder->buildTitleWithoutAuthor(TaskLinkModel::EVENT_DELETE, $eventData->getAll()); + $this->assertEquals('Internal link removed for the task #1', $title); + + $title = $taskLinkEventBuilder->buildTitleWithoutAuthor('not found', $eventData->getAll()); + $this->assertSame('', $title); + } +} diff --git a/tests/units/Job/TaskLinkEventJobTest.php b/tests/units/Job/TaskLinkEventJobTest.php new file mode 100644 index 00000000..1949316a --- /dev/null +++ b/tests/units/Job/TaskLinkEventJobTest.php @@ -0,0 +1,65 @@ +container); + $taskLinkEventJob->withParams(123, 'foobar'); + + $this->assertSame(array(123, 'foobar'), $taskLinkEventJob->getJobParams()); + } + + public function testWithMissingLink() + { + $this->container['dispatcher']->addListener(TaskLinkModel::EVENT_CREATE_UPDATE, function() {}); + + $taskLinkEventJob = new TaskLinkEventJob($this->container); + $taskLinkEventJob->execute(42, TaskLinkModel::EVENT_CREATE_UPDATE); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertEmpty($called); + } + + public function testTriggerCreationEvents() + { + $this->container['dispatcher']->addListener(TaskLinkModel::EVENT_CREATE_UPDATE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'task 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskLinkModel::EVENT_CREATE_UPDATE.'.closure', $called); + } + + public function testTriggerDeleteEvents() + { + $this->container['dispatcher']->addListener(TaskLinkModel::EVENT_DELETE, function() {}); + + $taskCreationModel = new TaskCreationModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'task 1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + $this->assertTrue($taskLinkModel->remove(1)); + + $called = $this->container['dispatcher']->getCalledListeners(); + $this->assertArrayHasKey(TaskLinkModel::EVENT_DELETE.'.closure', $called); + } +} diff --git a/tests/units/Model/NotificationModelTest.php b/tests/units/Model/NotificationModelTest.php index 889f3349..0bd9db6e 100644 --- a/tests/units/Model/NotificationModelTest.php +++ b/tests/units/Model/NotificationModelTest.php @@ -7,6 +7,7 @@ use Kanboard\Model\TaskCreationModel; use Kanboard\Model\SubtaskModel; use Kanboard\Model\CommentModel; use Kanboard\Model\TaskFileModel; +use Kanboard\Model\TaskLinkModel; use Kanboard\Model\TaskModel; use Kanboard\Model\ProjectModel; use Kanboard\Model\NotificationModel; @@ -23,47 +24,38 @@ class NotificationModelTest extends Base $subtaskModel = new SubtaskModel($this->container); $commentModel = new CommentModel($this->container); $taskFileModel = new TaskFileModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); $this->assertEquals(1, $subtaskModel->create(array('title' => 'test', 'task_id' => 1))); $this->assertEquals(1, $commentModel->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1))); $this->assertEquals(1, $taskFileModel->create(1, 'test', 'blah', 123)); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); $task = $taskFinderModel->getDetails(1); $subtask = $subtaskModel->getById(1, true); $comment = $commentModel->getById(1); $file = $commentModel->getById(1); + $tasklink = $taskLinkModel->getById(1); - $this->assertNotEmpty($task); - $this->assertNotEmpty($subtask); - $this->assertNotEmpty($comment); - $this->assertNotEmpty($file); - - foreach (NotificationSubscriber::getSubscribedEvents() as $event_name => $values) { - $title = $notificationModel->getTitleWithoutAuthor($event_name, array( - 'task' => $task, - 'comment' => $comment, - 'subtask' => $subtask, - 'file' => $file, - 'changes' => array() - )); - - $this->assertNotEmpty($title); - - $title = $notificationModel->getTitleWithAuthor('foobar', $event_name, array( + foreach (NotificationSubscriber::getSubscribedEvents() as $eventName => $values) { + $eventData = array( 'task' => $task, 'comment' => $comment, 'subtask' => $subtask, 'file' => $file, + 'task_link' => $tasklink, 'changes' => array() - )); + ); - $this->assertNotEmpty($title); + $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor($eventName, $eventData)); + $this->assertNotEmpty($notificationModel->getTitleWithAuthor('Foobar', $eventName, $eventData)); } $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor(TaskModel::EVENT_OVERDUE, array('tasks' => array(array('id' => 1))))); - $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor('unkown', array())); + $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor('unknown', array())); } public function testGetTaskIdFromEvent() @@ -75,6 +67,7 @@ class NotificationModelTest extends Base $subtaskModel = new SubtaskModel($this->container); $commentModel = new CommentModel($this->container); $taskFileModel = new TaskFileModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); @@ -86,18 +79,20 @@ class NotificationModelTest extends Base $subtask = $subtaskModel->getById(1, true); $comment = $commentModel->getById(1); $file = $commentModel->getById(1); + $tasklink = $taskLinkModel->getById(1); $this->assertNotEmpty($task); $this->assertNotEmpty($subtask); $this->assertNotEmpty($comment); $this->assertNotEmpty($file); - foreach (NotificationSubscriber::getSubscribedEvents() as $event_name => $values) { - $task_id = $notificationModel->getTaskIdFromEvent($event_name, array( + foreach (NotificationSubscriber::getSubscribedEvents() as $eventName => $values) { + $task_id = $notificationModel->getTaskIdFromEvent($eventName, array( 'task' => $task, 'comment' => $comment, 'subtask' => $subtask, 'file' => $file, + 'task_link' => $tasklink, 'changes' => array() )); diff --git a/tests/units/Model/TaskLinkModelTest.php b/tests/units/Model/TaskLinkModelTest.php index 78590891..01a7888b 100644 --- a/tests/units/Model/TaskLinkModelTest.php +++ b/tests/units/Model/TaskLinkModelTest.php @@ -9,6 +9,34 @@ use Kanboard\Model\ProjectModel; class TaskLinkModelTest extends Base { + public function testGeyById() + { + $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, 6)); + + $taskLink = $taskLinkModel->getById(1); + $this->assertEquals(1, $taskLink['id']); + $this->assertEquals(1, $taskLink['task_id']); + $this->assertEquals(2, $taskLink['opposite_task_id']); + $this->assertEquals(6, $taskLink['link_id']); + $this->assertEquals(7, $taskLink['opposite_link_id']); + $this->assertEquals('is a child of', $taskLink['label']); + + $taskLink = $taskLinkModel->getById(2); + $this->assertEquals(2, $taskLink['id']); + $this->assertEquals(2, $taskLink['task_id']); + $this->assertEquals(1, $taskLink['opposite_task_id']); + $this->assertEquals(7, $taskLink['link_id']); + $this->assertEquals(6, $taskLink['opposite_link_id']); + $this->assertEquals('is a parent of', $taskLink['label']); + } + // Check postgres issue: "Cardinality violation: 7 ERROR: more than one row returned by a subquery used as an expression" public function testGetTaskWithMultipleMilestoneLink() { diff --git a/tests/units/Notification/MailNotificationTest.php b/tests/units/Notification/MailNotificationTest.php new file mode 100644 index 00000000..6579d9bc --- /dev/null +++ b/tests/units/Notification/MailNotificationTest.php @@ -0,0 +1,117 @@ +container); + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $commentModel = new CommentModel($this->container); + $fileModel = new TaskFileModel($this->container); + $taskLinkModel = new TaskLinkModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'test', 'task_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'blah', 123)); + $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); + + $task = $taskFinderModel->getDetails(1); + $subtask = $subtaskModel->getById(1, true); + $comment = $commentModel->getById(1); + $file = $commentModel->getById(1); + $tasklink = $taskLinkModel->getById(1); + + $this->assertNotEmpty($task); + $this->assertNotEmpty($subtask); + $this->assertNotEmpty($comment); + $this->assertNotEmpty($file); + + foreach (NotificationSubscriber::getSubscribedEvents() as $eventName => $values) { + $eventData = array( + 'task' => $task, + 'comment' => $comment, + 'subtask' => $subtask, + 'file' => $file, + 'task_link' => $tasklink, + 'changes' => array() + ); + $this->assertNotEmpty($mailNotification->getMailContent($eventName, $eventData)); + $this->assertNotEmpty($mailNotification->getMailSubject($eventName, $eventData)); + } + } + + public function testSendWithEmailAddress() + { + $mailNotification = new MailNotification($this->container); + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $userModel = new UserModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + $this->assertTrue($userModel->update(array('id' => 1, 'email' => 'test@localhost'))); + + $this->container['emailClient'] = $this + ->getMockBuilder('\Kanboard\Core\Mail\Client') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('send')) + ->getMock(); + + $this->container['emailClient'] + ->expects($this->once()) + ->method('send') + ->with( + $this->equalTo('test@localhost'), + $this->equalTo('admin'), + $this->equalTo('[test][New task] test (#1)'), + $this->stringContains('test') + ); + + $mailNotification->notifyUser($userModel->getById(1), TaskModel::EVENT_CREATE, array('task' => $taskFinderModel->getDetails(1))); + } + + public function testSendWithoutEmailAddress() + { + $mailNotification = new MailNotification($this->container); + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $userModel = new UserModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $this->container['emailClient'] = $this + ->getMockBuilder('\Kanboard\Core\Mail\Client') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('send')) + ->getMock(); + + $this->container['emailClient'] + ->expects($this->never()) + ->method('send'); + + $mailNotification->notifyUser($userModel->getById(1), TaskModel::EVENT_CREATE, array('task' => $taskFinderModel->getDetails(1))); + } +} diff --git a/tests/units/Notification/MailTest.php b/tests/units/Notification/MailTest.php deleted file mode 100644 index 9f077ac8..00000000 --- a/tests/units/Notification/MailTest.php +++ /dev/null @@ -1,117 +0,0 @@ -container); - $p = new ProjectModel($this->container); - $tf = new TaskFinderModel($this->container); - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $c = new CommentModel($this->container); - $f = new TaskFileModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'test', 'task_id' => 1))); - $this->assertEquals(1, $c->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1))); - $this->assertEquals(1, $f->create(1, 'test', 'blah', 123)); - - $task = $tf->getDetails(1); - $subtask = $s->getById(1, true); - $comment = $c->getById(1); - $file = $c->getById(1); - - $this->assertNotEmpty($task); - $this->assertNotEmpty($subtask); - $this->assertNotEmpty($comment); - $this->assertNotEmpty($file); - - foreach (NotificationSubscriber::getSubscribedEvents() as $event => $values) { - $this->assertNotEmpty($en->getMailContent($event, array( - 'task' => $task, - 'comment' => $comment, - 'subtask' => $subtask, - 'file' => $file, - 'changes' => array()) - )); - - $this->assertNotEmpty($en->getMailSubject($event, array( - 'task' => $task, - 'comment' => $comment, - 'subtask' => $subtask, - 'file' => $file, - 'changes' => array()) - )); - } - } - - public function testSendWithEmailAddress() - { - $en = new MailNotification($this->container); - $p = new ProjectModel($this->container); - $tf = new TaskFinderModel($this->container); - $tc = new TaskCreationModel($this->container); - $u = new UserModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1))); - $this->assertTrue($u->update(array('id' => 1, 'email' => 'test@localhost'))); - - $this->container['emailClient'] = $this - ->getMockBuilder('\Kanboard\Core\Mail\Client') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('send')) - ->getMock(); - - $this->container['emailClient'] - ->expects($this->once()) - ->method('send') - ->with( - $this->equalTo('test@localhost'), - $this->equalTo('admin'), - $this->equalTo('[test][New task] test (#1)'), - $this->stringContains('test') - ); - - $en->notifyUser($u->getById(1), TaskModel::EVENT_CREATE, array('task' => $tf->getDetails(1))); - } - - public function testSendWithoutEmailAddress() - { - $en = new MailNotification($this->container); - $p = new ProjectModel($this->container); - $tf = new TaskFinderModel($this->container); - $tc = new TaskCreationModel($this->container); - $u = new UserModel($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1))); - - $this->container['emailClient'] = $this - ->getMockBuilder('\Kanboard\Core\Mail\Client') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('send')) - ->getMock(); - - $this->container['emailClient'] - ->expects($this->never()) - ->method('send'); - - $en->notifyUser($u->getById(1), TaskModel::EVENT_CREATE, array('task' => $tf->getDetails(1))); - } -} diff --git a/tests/units/Notification/WebhookNotificationTest.php b/tests/units/Notification/WebhookNotificationTest.php new file mode 100644 index 00000000..6fbc349c --- /dev/null +++ b/tests/units/Notification/WebhookNotificationTest.php @@ -0,0 +1,29 @@ +container); + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $this->container['dispatcher']->addSubscriber(new NotificationSubscriber($this->container)); + + $configModel->save(array('webhook_url' => 'http://localhost/?task-creation')); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('postJson') + ->with($this->stringContains('http://localhost/?task-creation&token='), $this->anything()); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + } +} diff --git a/tests/units/Notification/WebhookTest.php b/tests/units/Notification/WebhookTest.php deleted file mode 100644 index 5a9eb1c7..00000000 --- a/tests/units/Notification/WebhookTest.php +++ /dev/null @@ -1,29 +0,0 @@ -container); - $p = new ProjectModel($this->container); - $tc = new TaskCreationModel($this->container); - $this->container['dispatcher']->addSubscriber(new NotificationSubscriber($this->container)); - - $c->save(array('webhook_url' => 'http://localhost/?task-creation')); - - $this->container['httpClient'] - ->expects($this->once()) - ->method('postJson') - ->with($this->stringContains('http://localhost/?task-creation&token='), $this->anything()); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - } -} -- cgit v1.2.3 From a823cc1d08535539f850711c0b9edb5b648f1960 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 14:50:59 -0400 Subject: NotificationModel refactoring --- app/EventBuilder/BaseEventBuilder.php | 23 ++- app/EventBuilder/CommentEventBuilder.php | 52 +++++- app/EventBuilder/EventIteratorBuilder.php | 48 ++++++ app/EventBuilder/ProjectFileEventBuilder.php | 29 +++- app/EventBuilder/SubtaskEventBuilder.php | 48 +++++- app/EventBuilder/TaskEventBuilder.php | 102 +++++++++++- app/EventBuilder/TaskFileEventBuilder.php | 38 ++++- app/EventBuilder/TaskLinkEventBuilder.php | 2 +- app/Job/CommentEventJob.php | 2 +- app/Job/ProjectFileEventJob.php | 2 +- app/Job/SubtaskEventJob.php | 2 +- app/Job/TaskEventJob.php | 2 +- app/Job/TaskFileEventJob.php | 2 +- app/Job/TaskLinkEventJob.php | 2 +- app/Locale/bs_BA/translations.php | 6 +- app/Locale/cs_CZ/translations.php | 6 +- app/Locale/da_DK/translations.php | 6 +- app/Locale/de_DE/translations.php | 6 +- app/Locale/el_GR/translations.php | 6 +- app/Locale/es_ES/translations.php | 6 +- app/Locale/fi_FI/translations.php | 6 +- app/Locale/fr_FR/translations.php | 6 +- app/Locale/hu_HU/translations.php | 6 +- app/Locale/id_ID/translations.php | 6 +- app/Locale/it_IT/translations.php | 6 +- app/Locale/ja_JP/translations.php | 6 +- app/Locale/ko_KR/translations.php | 6 +- app/Locale/my_MY/translations.php | 6 +- app/Locale/nb_NO/translations.php | 6 +- app/Locale/nl_NL/translations.php | 6 +- app/Locale/pl_PL/translations.php | 6 +- app/Locale/pt_BR/translations.php | 6 +- app/Locale/pt_PT/translations.php | 6 +- app/Locale/ru_RU/translations.php | 6 +- app/Locale/sr_Latn_RS/translations.php | 6 +- app/Locale/sv_SE/translations.php | 6 +- app/Locale/th_TH/translations.php | 6 +- app/Locale/tr_TR/translations.php | 6 +- app/Locale/zh_CN/translations.php | 6 +- app/Model/NotificationModel.php | 176 +++++++-------------- app/Template/event/task_assignee_change.php | 2 +- tests/units/Action/TaskAssignCategoryLinkTest.php | 6 +- tests/units/Action/TaskAssignColorLinkTest.php | 4 +- .../units/EventBuilder/CommentEventBuilderTest.php | 4 +- .../EventBuilder/ProjectFileEventBuilderTest.php | 4 +- .../units/EventBuilder/SubtaskEventBuilderTest.php | 6 +- tests/units/EventBuilder/TaskEventBuilderTest.php | 10 +- .../EventBuilder/TaskFileEventBuilderTest.php | 4 +- .../EventBuilder/TaskLinkEventBuilderTest.php | 6 +- 49 files changed, 494 insertions(+), 232 deletions(-) create mode 100644 app/EventBuilder/EventIteratorBuilder.php diff --git a/app/EventBuilder/BaseEventBuilder.php b/app/EventBuilder/BaseEventBuilder.php index c677563e..5aa777a0 100644 --- a/app/EventBuilder/BaseEventBuilder.php +++ b/app/EventBuilder/BaseEventBuilder.php @@ -19,5 +19,26 @@ abstract class BaseEventBuilder extends Base * @access public * @return GenericEvent|null */ - abstract public function build(); + abstract public function buildEvent(); + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + abstract public function buildTitleWithAuthor($author, $eventName, array $eventData); + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + abstract public function buildTitleWithoutAuthor($eventName, array $eventData); } diff --git a/app/EventBuilder/CommentEventBuilder.php b/app/EventBuilder/CommentEventBuilder.php index 7b4060e4..ba5842a4 100644 --- a/app/EventBuilder/CommentEventBuilder.php +++ b/app/EventBuilder/CommentEventBuilder.php @@ -3,6 +3,7 @@ namespace Kanboard\EventBuilder; use Kanboard\Event\CommentEvent; +use Kanboard\Model\CommentModel; /** * Class CommentEventBuilder @@ -32,7 +33,7 @@ class CommentEventBuilder extends BaseEventBuilder * @access public * @return CommentEvent|null */ - public function build() + public function buildEvent() { $comment = $this->commentModel->getById($this->commentId); @@ -45,4 +46,53 @@ class CommentEventBuilder extends BaseEventBuilder 'task' => $this->taskFinderModel->getDetails($comment['task_id']), )); } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + switch ($eventName) { + case CommentModel::EVENT_UPDATE: + return e('%s updated a comment on the task #%d', $author, $eventData['task']['id']); + case CommentModel::EVENT_CREATE: + return e('%s commented on the task #%d', $author, $eventData['task']['id']); + case CommentModel::EVENT_DELETE: + return e('%s removed a comment on the task #%d', $author, $eventData['task']['id']); + case CommentModel::EVENT_USER_MENTION: + return e('%s mentioned you in a comment on the task #%d', $author, $eventData['task']['id']); + default: + return ''; + } + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + switch ($eventName) { + case CommentModel::EVENT_CREATE: + return e('New comment on task #%d', $eventData['comment']['task_id']); + case CommentModel::EVENT_UPDATE: + return e('Comment updated on task #%d', $eventData['comment']['task_id']); + case CommentModel::EVENT_DELETE: + return e('Comment removed on task #%d', $eventData['comment']['task_id']); + case CommentModel::EVENT_USER_MENTION: + return e('You were mentioned in a comment on the task #%d', $eventData['task']['id']); + default: + return ''; + } + } } diff --git a/app/EventBuilder/EventIteratorBuilder.php b/app/EventBuilder/EventIteratorBuilder.php new file mode 100644 index 00000000..afa146b6 --- /dev/null +++ b/app/EventBuilder/EventIteratorBuilder.php @@ -0,0 +1,48 @@ +builders[] = $builder; + return $this; + } + + public function rewind() { + $this->position = 0; + } + + /** + * @return BaseEventBuilder + */ + public function current() { + return $this->builders[$this->position]; + } + + public function key() { + return $this->position; + } + + public function next() { + ++$this->position; + } + + public function valid() { + return isset($this->builders[$this->position]); + } +} diff --git a/app/EventBuilder/ProjectFileEventBuilder.php b/app/EventBuilder/ProjectFileEventBuilder.php index 70514a99..6698f78a 100644 --- a/app/EventBuilder/ProjectFileEventBuilder.php +++ b/app/EventBuilder/ProjectFileEventBuilder.php @@ -33,7 +33,7 @@ class ProjectFileEventBuilder extends BaseEventBuilder * @access public * @return GenericEvent|null */ - public function build() + public function buildEvent() { $file = $this->projectFileModel->getById($this->fileId); @@ -47,4 +47,31 @@ class ProjectFileEventBuilder extends BaseEventBuilder 'project' => $this->projectModel->getById($file['project_id']), )); } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + return ''; + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + return ''; + } } diff --git a/app/EventBuilder/SubtaskEventBuilder.php b/app/EventBuilder/SubtaskEventBuilder.php index f0271257..5f7e831d 100644 --- a/app/EventBuilder/SubtaskEventBuilder.php +++ b/app/EventBuilder/SubtaskEventBuilder.php @@ -4,6 +4,7 @@ namespace Kanboard\EventBuilder; use Kanboard\Event\SubtaskEvent; use Kanboard\Event\GenericEvent; +use Kanboard\Model\SubtaskModel; /** * Class SubtaskEventBuilder @@ -59,7 +60,7 @@ class SubtaskEventBuilder extends BaseEventBuilder * @access public * @return GenericEvent|null */ - public function build() + public function buildEvent() { $eventData = array(); $eventData['subtask'] = $this->subtaskModel->getById($this->subtaskId, true); @@ -76,4 +77,49 @@ class SubtaskEventBuilder extends BaseEventBuilder $eventData['task'] = $this->taskFinderModel->getDetails($eventData['subtask']['task_id']); return new SubtaskEvent($eventData); } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + switch ($eventName) { + case SubtaskModel::EVENT_UPDATE: + return e('%s updated a subtask for the task #%d', $author, $eventData['task']['id']); + case SubtaskModel::EVENT_CREATE: + return e('%s created a subtask for the task #%d', $author, $eventData['task']['id']); + case SubtaskModel::EVENT_DELETE: + return e('%s removed a subtask for the task #%d', $author, $eventData['task']['id']); + default: + return ''; + } + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + switch ($eventName) { + case SubtaskModel::EVENT_CREATE: + return e('New subtask on task #%d', $eventData['subtask']['task_id']); + case SubtaskModel::EVENT_UPDATE: + return e('Subtask updated on task #%d', $eventData['subtask']['task_id']); + case SubtaskModel::EVENT_DELETE: + return e('Subtask removed on task #%d', $eventData['subtask']['task_id']); + default: + return ''; + } + } } diff --git a/app/EventBuilder/TaskEventBuilder.php b/app/EventBuilder/TaskEventBuilder.php index e7a5653d..aa897632 100644 --- a/app/EventBuilder/TaskEventBuilder.php +++ b/app/EventBuilder/TaskEventBuilder.php @@ -3,6 +3,7 @@ namespace Kanboard\EventBuilder; use Kanboard\Event\TaskEvent; +use Kanboard\Model\TaskModel; /** * Class TaskEventBuilder @@ -98,7 +99,7 @@ class TaskEventBuilder extends BaseEventBuilder * @access public * @return TaskEvent|null */ - public function build() + public function buildEvent() { $eventData = array(); $eventData['task_id'] = $this->taskId; @@ -120,4 +121,103 @@ class TaskEventBuilder extends BaseEventBuilder return new TaskEvent(array_merge($eventData, $this->values)); } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + switch ($eventName) { + case TaskModel::EVENT_ASSIGNEE_CHANGE: + $assignee = $eventData['task']['assignee_name'] ?: $eventData['task']['assignee_username']; + + if (! empty($assignee)) { + return e('%s changed the assignee of the task #%d to %s', $author, $eventData['task']['id'], $assignee); + } + + return e('%s removed the assignee of the task %s', $author, e('#%d', $eventData['task']['id'])); + case TaskModel::EVENT_UPDATE: + return e('%s updated the task #%d', $author, $eventData['task']['id']); + case TaskModel::EVENT_CREATE: + return e('%s created the task #%d', $author, $eventData['task']['id']); + case TaskModel::EVENT_CLOSE: + return e('%s closed the task #%d', $author, $eventData['task']['id']); + case TaskModel::EVENT_OPEN: + return e('%s opened the task #%d', $author, $eventData['task']['id']); + case TaskModel::EVENT_MOVE_COLUMN: + return e( + '%s moved the task #%d to the column "%s"', + $author, + $eventData['task']['id'], + $eventData['task']['column_title'] + ); + case TaskModel::EVENT_MOVE_POSITION: + return e( + '%s moved the task #%d to the position %d in the column "%s"', + $author, + $eventData['task']['id'], + $eventData['task']['position'], + $eventData['task']['column_title'] + ); + case TaskModel::EVENT_MOVE_SWIMLANE: + if ($eventData['task']['swimlane_id'] == 0) { + return e('%s moved the task #%d to the first swimlane', $author, $eventData['task']['id']); + } + + return e( + '%s moved the task #%d to the swimlane "%s"', + $author, + $eventData['task']['id'], + $eventData['task']['swimlane_name'] + ); + + case TaskModel::EVENT_USER_MENTION: + return e('%s mentioned you in the task #%d', $author, $eventData['task']['id']); + default: + return ''; + } + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + switch ($eventName) { + case TaskModel::EVENT_CREATE: + return e('New task #%d: %s', $eventData['task']['id'], $eventData['task']['title']); + case TaskModel::EVENT_UPDATE: + return e('Task updated #%d', $eventData['task']['id']); + case TaskModel::EVENT_CLOSE: + return e('Task #%d closed', $eventData['task']['id']); + case TaskModel::EVENT_OPEN: + return e('Task #%d opened', $eventData['task']['id']); + case TaskModel::EVENT_MOVE_COLUMN: + return e('Column changed for task #%d', $eventData['task']['id']); + case TaskModel::EVENT_MOVE_POSITION: + return e('New position for task #%d', $eventData['task']['id']); + case TaskModel::EVENT_MOVE_SWIMLANE: + return e('Swimlane changed for task #%d', $eventData['task']['id']); + case TaskModel::EVENT_ASSIGNEE_CHANGE: + return e('Assignee changed on task #%d', $eventData['task']['id']); + case TaskModel::EVENT_OVERDUE: + $nb = count($eventData['tasks']); + return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $eventData['tasks'][0]['id']); + case TaskModel::EVENT_USER_MENTION: + return e('You were mentioned in the task #%d', $eventData['task']['id']); + default: + return ''; + } + } } diff --git a/app/EventBuilder/TaskFileEventBuilder.php b/app/EventBuilder/TaskFileEventBuilder.php index 7f1ce3b3..8c985cc0 100644 --- a/app/EventBuilder/TaskFileEventBuilder.php +++ b/app/EventBuilder/TaskFileEventBuilder.php @@ -4,6 +4,7 @@ namespace Kanboard\EventBuilder; use Kanboard\Event\TaskFileEvent; use Kanboard\Event\GenericEvent; +use Kanboard\Model\TaskFileModel; /** * Class TaskFileEventBuilder @@ -33,7 +34,7 @@ class TaskFileEventBuilder extends BaseEventBuilder * @access public * @return GenericEvent|null */ - public function build() + public function buildEvent() { $file = $this->taskFileModel->getById($this->fileId); @@ -47,4 +48,39 @@ class TaskFileEventBuilder extends BaseEventBuilder 'task' => $this->taskFinderModel->getDetails($file['task_id']), )); } + + /** + * Get event title with author + * + * @access public + * @param string $author + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithAuthor($author, $eventName, array $eventData) + { + if ($eventName === TaskFileModel::EVENT_CREATE) { + return e('%s attached a file to the task #%d', $author, $eventData['task']['id']); + } + + return ''; + } + + /** + * Get event title without author + * + * @access public + * @param string $eventName + * @param array $eventData + * @return string + */ + public function buildTitleWithoutAuthor($eventName, array $eventData) + { + if ($eventName === TaskFileModel::EVENT_CREATE) { + return e('New attachment on task #%d: %s', $eventData['file']['task_id'], $eventData['file']['name']); + } + + return ''; + } } diff --git a/app/EventBuilder/TaskLinkEventBuilder.php b/app/EventBuilder/TaskLinkEventBuilder.php index 8be5299f..f1a3fba2 100644 --- a/app/EventBuilder/TaskLinkEventBuilder.php +++ b/app/EventBuilder/TaskLinkEventBuilder.php @@ -33,7 +33,7 @@ class TaskLinkEventBuilder extends BaseEventBuilder * @access public * @return TaskLinkEvent|null */ - public function build() + public function buildEvent() { $taskLink = $this->taskLinkModel->getById($this->taskLinkId); diff --git a/app/Job/CommentEventJob.php b/app/Job/CommentEventJob.php index c89350ed..47cf8020 100644 --- a/app/Job/CommentEventJob.php +++ b/app/Job/CommentEventJob.php @@ -37,7 +37,7 @@ class CommentEventJob extends BaseJob { $event = CommentEventBuilder::getInstance($this->container) ->withCommentId($commentId) - ->build(); + ->buildEvent(); if ($event !== null) { $this->dispatcher->dispatch($eventName, $event); diff --git a/app/Job/ProjectFileEventJob.php b/app/Job/ProjectFileEventJob.php index d68949c5..45e6ece3 100644 --- a/app/Job/ProjectFileEventJob.php +++ b/app/Job/ProjectFileEventJob.php @@ -36,7 +36,7 @@ class ProjectFileEventJob extends BaseJob { $event = ProjectFileEventBuilder::getInstance($this->container) ->withFileId($fileId) - ->build(); + ->buildEvent(); if ($event !== null) { $this->dispatcher->dispatch($eventName, $event); diff --git a/app/Job/SubtaskEventJob.php b/app/Job/SubtaskEventJob.php index 1dc243ef..85c4d73e 100644 --- a/app/Job/SubtaskEventJob.php +++ b/app/Job/SubtaskEventJob.php @@ -39,7 +39,7 @@ class SubtaskEventJob extends BaseJob $event = SubtaskEventBuilder::getInstance($this->container) ->withSubtaskId($subtaskId) ->withValues($values) - ->build(); + ->buildEvent(); if ($event !== null) { $this->dispatcher->dispatch($eventName, $event); diff --git a/app/Job/TaskEventJob.php b/app/Job/TaskEventJob.php index 46f7a16c..7d026a68 100644 --- a/app/Job/TaskEventJob.php +++ b/app/Job/TaskEventJob.php @@ -47,7 +47,7 @@ class TaskEventJob extends BaseJob ->withChanges($changes) ->withValues($values) ->withTask($task) - ->build(); + ->buildEvent(); if ($event !== null) { foreach ($eventNames as $eventName) { diff --git a/app/Job/TaskFileEventJob.php b/app/Job/TaskFileEventJob.php index de2c40db..293dbf27 100644 --- a/app/Job/TaskFileEventJob.php +++ b/app/Job/TaskFileEventJob.php @@ -36,7 +36,7 @@ class TaskFileEventJob extends BaseJob { $event = TaskFileEventBuilder::getInstance($this->container) ->withFileId($fileId) - ->build(); + ->buildEvent(); if ($event !== null) { $this->dispatcher->dispatch($eventName, $event); diff --git a/app/Job/TaskLinkEventJob.php b/app/Job/TaskLinkEventJob.php index 669608ad..31f62f07 100644 --- a/app/Job/TaskLinkEventJob.php +++ b/app/Job/TaskLinkEventJob.php @@ -36,7 +36,7 @@ class TaskLinkEventJob extends BaseJob { $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId($taskLinkId) - ->build(); + ->buildEvent(); if ($event !== null) { $this->dispatcher->dispatch($eventName, $event); diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 6a062068..f1529e02 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s ažurirao zadatak #%d', '%s created the task #%d' => '%s kreirao zadatak #%d', '%s closed the task #%d' => '%s zatvorio zadatak #%d', - '%s open the task #%d' => '%s otvorio zadatak #%d', + '%s opened the task #%d' => '%s otvorio zadatak #%d', '%s moved the task #%d to the column "%s"' => '%s premjestio zadatak #%d u kolonu "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s premjestio zadatak #%d na poziciju %d u koloni "%s"', 'Activity' => 'Aktivnosti', 'Default values are "%s"' => 'Podrazumijevane vrijednosti su: "%s"', 'Default columns for new projects (Comma-separated)' => 'Podrazumijevane kolone za novi projekat (Odvojene zarezom)', 'Task assignee change' => 'Promijena izvršioca zadatka', - '%s change the assignee of the task #%d to %s' => '%s zamijeni izvršioca za zadatak #%d u %s', + '%s changed the assignee of the task #%d to %s' => '%s zamijeni izvršioca za zadatak #%d u %s', '%s changed the assignee of the task %s to %s' => '%s promijenio izvršioca za zadatak %s u %s', 'New password for the user "%s"' => 'Nova šifra korisnika "%s"', 'Choose an event' => 'Izaberi događaj', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Stopa valute je uspješno dodana.', 'Unable to add this currency rate.' => 'Nemoguće dodati stopu valute.', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s je uklonio izvršioca zadatka %s', + '%s removed the assignee of the task %s' => '%s je uklonio izvršioca zadatka %s', 'Enable Gravatar images' => 'Omogući Gravatar slike', 'Information' => 'Informacije', 'Check two factor authentication code' => 'Provjera faktor-dva autentifikacionog koda', diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index b9a4de6e..c7e6e536 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s aktualizoval úkol #%d ', '%s created the task #%d' => '%s vytvořil úkol #%d ', '%s closed the task #%d' => '%s uzavřel úkol #%d ', - '%s open the task #%d' => '%s znovu otevřel úkol #%d ', + '%s opened the task #%d' => '%s znovu otevřel úkol #%d ', '%s moved the task #%d to the column "%s"' => '%s přesunul úkol #%d do sloupce "%s" ', '%s moved the task #%d to the position %d in the column "%s"' => '%s přesunul úkol #%d na pozici %d ve sloupci "%s" ', 'Activity' => 'Aktivity', 'Default values are "%s"' => 'Standardní hodnoty jsou: "%s"', 'Default columns for new projects (Comma-separated)' => 'Výchozí sloupce pro nové projekty (odděleny čárkou)', 'Task assignee change' => 'Změna přiřazení uživatelů', - '%s change the assignee of the task #%d to %s' => '%s změnil přidělení úkolu #%d na uživatele %s', + '%s changed the assignee of the task #%d to %s' => '%s změnil přidělení úkolu #%d na uživatele %s', '%s changed the assignee of the task %s to %s' => '%s změnil přidělení úkolu %s na uživatele %s', 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', 'Choose an event' => 'Vybrat událost', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Směnný kurz byl úspěšně přidán.', 'Unable to add this currency rate.' => 'Nelze přidat tento směnný kurz', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ', + '%s removed the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ', 'Enable Gravatar images' => 'Aktiviere Gravatar Bilder', 'Information' => 'Informace', 'Check two factor authentication code' => 'Zkontrolujte dvouúrovňový autentifikační klíč', diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 050a37d9..6cecfaec 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s opdaterede opgaven #%d', '%s created the task #%d' => '%s oprettede opgaven #%d', '%s closed the task #%d' => '%s lukkede opgaven #%d', - '%s open the task #%d' => '%s åbnede opgaven #%d', + '%s opened the task #%d' => '%s åbnede opgaven #%d', '%s moved the task #%d to the column "%s"' => '%s flyttede opgaven #%d til kolonnen "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s flyttede opgaven #%d til position %d i kolonnen "%s"', 'Activity' => 'Aktivitet', 'Default values are "%s"' => 'Standard værdier er "%s"', 'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye projekter (kommasepareret)', 'Task assignee change' => 'Opgaven ansvarlig ændring', - '%s change the assignee of the task #%d to %s' => '%s skrift ansvarlig for opgaven #%d til %s', + '%s changed the assignee of the task #%d to %s' => '%s skrift ansvarlig for opgaven #%d til %s', '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s', 'New password for the user "%s"' => 'Ny adgangskode for brugeren "%s"', 'Choose an event' => 'Vælg et event', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', + // '%s removed the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', // 'Check two factor authentication code' => '', diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index d6c8bf60..d25e7e8a 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s hat die Aufgabe #%d aktualisiert', '%s created the task #%d' => '%s hat die Aufgabe #%d angelegt', '%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen', - '%s open the task #%d' => '%s hat die Aufgabe #%d geöffnet', + '%s opened the task #%d' => '%s hat die Aufgabe #%d geöffnet', '%s moved the task #%d to the column "%s"' => '%s hat die Aufgabe #%d in die Spalte "%s" verschoben', '%s moved the task #%d to the position %d in the column "%s"' => '%s hat die Aufgabe #%d an die Position %d in der Spalte "%s" verschoben', 'Activity' => 'Aktivität', 'Default values are "%s"' => 'Die Standardwerte sind "%s"', 'Default columns for new projects (Comma-separated)' => 'Standardspalten für neue Projekte (komma-getrennt)', 'Task assignee change' => 'Zuständigkeit geändert', - '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', + '%s changed the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Der Währungskurs wurde erfolgreich hinzugefügt.', 'Unable to add this currency rate.' => 'Währungskurs konnte nicht hinzugefügt werden', 'Webhook URL' => 'Webhook-URL', - '%s remove the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen', + '%s removed the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen', 'Enable Gravatar images' => 'Aktiviere Gravatar-Bilder', 'Information' => 'Information', 'Check two factor authentication code' => 'Prüfe Zwei-Faktor-Authentifizierungscode', diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 87ea68b0..b02207d5 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s ενημέρωσε την εργασία n°%d', '%s created the task #%d' => '%s δημιούργησε την εργασία n°%d', '%s closed the task #%d' => '%s έκλεισε την εργασία n°%d', - '%s open the task #%d' => '%s άνοιξε την εργασία n°%d', + '%s opened the task #%d' => '%s άνοιξε την εργασία n°%d', '%s moved the task #%d to the column "%s"' => '%s μετακίνησε την εργασία n°%d στη στήλη « %s »', '%s moved the task #%d to the position %d in the column "%s"' => '%s μετακίνησε την εργασία n°%d στη θέση n°%d της στήλης « %s »', 'Activity' => 'Δραστηριότητα', 'Default values are "%s"' => 'Οι προεπιλεγμένες τιμές είναι « %s »', 'Default columns for new projects (Comma-separated)' => 'Προεπιλεγμένες στήλες για νέα έργα (Comma-separated)', 'Task assignee change' => 'Αλλαγή εκδοχέα εργασίας', - '%s change the assignee of the task #%d to %s' => '%s άλλαξε τον εκδοχέα της εργασίας n˚%d σε %s', + '%s changed the assignee of the task #%d to %s' => '%s άλλαξε τον εκδοχέα της εργασίας n˚%d σε %s', '%s changed the assignee of the task %s to %s' => '%s ενημέρωσε τον εκδοχέα της εργασίας %s σε %s', 'New password for the user "%s"' => 'Νέο password του χρήστη « %s »', 'Choose an event' => 'Επιλογή event', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Η ισοτιμία προστέθηκε με επιτυχία.', 'Unable to add this currency rate.' => 'Αδύνατο να προστεθεί αυτή η ισοτιμία.', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s αφαίρεσε τον εκδοχέα της εργασίας %s', + '%s removed the assignee of the task %s' => '%s αφαίρεσε τον εκδοχέα της εργασίας %s', 'Enable Gravatar images' => 'Ενεργοποίηση εικόνων Gravatar', 'Information' => 'Πληροφορίες', 'Check two factor authentication code' => 'Ελέγξτε δύο παράγοντες ελέγχου ταυτότητας κωδικού', diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 1a4bae82..fa59ca07 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s actualizó la tarea #%d', '%s created the task #%d' => '%s creó la tarea #%d', '%s closed the task #%d' => '%s cerró la tarea #%d', - '%s open the task #%d' => '%s abrió la tarea #%d', + '%s opened the task #%d' => '%s abrió la tarea #%d', '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna «%s»', '%s moved the task #%d to the position %d in the column "%s"' => '%s movió la tarea #%d a la posición %d de la columna «%s»', 'Activity' => 'Actividad', 'Default values are "%s"' => 'Los valores por defecto son «%s»', 'Default columns for new projects (Comma-separated)' => 'Columnas por defecto para los nuevos proyectos (separadas mediante comas)', 'Task assignee change' => 'Cambiar responsable de la tarea', - '%s change the assignee of the task #%d to %s' => '%s cambió el responsable de la tarea #%d por %s', + '%s changed the assignee of the task #%d to %s' => '%s cambió el responsable de la tarea #%d por %s', '%s changed the assignee of the task %s to %s' => '%s cambió el responsable de la tarea %s por %s', 'New password for the user "%s"' => 'Nueva contraseña para el usuario «%s»', 'Choose an event' => 'Seleccione un evento', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'El cambio de moneda se ha añadido correctamente.', 'Unable to add this currency rate.' => 'No se puede añadir este cambio de moneda.', 'Webhook URL' => 'URL del disparador web (webhook)', - '%s remove the assignee of the task %s' => '%s quita el responsable de la tarea %s', + '%s removed the assignee of the task %s' => '%s quita el responsable de la tarea %s', 'Enable Gravatar images' => 'Activar imágenes Gravatar', 'Information' => 'Información', 'Check two factor authentication code' => 'Revisar código de autenticación en dos pasos', diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 5d37cb82..200a9cde 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s päivitti tehtävää #%d', '%s created the task #%d' => '%s loi tehtävän #%d', '%s closed the task #%d' => '%s sulki tehtävän #%d', - '%s open the task #%d' => '%s avasi tehtävän #%d', + '%s opened the task #%d' => '%s avasi tehtävän #%d', '%s moved the task #%d to the column "%s"' => '%s siirsi tehtävän #%d sarakkeeseen "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s siirsi tehtävän #%d %d. sarakkeessa %s', 'Activity' => 'Toiminta', 'Default values are "%s"' => 'Oletusarvot ovat "%s"', 'Default columns for new projects (Comma-separated)' => 'Oletussarakkeet uusille projekteille', 'Task assignee change' => 'Tehtävän saajan vaihto', - '%s change the assignee of the task #%d to %s' => '%s vaihtoi tehtävän #%d saajaksi %s', + '%s changed the assignee of the task #%d to %s' => '%s vaihtoi tehtävän #%d saajaksi %s', '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s', 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', 'Choose an event' => 'Valitse toiminta', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', + // '%s removed the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', // 'Check two factor authentication code' => '', diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index c8f7d343..9f6cf971 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s a mis à jour la tâche n°%d', '%s created the task #%d' => '%s a créé la tâche n°%d', '%s closed the task #%d' => '%s a fermé la tâche n°%d', - '%s open the task #%d' => '%s a ouvert la tâche n°%d', + '%s opened the task #%d' => '%s a ouvert la tâche n°%d', '%s moved the task #%d to the column "%s"' => '%s a déplacé la tâche n°%d dans la colonne « %s »', '%s moved the task #%d to the position %d in the column "%s"' => '%s a déplacé la tâche n°%d à la position n°%d dans la colonne « %s »', 'Activity' => 'Activité', 'Default values are "%s"' => 'Les valeurs par défaut sont « %s »', 'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparation par des virgules)', 'Task assignee change' => 'Modification de la personne assignée à une tâche', - '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée à la tâche n˚%d pour %s', + '%s changed the assignee of the task #%d to %s' => '%s a changé la personne assignée à la tâche n˚%d pour %s', '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée à la tâche %s pour %s', 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', 'Choose an event' => 'Choisir un événement', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.', 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change', 'Webhook URL' => 'URL du webhook', - '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', + '%s removed the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', 'Enable Gravatar images' => 'Activer les images Gravatar', 'Information' => 'Informations', 'Check two factor authentication code' => 'Vérification du code pour l\'authentification à deux-facteurs', diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index febf8bc0..781a0423 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s frissítette a feladatot #%d', '%s created the task #%d' => '%s létrehozta a feladatot #%d', '%s closed the task #%d' => '%s lezárta a feladatot #%d', - '%s open the task #%d' => '%s megnyitotta a feladatot #%d', + '%s opened the task #%d' => '%s megnyitotta a feladatot #%d', '%s moved the task #%d to the column "%s"' => '%s átmozgatta a feladatot #%d a "%s" oszlopba', '%s moved the task #%d to the position %d in the column "%s"' => '%s átmozgatta a feladatot #%d a %d pozícióba a "%s" oszlopban', 'Activity' => 'Tevékenységek', 'Default values are "%s"' => 'Az alapértelmezett értékek: %s', 'Default columns for new projects (Comma-separated)' => 'Alapértelmezett oszlopok az új projektekben (vesszővel elválasztva)', 'Task assignee change' => 'Felelős módosítása', - '%s change the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s', + '%s changed the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s', '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s', 'New password for the user "%s"' => 'Felhasználó új jelszava: %s', 'Choose an event' => 'Válasszon eseményt', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Az átváltási árfolyammal történő bővítés sikerült', 'Unable to add this currency rate.' => 'Nem sikerült az átváltási árfolyam felvétele', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s eltávolította a %s feladathoz rendelt személyt', + '%s removed the assignee of the task %s' => '%s eltávolította a %s feladathoz rendelt személyt', 'Enable Gravatar images' => 'Gravatár képek engedélyezése', 'Information' => 'Információ', 'Check two factor authentication code' => 'Két fázisú beléptető kód ellenőrzése', diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index 18a7a72d..26e091ce 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s memperbaharui tugas n°%d', '%s created the task #%d' => '%s membuat tugas n°%d', '%s closed the task #%d' => '%s menutup tugas n°%d', - '%s open the task #%d' => '%s membuka tugas n°%d', + '%s opened the task #%d' => '%s membuka tugas n°%d', '%s moved the task #%d to the column "%s"' => '%s memindahkan tugas n°%d ke kolom « %s »', '%s moved the task #%d to the position %d in the column "%s"' => '%s memindahkan tugas n°%d ke posisi n°%d dalam kolom « %s »', 'Activity' => 'Aktifitas', 'Default values are "%s"' => 'Standar nilai adalah« %s »', 'Default columns for new projects (Comma-separated)' => 'Kolom default untuk proyek baru (dipisahkan dengan koma)', 'Task assignee change' => 'Mengubah orang ditugaskan untuk tugas', - '%s change the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', + '%s changed the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', 'New password for the user "%s"' => 'Kata sandi baru untuk pengguna « %s »', 'Choose an event' => 'Pilih acara', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.', 'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang', 'Webhook URL' => 'URL webhook', - '%s remove the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', + '%s removed the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', 'Enable Gravatar images' => 'Mengaktifkan gambar Gravatar', 'Information' => 'Informasi', 'Check two factor authentication code' => 'Cek dua faktor kode otentifikasi', diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index f6c63076..aadbfe5b 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s ha aggiornato il task #%d', '%s created the task #%d' => '%s ha creato il task #%d', '%s closed the task #%d' => '%s ha chiuso il task #%d', - '%s open the task #%d' => '%s ha aperto il task #%d', + '%s opened the task #%d' => '%s ha aperto il task #%d', '%s moved the task #%d to the column "%s"' => '%s ha spostato il task #%d nella colonna "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s ha spostato il task #%d nella posizione %d della colonna "%s"', 'Activity' => 'Attività', 'Default values are "%s"' => 'Valori di default "%s"', 'Default columns for new projects (Comma-separated)' => 'Colonne di default per i nuovi progetti (Separati da virgola)', 'Task assignee change' => 'Cambia l\'assegnatario del task', - '%s change the assignee of the task #%d to %s' => '%s dai l\'assegnazione del task #%d a %s', + '%s changed the assignee of the task #%d to %s' => '%s dai l\'assegnazione del task #%d a %s', '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del task %s a %s', 'New password for the user "%s"' => 'Nuova password per l\'utente "%s"', 'Choose an event' => 'Scegli un evento', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Il tasso di cambio è stato aggiunto con successo.', 'Unable to add this currency rate.' => 'Impossibile aggiungere questo tasso di cambio.', 'Webhook URL' => 'URL Webhook', - '%s remove the assignee of the task %s' => '%s rimuove l\'assegnatario del task %s', + '%s removed the assignee of the task %s' => '%s rimuove l\'assegnatario del task %s', 'Enable Gravatar images' => 'Abilita immagini Gravatar', 'Information' => 'Informazioni', 'Check two factor authentication code' => 'Controlla il codice di autenticazione "two-factor"', diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index dab731d2..03fa55ed 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s がタスク #%d を更新しました', '%s created the task #%d' => '%s がタスク #%d を追加しました', '%s closed the task #%d' => '%s がタスク #%d をクローズしました', - '%s open the task #%d' => '%s がタスク #%d をオープンしました', + '%s opened the task #%d' => '%s がタスク #%d をオープンしました', '%s moved the task #%d to the column "%s"' => '%s がタスク #%d をカラム「%s」に移動しました', '%s moved the task #%d to the position %d in the column "%s"' => '%s がタスク #%d を位置 %d カラム「%s」移動しました', 'Activity' => 'アクティビティ', 'Default values are "%s"' => 'デフォルト値は「%s」', 'Default columns for new projects (Comma-separated)' => '新規プロジェクトのデフォルトカラム (コンマで区切って入力)', 'Task assignee change' => '担当者の変更', - '%s change the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました', + '%s changed the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました', '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました', 'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード', 'Choose an event' => 'イベントの選択', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', 'Unable to add this currency rate.' => 'この通貨レートを追加できません。', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s がタスク「%s」の担当を解除しました。', + '%s removed the assignee of the task %s' => '%s がタスク「%s」の担当を解除しました。', 'Enable Gravatar images' => 'Gravatar イメージを有効化', 'Information' => '情報 ', 'Check two factor authentication code' => '2 段認証をチェックする', diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index 0b6007b1..bf140d94 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s이 할일#%d을 갱신했습니다', '%s created the task #%d' => '%s이 할일#%d을 추가했습니다', '%s closed the task #%d' => '%s이 할일#%d을 닫혔습니다', - '%s open the task #%d' => '%s이 할일#%d를 오픈했습니다', + '%s opened the task #%d' => '%s이 할일#%d를 오픈했습니다', '%s moved the task #%d to the column "%s"' => '%s이 할일#%d을 칼럼"%s"로 옮겼습니다', '%s moved the task #%d to the position %d in the column "%s"' => '%s이 할일#%d을 칼럼 "%s"의 %d 위치로 이동시켰습니다', 'Activity' => '활동', 'Default values are "%s"' => '기본 값은 "%s" 입니다', 'Default columns for new projects (Comma-separated)' => '새로운 프로젝트의 기본 칼럼 (콤마(,)로 분리됨)', 'Task assignee change' => '담당자의 변경', - '%s change the assignee of the task #%d to %s' => '%s이 할일 #%d의 담당을 %s로 변경합니다', + '%s changed the assignee of the task #%d to %s' => '%s이 할일 #%d의 담당을 %s로 변경합니다', '%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다', 'New password for the user "%s"' => '사용자 "%s"의 새로운 패스워드', 'Choose an event' => '행사의 선택', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => '통화가 성공적으로 추가되었습니다', 'Unable to add this currency rate.' => '이 통화 환율을 추가할 수 없습니다.', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다', + '%s removed the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다', 'Enable Gravatar images' => 'Gravatar이미지를 활성화', 'Information' => '정보', 'Check two factor authentication code' => '2단 인증을 체크한다', diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index 3d66b0bb..cf4f399c 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s memperbaharui tugas n°%d', '%s created the task #%d' => '%s membuat tugas n°%d', '%s closed the task #%d' => '%s menutup tugas n°%d', - '%s open the task #%d' => '%s membuka tugas n°%d', + '%s opened the task #%d' => '%s membuka tugas n°%d', '%s moved the task #%d to the column "%s"' => '%s memindahkan tugas n°%d ke kolom « %s »', '%s moved the task #%d to the position %d in the column "%s"' => '%s memindahkan tugas n°%d ke posisi n°%d dalam kolom « %s »', 'Activity' => 'Aktifitas', 'Default values are "%s"' => 'Standar nilai adalah« %s »', 'Default columns for new projects (Comma-separated)' => 'Kolom default untuk projek baru (dipisahkan dengan koma)', 'Task assignee change' => 'Mengubah orang ditugaskan untuk tugas', - '%s change the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', + '%s changed the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', 'New password for the user "%s"' => 'Kata laluan baru untuk pengguna « %s »', 'Choose an event' => 'Pilih sebuah acara', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.', 'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang', 'Webhook URL' => 'URL webhook', - '%s remove the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', + '%s removed the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', 'Enable Gravatar images' => 'Mengaktifkan gambar Gravatar', 'Information' => 'Informasi', 'Check two factor authentication code' => 'Cek dua faktor kode otentifikasi', diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 14e260cb..ce69deb9 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s oppdaterte oppgaven #%d', '%s created the task #%d' => '%s opprettet oppgaven #%d', '%s closed the task #%d' => '%s lukket oppgaven #%d', - '%s open the task #%d' => '%s åpnet oppgaven #%d', + '%s opened the task #%d' => '%s åpnet oppgaven #%d', '%s moved the task #%d to the column "%s"' => '%s flyttet oppgaven #%d til kolonnen "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s flyttet oppgaven #%d til posisjonen %d i kolonnen "%s"', 'Activity' => 'Aktivitetslogg', 'Default values are "%s"' => 'Standardverdier er "%s"', 'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye prosjekter (komma-separert)', 'Task assignee change' => 'Endring av oppgaveansvarlig', - '%s change the assignee of the task #%d to %s' => '%s endre ansvarlig for oppgaven #%d til %s', + '%s changed the assignee of the task #%d to %s' => '%s endre ansvarlig for oppgaven #%d til %s', '%s changed the assignee of the task %s to %s' => '%s endret ansvarlig for oppgaven %s til %s', 'New password for the user "%s"' => 'Nytt passord for brukeren "%s"', 'Choose an event' => 'Velg en hendelse', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', + // '%s removed the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', // 'Check two factor authentication code' => '', diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 8b47d514..d5ba7036 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s heeft taak %d aangepast', '%s created the task #%d' => '%s heeft taak %d aangemaakt', '%s closed the task #%d' => '%s heeft taak %d gesloten', - '%s open the task #%d' => '%s a heeft taak %d geopend', + '%s opened the task #%d' => '%s a heeft taak %d geopend', '%s moved the task #%d to the column "%s"' => '%s heeft taak %d verplaatst naar kolom « %s »', '%s moved the task #%d to the position %d in the column "%s"' => '%s heeft taak %d verplaatst naar positie %d in kolom « %s »', 'Activity' => 'Activiteit', 'Default values are "%s"' => 'Standaardwaarden zijn « %s »', 'Default columns for new projects (Comma-separated)' => 'Standaard kolommen voor nieuw projecten (komma gescheiden)', 'Task assignee change' => 'Taak toegewezene verandering', - '%s change the assignee of the task #%d to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s', + '%s changed the assignee of the task #%d to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s', '%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %s veranderd in %s', 'New password for the user "%s"' => 'Nieuw wachtwoord voor gebruiker « %s »', 'Choose an event' => 'Kies een gebeurtenis', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', 'Webhook URL' => 'Webhook URL', - // '%s remove the assignee of the task %s' => '', + // '%s removed the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', // 'Check two factor authentication code' => '', diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index e72649e6..f2570d7c 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s zaktualizował zadanie #%d', '%s created the task #%d' => '%s utworzył zadanie #%d', '%s closed the task #%d' => '%s zamknął zadanie #%d', - '%s open the task #%d' => '%s otworzył zadanie #%d', + '%s opened the task #%d' => '%s otworzył zadanie #%d', '%s moved the task #%d to the column "%s"' => '%s przeniósł zadanie #%d do kolumny "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s przeniósł zadanie #%d na pozycję %d w kolmnie "%s"', 'Activity' => 'Aktywność', 'Default values are "%s"' => 'Domyślne wartości: "%s"', 'Default columns for new projects (Comma-separated)' => 'Domyślne kolumny dla nowych projektów (oddzielone przecinkiem)', 'Task assignee change' => 'Zmień osobę odpowiedzialną', - '%s change the assignee of the task #%d to %s' => '%s zmienił osobę odpowiedzialną za zadanie #%d na %s', + '%s changed the assignee of the task #%d to %s' => '%s zmienił osobę odpowiedzialną za zadanie #%d na %s', '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s', 'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"', 'Choose an event' => 'Wybierz zdarzenie', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Dodano kurs waluty', 'Unable to add this currency rate.' => 'Nie można dodać kursu waluty', 'Webhook URL' => 'Adres webhooka', - '%s remove the assignee of the task %s' => '%s usunął osobę przypisaną do zadania %s', + '%s removed the assignee of the task %s' => '%s usunął osobę przypisaną do zadania %s', 'Enable Gravatar images' => 'Włącz Gravatar', 'Information' => 'Informacje', 'Check two factor authentication code' => 'Sprawdź kod weryfikujący', diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 7b64f0e7..46749043 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s atualizou a tarefa #%d', '%s created the task #%d' => '%s criou a tarefa #%d', '%s closed the task #%d' => '%s finalizou a tarefa #%d', - '%s open the task #%d' => '%s abriu a tarefa #%d', + '%s opened the task #%d' => '%s abriu a tarefa #%d', '%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"', 'Activity' => 'Atividade', 'Default values are "%s"' => 'Os valores padrão são "%s"', 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projetos (Separado por vírgula)', 'Task assignee change' => 'Mudar designação da tarefa', - '%s change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s', + '%s changed the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s', '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', 'Choose an event' => 'Escolher um evento', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'A taxa de câmbio foi adicionada com sucesso.', 'Unable to add this currency rate.' => 'Impossível de adicionar essa taxa de câmbio.', 'Webhook URL' => 'URL do webhook', - '%s remove the assignee of the task %s' => '%s removeu a pessoa designada para a tarefa %s', + '%s removed the assignee of the task %s' => '%s removeu a pessoa designada para a tarefa %s', 'Enable Gravatar images' => 'Ativar imagens do Gravatar', 'Information' => 'Informações', 'Check two factor authentication code' => 'Verifique o código de autenticação em duas etapas', diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 5267b03b..4fd070d1 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s actualizou a tarefa #%d', '%s created the task #%d' => '%s criou a tarefa #%d', '%s closed the task #%d' => '%s finalizou a tarefa #%d', - '%s open the task #%d' => '%s abriu a tarefa #%d', + '%s opened the task #%d' => '%s abriu a tarefa #%d', '%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"', 'Activity' => 'Actividade', 'Default values are "%s"' => 'Os valores padrão são "%s"', 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projectos (Separado por vírgula)', 'Task assignee change' => 'Mudar assignação da tarefa', - '%s change the assignee of the task #%d to %s' => '%s mudou a assignação da tarefa #%d para %s', + '%s changed the assignee of the task #%d to %s' => '%s mudou a assignação da tarefa #%d para %s', '%s changed the assignee of the task %s to %s' => '%s mudou a assignação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o utilizador "%s"', 'Choose an event' => 'Escolher um evento', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'A taxa de câmbio foi adicionada com sucesso.', 'Unable to add this currency rate.' => 'Impossível adicionar essa taxa de câmbio.', 'Webhook URL' => 'URL do webhook', - '%s remove the assignee of the task %s' => '%s removeu a pessoa assignada à tarefa %s', + '%s removed the assignee of the task %s' => '%s removeu a pessoa assignada à tarefa %s', 'Enable Gravatar images' => 'Activar imagem Gravatar', 'Information' => 'Informações', 'Check two factor authentication code' => 'Verificação do código de autenticação com factor duplo', diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index b3682f03..92fba163 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s обновил задачу #%d', '%s created the task #%d' => '%s создал задачу #%d', '%s closed the task #%d' => '%s закрыл задачу #%d', - '%s open the task #%d' => '%s открыл задачу #%d', + '%s opened the task #%d' => '%s открыл задачу #%d', '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"', 'Activity' => 'Активность', 'Default values are "%s"' => 'Колонки по умолчанию: "%s"', 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)', 'Task assignee change' => 'Изменен назначенный', - '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', + '%s changed the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', '%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s', 'New password for the user "%s"' => 'Новый пароль для пользователя "%s"', 'Choose an event' => 'Выберите событие', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Курс валюты был успешно добавлен.', 'Unable to add this currency rate.' => 'Невозможно добавить этот курс валюты.', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s удалить назначенную задачу %s', + '%s removed the assignee of the task %s' => '%s удалить назначенную задачу %s', 'Enable Gravatar images' => 'Включить Gravatar изображения', 'Information' => 'Информация', 'Check two factor authentication code' => 'Проверка кода двухфакторной авторизации', diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 157d9e2d..6a4bfc68 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s izmenjen zadatak #%d', '%s created the task #%d' => '%s kreirao zadatak #%d', '%s closed the task #%d' => '%s zatvorio zadatak #%d', - '%s open the task #%d' => '%s otvorio zadatak #%d', + '%s opened the task #%d' => '%s otvorio zadatak #%d', '%s moved the task #%d to the column "%s"' => '%s premestio zadatak #%d u kolonu "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s premestio zadatak #%d na pozycję %d w kolmnie "%s"', 'Activity' => 'Aktivnosti', 'Default values are "%s"' => 'Osnovne vrednosti su: "%s"', 'Default columns for new projects (Comma-separated)' => 'Osnovne kolone za novi projekat (Odvojeni zarezom)', 'Task assignee change' => 'Zmień osobę odpowiedzialną', - '%s change the assignee of the task #%d to %s' => '%s zamena dodele za zadatak #%d na %s', + '%s changed the assignee of the task #%d to %s' => '%s zamena dodele za zadatak #%d na %s', '%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s', 'New password for the user "%s"' => 'Nova lozinka za korisnika "%s"', 'Choose an event' => 'Izaberi događaj', @@ -601,7 +601,7 @@ return array( // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', + // '%s removed the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', // 'Check two factor authentication code' => '', diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index e42a801d..7eb46a98 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s uppdaterade uppgiften #%d', '%s created the task #%d' => '%s skapade uppgiften #%d', '%s closed the task #%d' => '%s stängde uppgiften #%d', - '%s open the task #%d' => '%s öppnade uppgiften #%d', + '%s opened the task #%d' => '%s öppnade uppgiften #%d', '%s moved the task #%d to the column "%s"' => '%s flyttade uppgiften #%d till kolumnen "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s flyttade uppgiften #%d till positionen %d i kolumnen "%s"', 'Activity' => 'Aktivitet', 'Default values are "%s"' => 'Standardvärden är "%s"', 'Default columns for new projects (Comma-separated)' => 'Standardkolumner för nya projekt (kommaseparerade)', 'Task assignee change' => 'Ändra tilldelning av uppgiften', - '%s change the assignee of the task #%d to %s' => '%s byt tilldelning av uppgiften #%d till %s', + '%s changed the assignee of the task #%d to %s' => '%s byt tilldelning av uppgiften #%d till %s', '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s', 'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"', 'Choose an event' => 'Välj en händelse', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Valutakursen har lagts till.', 'Unable to add this currency rate.' => 'Kunde inte lägga till valutakursen.', 'Webhook URL' => 'Webhook URL', - '%s remove the assignee of the task %s' => '%s ta bort tilldelningen av uppgiften %s', + '%s removed the assignee of the task %s' => '%s ta bort tilldelningen av uppgiften %s', 'Enable Gravatar images' => 'Aktivera Gravatar bilder', 'Information' => 'Information', 'Check two factor authentication code' => 'Kolla tvåfaktorsverifieringskod', diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 56adbdb8..65979753 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s ปรับปรุงงานแล้ว #%d', '%s created the task #%d' => '%s สร้างงานแล้ว #%d', '%s closed the task #%d' => '%s ปิดงานแล้ว #%d', - '%s open the task #%d' => '%s เปิดงานแล้ว #%d', + '%s opened the task #%d' => '%s เปิดงานแล้ว #%d', '%s moved the task #%d to the column "%s"' => '%s ย้ายงานแล้ว #%d ไปที่คอลัมน์ "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s ย้ายงานแล้ว #%d ไปตำแหน่ง %d ในคอลัมน์ที่ "%s"', 'Activity' => 'กิจกรรม', 'Default values are "%s"' => 'ค่าเริ่มต้น "%s"', 'Default columns for new projects (Comma-separated)' => 'คอลัมน์เริ่มต้นสำหรับโปรเจคใหม่ (Comma-separated)', 'Task assignee change' => 'เปลี่ยนการกำหนดบุคคลของงาน', - '%s change the assignee of the task #%d to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน #%d เป็น %s', + '%s changed the assignee of the task #%d to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน #%d เป็น %s', '%s changed the assignee of the task %s to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน %s เป็น %s', 'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"', 'Choose an event' => 'เลือกเหตุการณ์', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'เพิ่มอัตราค่าเงินเรียบร้อย', 'Unable to add this currency rate.' => 'ไม่สามารถเพิ่มค่าเงินนี้', // 'Webhook URL' => '', - '%s remove the assignee of the task %s' => '%s เอาผู้รับผิดชอบออกจากงาน %s', + '%s removed the assignee of the task %s' => '%s เอาผู้รับผิดชอบออกจากงาน %s', 'Enable Gravatar images' => 'สามารถใช้งานภาพ Gravatar', 'Information' => 'ข้อมูลสารสนเทศ', // 'Check two factor authentication code' => '', diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 4f4c84cd..5a1b84b8 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s kullanıcısı #%d nolu görevi güncelledi', '%s created the task #%d' => '%s kullanıcısı #%d nolu görevi oluşturdu', '%s closed the task #%d' => '%s kullanıcısı #%d nolu görevi kapattı', - '%s open the task #%d' => '%s kullanıcısı #%d nolu görevi açtı', + '%s opened the task #%d' => '%s kullanıcısı #%d nolu görevi açtı', '%s moved the task #%d to the column "%s"' => '%s kullanıcısı #%d nolu görevi "%s" sütununa taşıdı', '%s moved the task #%d to the position %d in the column "%s"' => '%s kullanıcısı #%d nolu görevi %d pozisyonu "%s" sütununa taşıdı', 'Activity' => 'Aktivite', 'Default values are "%s"' => 'Varsayılan değerler "%s"', 'Default columns for new projects (Comma-separated)' => 'Yeni projeler için varsayılan sütunlar (virgül ile ayrılmış)', 'Task assignee change' => 'Göreve atanan kullanıcı değişikliği', - '%s change the assignee of the task #%d to %s' => '%s kullanıcısı #%d nolu görevin sorumlusunu %s olarak değiştirdi', + '%s changed the assignee of the task #%d to %s' => '%s kullanıcısı #%d nolu görevin sorumlusunu %s olarak değiştirdi', '%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi', 'New password for the user "%s"' => '"%s" kullanıcısı için yeni şifre', 'Choose an event' => 'Bir durum seçin', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => 'Kur başarıyla eklendi', 'Unable to add this currency rate.' => 'Bu kur eklenemedi', // 'Webhook URL' => '', - '%s remove the assignee of the task %s' => '%s, %s görevinin atanan bilgisini kaldırdı', + '%s removed the assignee of the task %s' => '%s, %s görevinin atanan bilgisini kaldırdı', 'Enable Gravatar images' => 'Gravatar resimlerini kullanıma aç', 'Information' => 'Bilgi', 'Check two factor authentication code' => 'İki kademeli doğrulama kodunu kontrol et', diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index 01eaff17..f173fdff 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -386,14 +386,14 @@ return array( '%s updated the task #%d' => '%s 更新了任务 #%d', '%s created the task #%d' => '%s 创建了任务 #%d', '%s closed the task #%d' => '%s 关闭了任务 #%d', - '%s open the task #%d' => '%s 开启了任务 #%d', + '%s opened the task #%d' => '%s 开启了任务 #%d', '%s moved the task #%d to the column "%s"' => '%s 将任务 #%d 移动到栏目 "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s将任务#%d移动到"%s"的第 %d 列', 'Activity' => '动态', 'Default values are "%s"' => '默认值为 "%s"', 'Default columns for new projects (Comma-separated)' => '新建项目的默认栏目(用逗号分开)', 'Task assignee change' => '任务分配变更', - '%s change the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s', + '%s changed the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s', '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s', 'New password for the user "%s"' => '用户"%s"的新密码', 'Choose an event' => '选择一个事件', @@ -601,7 +601,7 @@ return array( 'The currency rate have been added successfully.' => '成功添加汇率。', 'Unable to add this currency rate.' => '无法添加此汇率', 'Webhook URL' => '网络钩子 URL', - '%s remove the assignee of the task %s' => '%s删除了任务%s的负责人', + '%s removed the assignee of the task %s' => '%s删除了任务%s的负责人', 'Enable Gravatar images' => '启用 Gravatar 图像', 'Information' => '信息', 'Check two factor authentication code' => '检查双重认证码', diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 39c1f581..803d4f18 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -3,10 +3,15 @@ namespace Kanboard\Model; use Kanboard\Core\Base; +use Kanboard\EventBuilder\CommentEventBuilder; +use Kanboard\EventBuilder\EventIteratorBuilder; +use Kanboard\EventBuilder\SubtaskEventBuilder; +use Kanboard\EventBuilder\TaskEventBuilder; +use Kanboard\EventBuilder\TaskFileEventBuilder; use Kanboard\EventBuilder\TaskLinkEventBuilder; /** - * Notification + * Notification Model * * @package Kanboard\Model * @author Frederic Guillot @@ -17,150 +22,79 @@ class NotificationModel extends Base * Get the event title with author * * @access public - * @param string $event_author - * @param string $event_name - * @param array $event_data + * @param string $eventAuthor + * @param string $eventName + * @param array $eventData * @return string */ - public function getTitleWithAuthor($event_author, $event_name, array $event_data) + public function getTitleWithAuthor($eventAuthor, $eventName, array $eventData) { - switch ($event_name) { - case TaskModel::EVENT_ASSIGNEE_CHANGE: - $assignee = $event_data['task']['assignee_name'] ?: $event_data['task']['assignee_username']; + foreach ($this->getIteratorBuilder() as $builder) { + $title = $builder->buildTitleWithAuthor($eventAuthor, $eventName, $eventData); - if (! empty($assignee)) { - return e('%s change the assignee of the task #%d to %s', $event_author, $event_data['task']['id'], $assignee); - } - - return e('%s remove the assignee of the task %s', $event_author, e('#%d', $event_data['task']['id'])); - case TaskModel::EVENT_UPDATE: - return e('%s updated the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_CREATE: - return e('%s created the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_CLOSE: - return e('%s closed the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_OPEN: - return e('%s open the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_MOVE_COLUMN: - return e( - '%s moved the task #%d to the column "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['column_title'] - ); - case TaskModel::EVENT_MOVE_POSITION: - return e( - '%s moved the task #%d to the position %d in the column "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['position'], - $event_data['task']['column_title'] - ); - case TaskModel::EVENT_MOVE_SWIMLANE: - if ($event_data['task']['swimlane_id'] == 0) { - return e('%s moved the task #%d to the first swimlane', $event_author, $event_data['task']['id']); - } - - return e( - '%s moved the task #%d to the swimlane "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['swimlane_name'] - ); - case SubtaskModel::EVENT_UPDATE: - return e('%s updated a subtask for the task #%d', $event_author, $event_data['task']['id']); - case SubtaskModel::EVENT_CREATE: - return e('%s created a subtask for the task #%d', $event_author, $event_data['task']['id']); - case SubtaskModel::EVENT_DELETE: - return e('%s removed a subtask for the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_UPDATE: - return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_CREATE: - return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_DELETE: - return e('%s removed a comment on the task #%d', $event_author, $event_data['task']['id']); - case TaskFileModel::EVENT_CREATE: - return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_USER_MENTION: - return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_USER_MENTION: - return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); - default: - return TaskLinkEventBuilder::getInstance($this->container) - ->buildTitleWithAuthor($event_author, $event_name, $event_data) ?: - e('Notification'); + if ($title !== '') { + return $title; + } } + + return e('Notification'); } /** * Get the event title without author * * @access public - * @param string $event_name - * @param array $event_data + * @param string $eventName + * @param array $eventData * @return string */ - public function getTitleWithoutAuthor($event_name, array $event_data) + public function getTitleWithoutAuthor($eventName, array $eventData) { - switch ($event_name) { - case TaskFileModel::EVENT_CREATE: - return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - case CommentModel::EVENT_CREATE: - return e('New comment on task #%d', $event_data['comment']['task_id']); - case CommentModel::EVENT_UPDATE: - return e('Comment updated on task #%d', $event_data['comment']['task_id']); - case CommentModel::EVENT_DELETE: - return e('Comment removed on task #%d', $event_data['comment']['task_id']); - case SubtaskModel::EVENT_CREATE: - return e('New subtask on task #%d', $event_data['subtask']['task_id']); - case SubtaskModel::EVENT_UPDATE: - return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - case SubtaskModel::EVENT_DELETE: - return e('Subtask removed on task #%d', $event_data['subtask']['task_id']); - case TaskModel::EVENT_CREATE: - return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - case TaskModel::EVENT_UPDATE: - return e('Task updated #%d', $event_data['task']['id']); - case TaskModel::EVENT_CLOSE: - return e('Task #%d closed', $event_data['task']['id']); - case TaskModel::EVENT_OPEN: - return e('Task #%d opened', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_COLUMN: - return e('Column changed for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_POSITION: - return e('New position for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_SWIMLANE: - return e('Swimlane changed for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_ASSIGNEE_CHANGE: - return e('Assignee changed on task #%d', $event_data['task']['id']); - case TaskModel::EVENT_OVERDUE: - $nb = count($event_data['tasks']); - return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - case TaskModel::EVENT_USER_MENTION: - return e('You were mentioned in the task #%d', $event_data['task']['id']); - case CommentModel::EVENT_USER_MENTION: - return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); - default: - return TaskLinkEventBuilder::getInstance($this->container) - ->buildTitleWithoutAuthor($event_name, $event_data) ?: - e('Notification'); + foreach ($this->getIteratorBuilder() as $builder) { + $title = $builder->buildTitleWithoutAuthor($eventName, $eventData); + + if ($title !== '') { + return $title; + } } + + return e('Notification'); } /** * Get task id from event * * @access public - * @param string $event_name - * @param array $event_data + * @param string $eventName + * @param array $eventData * @return integer */ - public function getTaskIdFromEvent($event_name, array $event_data) + public function getTaskIdFromEvent($eventName, array $eventData) { - if ($event_name === TaskModel::EVENT_OVERDUE) { - return $event_data['tasks'][0]['id']; + if ($eventName === TaskModel::EVENT_OVERDUE) { + return $eventData['tasks'][0]['id']; } - - return isset($event_data['task']['id']) ? $event_data['task']['id'] : 0; + + return isset($eventData['task']['id']) ? $eventData['task']['id'] : 0; + } + + /** + * Get iterator builder + * + * @access protected + * @return EventIteratorBuilder + */ + protected function getIteratorBuilder() + { + $iterator = new EventIteratorBuilder(); + $iterator + ->withBuilder(TaskEventBuilder::getInstance($this->container)) + ->withBuilder(CommentEventBuilder::getInstance($this->container)) + ->withBuilder(SubtaskEventBuilder::getInstance($this->container)) + ->withBuilder(TaskFileEventBuilder::getInstance($this->container)) + ->withBuilder(TaskLinkEventBuilder::getInstance($this->container)) + ; + + return $iterator; } } diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php index 7c962223..7539cd0b 100644 --- a/app/Template/event/task_assignee_change.php +++ b/app/Template/event/task_assignee_change.php @@ -8,7 +8,7 @@ $this->text->e($assignee) ) ?> - text->e($author), $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + text->e($author), $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> dt->datetime($date_creation) ?>

diff --git a/tests/units/Action/TaskAssignCategoryLinkTest.php b/tests/units/Action/TaskAssignCategoryLinkTest.php index b9d7e9d9..1576f81b 100644 --- a/tests/units/Action/TaskAssignCategoryLinkTest.php +++ b/tests/units/Action/TaskAssignCategoryLinkTest.php @@ -33,7 +33,7 @@ class TaskAssignCategoryLinkTest extends Base $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId(1) - ->build(); + ->buildEvent(); $this->assertTrue($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); @@ -62,7 +62,7 @@ class TaskAssignCategoryLinkTest extends Base $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId(1) - ->build(); + ->buildEvent(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); @@ -91,7 +91,7 @@ class TaskAssignCategoryLinkTest extends Base $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId(1) - ->build(); + ->buildEvent(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); diff --git a/tests/units/Action/TaskAssignColorLinkTest.php b/tests/units/Action/TaskAssignColorLinkTest.php index 27364bc9..77a6c90e 100644 --- a/tests/units/Action/TaskAssignColorLinkTest.php +++ b/tests/units/Action/TaskAssignColorLinkTest.php @@ -30,7 +30,7 @@ class TaskAssignColorLinkTest extends Base $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId(1) - ->build(); + ->buildEvent(); $this->assertTrue($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); @@ -57,7 +57,7 @@ class TaskAssignColorLinkTest extends Base $event = TaskLinkEventBuilder::getInstance($this->container) ->withTaskLinkId(1) - ->build(); + ->buildEvent(); $this->assertFalse($action->execute($event, TaskLinkModel::EVENT_CREATE_UPDATE)); diff --git a/tests/units/EventBuilder/CommentEventBuilderTest.php b/tests/units/EventBuilder/CommentEventBuilderTest.php index a490799e..2f6a90b5 100644 --- a/tests/units/EventBuilder/CommentEventBuilderTest.php +++ b/tests/units/EventBuilder/CommentEventBuilderTest.php @@ -13,7 +13,7 @@ class CommentEventBuilderTest extends Base { $commentEventBuilder = new CommentEventBuilder($this->container); $commentEventBuilder->withCommentId(42); - $this->assertNull($commentEventBuilder->build()); + $this->assertNull($commentEventBuilder->buildEvent()); } public function testBuild() @@ -28,7 +28,7 @@ class CommentEventBuilderTest extends Base $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'comment' => 'bla bla', 'user_id' => 1))); $commentEventBuilder->withCommentId(1); - $event = $commentEventBuilder->build(); + $event = $commentEventBuilder->buildEvent(); $this->assertInstanceOf('Kanboard\Event\CommentEvent', $event); $this->assertNotEmpty($event['comment']); diff --git a/tests/units/EventBuilder/ProjectFileEventBuilderTest.php b/tests/units/EventBuilder/ProjectFileEventBuilderTest.php index bfe22719..8f5eb87e 100644 --- a/tests/units/EventBuilder/ProjectFileEventBuilderTest.php +++ b/tests/units/EventBuilder/ProjectFileEventBuilderTest.php @@ -12,7 +12,7 @@ class ProjectFileEventBuilderTest extends Base { $projectFileEventBuilder = new ProjectFileEventBuilder($this->container); $projectFileEventBuilder->withFileId(42); - $this->assertNull($projectFileEventBuilder->build()); + $this->assertNull($projectFileEventBuilder->buildEvent()); } public function testBuild() @@ -24,7 +24,7 @@ class ProjectFileEventBuilderTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $projectFileModel->create(1, 'Test', '/tmp/test', 123)); - $event = $projectFileEventBuilder->withFileId(1)->build(); + $event = $projectFileEventBuilder->withFileId(1)->buildEvent(); $this->assertInstanceOf('Kanboard\Event\ProjectFileEvent', $event); $this->assertNotEmpty($event['file']); diff --git a/tests/units/EventBuilder/SubtaskEventBuilderTest.php b/tests/units/EventBuilder/SubtaskEventBuilderTest.php index 062bdfb4..fe425cb8 100644 --- a/tests/units/EventBuilder/SubtaskEventBuilderTest.php +++ b/tests/units/EventBuilder/SubtaskEventBuilderTest.php @@ -13,7 +13,7 @@ class SubtaskEventBuilderTest extends Base { $subtaskEventBuilder = new SubtaskEventBuilder($this->container); $subtaskEventBuilder->withSubtaskId(42); - $this->assertNull($subtaskEventBuilder->build()); + $this->assertNull($subtaskEventBuilder->buildEvent()); } public function testBuildWithoutChanges() @@ -27,7 +27,7 @@ class SubtaskEventBuilderTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'test'))); - $event = $subtaskEventBuilder->withSubtaskId(1)->build(); + $event = $subtaskEventBuilder->withSubtaskId(1)->buildEvent(); $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); $this->assertNotEmpty($event['subtask']); @@ -49,7 +49,7 @@ class SubtaskEventBuilderTest extends Base $event = $subtaskEventBuilder ->withSubtaskId(1) ->withValues(array('title' => 'new title', 'user_id' => 1)) - ->build(); + ->buildEvent(); $this->assertInstanceOf('Kanboard\Event\SubtaskEvent', $event); $this->assertNotEmpty($event['subtask']); diff --git a/tests/units/EventBuilder/TaskEventBuilderTest.php b/tests/units/EventBuilder/TaskEventBuilderTest.php index e6334fe2..c89dcd85 100644 --- a/tests/units/EventBuilder/TaskEventBuilderTest.php +++ b/tests/units/EventBuilder/TaskEventBuilderTest.php @@ -12,7 +12,7 @@ class TaskEventBuilderTest extends Base { $taskEventBuilder = new TaskEventBuilder($this->container); $taskEventBuilder->withTaskId(42); - $this->assertNull($taskEventBuilder->build()); + $this->assertNull($taskEventBuilder->buildEvent()); } public function testBuildWithTask() @@ -28,7 +28,7 @@ class TaskEventBuilderTest extends Base ->withTaskId(1) ->withTask(array('title' => 'before')) ->withChanges(array('title' => 'after')) - ->build(); + ->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); $this->assertNotEmpty($event['task']); @@ -45,7 +45,7 @@ class TaskEventBuilderTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); - $event = $taskEventBuilder->withTaskId(1)->build(); + $event = $taskEventBuilder->withTaskId(1)->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); $this->assertNotEmpty($event['task']); @@ -65,7 +65,7 @@ class TaskEventBuilderTest extends Base $event = $taskEventBuilder ->withTaskId(1) ->withChanges(array('title' => 'new title')) - ->build(); + ->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); $this->assertNotEmpty($event['task']); @@ -86,7 +86,7 @@ class TaskEventBuilderTest extends Base ->withTaskId(1) ->withChanges(array('title' => 'new title', 'project_id' => 1)) ->withValues(array('key' => 'value')) - ->build(); + ->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event); $this->assertNotEmpty($event['task']); diff --git a/tests/units/EventBuilder/TaskFileEventBuilderTest.php b/tests/units/EventBuilder/TaskFileEventBuilderTest.php index c253b913..c90e18d3 100644 --- a/tests/units/EventBuilder/TaskFileEventBuilderTest.php +++ b/tests/units/EventBuilder/TaskFileEventBuilderTest.php @@ -13,7 +13,7 @@ class TaskFileEventBuilderTest extends Base { $taskFileEventBuilder = new TaskFileEventBuilder($this->container); $taskFileEventBuilder->withFileId(42); - $this->assertNull($taskFileEventBuilder->build()); + $this->assertNull($taskFileEventBuilder->buildEvent()); } public function testBuild() @@ -27,7 +27,7 @@ class TaskFileEventBuilderTest extends Base $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); $this->assertEquals(1, $taskFileModel->create(1, 'Test', '/tmp/test', 123)); - $event = $taskFileEventBuilder->withFileId(1)->build(); + $event = $taskFileEventBuilder->withFileId(1)->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskFileEvent', $event); $this->assertNotEmpty($event['file']); diff --git a/tests/units/EventBuilder/TaskLinkEventBuilderTest.php b/tests/units/EventBuilder/TaskLinkEventBuilderTest.php index 7364d651..18508146 100644 --- a/tests/units/EventBuilder/TaskLinkEventBuilderTest.php +++ b/tests/units/EventBuilder/TaskLinkEventBuilderTest.php @@ -13,7 +13,7 @@ class TaskLinkEventBuilderTest extends Base { $taskLinkEventBuilder = new TaskLinkEventBuilder($this->container); $taskLinkEventBuilder->withTaskLinkId(42); - $this->assertNull($taskLinkEventBuilder->build()); + $this->assertNull($taskLinkEventBuilder->buildEvent()); } public function testBuild() @@ -28,7 +28,7 @@ class TaskLinkEventBuilderTest extends Base $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); - $event = $taskLinkEventBuilder->withTaskLinkId(1)->build(); + $event = $taskLinkEventBuilder->withTaskLinkId(1)->buildEvent(); $this->assertInstanceOf('Kanboard\Event\TaskLinkEvent', $event); $this->assertNotEmpty($event['task_link']); @@ -47,7 +47,7 @@ class TaskLinkEventBuilderTest extends Base $this->assertEquals(2, $taskCreationModel->create(array('title' => 'task 2', 'project_id' => 1))); $this->assertEquals(1, $taskLinkModel->create(1, 2, 1)); - $eventData = $taskLinkEventBuilder->withTaskLinkId(1)->build(); + $eventData = $taskLinkEventBuilder->withTaskLinkId(1)->buildEvent(); $title = $taskLinkEventBuilder->buildTitleWithAuthor('Foobar', TaskLinkModel::EVENT_CREATE_UPDATE, $eventData->getAll()); $this->assertEquals('Foobar set a new internal link for the task #1', $title); -- cgit v1.2.3 From b179802a85b262529aaa46ed9cf072a570be25ce Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 14:58:40 -0400 Subject: Sync locales --- app/Locale/bs_BA/translations.php | 6 ++++++ app/Locale/cs_CZ/translations.php | 6 ++++++ app/Locale/da_DK/translations.php | 6 ++++++ app/Locale/de_DE/translations.php | 6 ++++++ app/Locale/el_GR/translations.php | 6 ++++++ app/Locale/es_ES/translations.php | 6 ++++++ app/Locale/fi_FI/translations.php | 6 ++++++ app/Locale/fr_FR/translations.php | 6 ++++++ app/Locale/hu_HU/translations.php | 6 ++++++ app/Locale/id_ID/translations.php | 6 ++++++ app/Locale/it_IT/translations.php | 6 ++++++ app/Locale/ja_JP/translations.php | 6 ++++++ app/Locale/ko_KR/translations.php | 6 ++++++ app/Locale/my_MY/translations.php | 6 ++++++ app/Locale/nb_NO/translations.php | 6 ++++++ app/Locale/nl_NL/translations.php | 6 ++++++ app/Locale/pl_PL/translations.php | 6 ++++++ app/Locale/pt_BR/translations.php | 6 ++++++ app/Locale/pt_PT/translations.php | 6 ++++++ app/Locale/ru_RU/translations.php | 6 ++++++ app/Locale/sr_Latn_RS/translations.php | 6 ++++++ app/Locale/sv_SE/translations.php | 6 ++++++ app/Locale/th_TH/translations.php | 6 ++++++ app/Locale/tr_TR/translations.php | 6 ++++++ app/Locale/zh_CN/translations.php | 6 ++++++ 25 files changed, 150 insertions(+) diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index f1529e02..908112df 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index c7e6e536..3606e6d2 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 6cecfaec..d1e86739 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index d25e7e8a..51998dc4 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index b02207d5..e2ea0a69 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index fa59ca07..088a4fbb 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 200a9cde..316c2089 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 9f6cf971..19ce49b4 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1227,4 +1227,10 @@ return array( '%s removed a subtask for the task %s' => '%s a supprimé une sous-tâche de la tâche %s', 'Comment removed' => 'Commentaire supprimé', 'Subtask removed' => 'Sous-tâche supprimée', + '%s set a new internal link for the task #%d' => '%s a défini un nouveau lien interne pour la tâche n°%d', + '%s removed an internal link for the task #%d' => '%s a supprimé un lien interne pour la tâche n°%d', + 'A new internal link for the task #%d have been defined' => 'Un nouveau lien interne pour la tâche n°%d a été défini', + 'Internal link removed for the task #%d' => 'Lien interne supprimé pour la tâche n°%d', + '%s set a new internal link for the task %s' => '%s a défini un nouveau lien interne pour la tâche %s', + '%s removed an internal link for the task %s' => '%s a supprimé un lien interne pour la tâche %s', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 781a0423..a0365940 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index 26e091ce..4cdfd129 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index aadbfe5b..334faa46 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 03fa55ed..fea7283d 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index bf140d94..7ba0f456 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index cf4f399c..68bd12bf 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index ce69deb9..c81e073c 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index d5ba7036..d0b90ef8 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index f2570d7c..7c28190a 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 46749043..3f5a6de5 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 4fd070d1..4e3e0151 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1226,4 +1226,10 @@ return array( '%s removed a subtask for the task %s' => '%s removeu uma sub-tarefa da tarefa %s', 'Comment removed' => 'Comentário removido', 'Subtask removed' => 'Sub-tarefa removida', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 92fba163..728a79f2 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 6a4bfc68..25779a15 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 7eb46a98..f4206bb1 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 65979753..ec399732 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 5a1b84b8..aa59f7e7 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index f173fdff..99d07561 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1226,4 +1226,10 @@ return array( // '%s removed a subtask for the task %s' => '', // 'Comment removed' => '', // 'Subtask removed' => '', + // '%s set a new internal link for the task #%d' => '', + // '%s removed an internal link for the task #%d' => '', + // 'A new internal link for the task #%d have been defined' => '', + // 'Internal link removed for the task #%d' => '', + // '%s set a new internal link for the task %s' => '', + // '%s removed an internal link for the task %s' => '', ); -- cgit v1.2.3 From d4606f69f6b0517f45f19d511a46004ae5dc7a5b Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 15:15:11 -0400 Subject: Minor cleanup --- .scrutinizer.yml | 11 ----------- app/Core/Base.php | 7 +++---- app/EventBuilder/EventIteratorBuilder.php | 4 ++++ config.default.php | 8 ++++---- 4 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 25ef09c4..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,11 +0,0 @@ -filter: - paths: - - app/* - excluded_paths: - - app/Schema/* - - app/Template/* - - app/Locale/* - - app/Library/* - - app/constants.php - - app/common.php - - app/check_setup.php diff --git a/app/Core/Base.php b/app/Core/Base.php index 20a2d391..6931d93a 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -185,10 +185,10 @@ abstract class Base } /** - * Load automatically models + * Load automatically dependencies * * @access public - * @param string $name Model name + * @param string $name Class name * @return mixed */ public function __get($name) @@ -206,7 +206,6 @@ abstract class Base */ public static function getInstance(Container $container) { - $self = new static($container); - return $self; + return new static($container); } } diff --git a/app/EventBuilder/EventIteratorBuilder.php b/app/EventBuilder/EventIteratorBuilder.php index afa146b6..ba821753 100644 --- a/app/EventBuilder/EventIteratorBuilder.php +++ b/app/EventBuilder/EventIteratorBuilder.php @@ -15,6 +15,10 @@ class EventIteratorBuilder implements Iterator { private $builders = array(); /** + * Set builder + * + * @access public + * @param BaseEventBuilder $builder * @return $this */ public function withBuilder(BaseEventBuilder $builder) diff --git a/config.default.php b/config.default.php index a9fd7d99..d0e93a8e 100644 --- a/config.default.php +++ b/config.default.php @@ -11,16 +11,16 @@ define('DEBUG', false); define('LOG_DRIVER', ''); // Log filename if the log driver is "file" -define('LOG_FILE', __DIR__.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); +define('LOG_FILE', DATA_DIR.DIRECTORY_SEPARATOR.'debug.log'); // Plugins directory -define('PLUGINS_DIR', 'plugins'); +define('PLUGINS_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'plugins'); // Folder for uploaded files -define('FILES_DIR', 'data'.DIRECTORY_SEPARATOR.'files'); +define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files'); // E-mail address for the "From" header (notifications) -define('MAIL_FROM', 'notifications@kanboard.local'); +define('MAIL_FROM', 'replace-me@kanboard.local'); // Mail transport available: "smtp", "sendmail", "mail" (PHP mail function), "postmark", "mailgun", "sendgrid" define('MAIL_TRANSPORT', 'mail'); -- cgit v1.2.3 From e0d330dda8dea91936d5b76e212603d106e45386 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 15:19:35 -0400 Subject: Move Github templates to a folder --- .github/issue_template.md | 26 ++++++++++++++++++++++++++ .github/pull_request_template.md | 8 ++++++++ CONTRIBUTING | 1 + issue_template.md | 26 -------------------------- pull_request_template.md | 8 -------- 5 files changed, 35 insertions(+), 34 deletions(-) create mode 100644 .github/issue_template.md create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING delete mode 100644 issue_template.md delete mode 100644 pull_request_template.md diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..3aede7f5 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,26 @@ +### Expected behaviour + +Tell us what should happen + + +### Actual behaviour + +Tell us what happens instead + + +### Steps to reproduce + +1. +2. +3. + + +### Configuration + +Copy and paste the configuration section from the Kanboard settings page or fill these fields: + +- Kanboard version: +- Database type and version: +- PHP version: +- OS: +- Browser: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..be59deac --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +Before to submit your pull-request: + +- Be sure that the unit tests pass +- If you create a new feature, test your code, do not introduce new bugs +- Avoid code duplication +- Small pull-requests are easier to review and can be merged quickly +- 1 pull-request == 1 feature/improvement +- Non necessary features should be implemented as plugin diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 00000000..68a20f51 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1 @@ +Read https://kanboard.net/documentation/contributing diff --git a/issue_template.md b/issue_template.md deleted file mode 100644 index 3aede7f5..00000000 --- a/issue_template.md +++ /dev/null @@ -1,26 +0,0 @@ -### Expected behaviour - -Tell us what should happen - - -### Actual behaviour - -Tell us what happens instead - - -### Steps to reproduce - -1. -2. -3. - - -### Configuration - -Copy and paste the configuration section from the Kanboard settings page or fill these fields: - -- Kanboard version: -- Database type and version: -- PHP version: -- OS: -- Browser: diff --git a/pull_request_template.md b/pull_request_template.md deleted file mode 100644 index be59deac..00000000 --- a/pull_request_template.md +++ /dev/null @@ -1,8 +0,0 @@ -Before to submit your pull-request: - -- Be sure that the unit tests pass -- If you create a new feature, test your code, do not introduce new bugs -- Avoid code duplication -- Small pull-requests are easier to review and can be merged quickly -- 1 pull-request == 1 feature/improvement -- Non necessary features should be implemented as plugin -- cgit v1.2.3 From 2a42e0e1aae35a9bb7abf054155b516ffab701d4 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 18:10:05 -0400 Subject: Added a new automatic action to set due date --- ChangeLog | 5 +- app/Action/TaskAssignDueDateOnCreation.php | 96 ++++++++++++++++++++++ app/ServiceProvider/ActionProvider.php | 2 + .../Action/TaskAssignDueDateOnCreationTest.php | 37 +++++++++ tests/units/Action/TaskUpdateStartDateTest.php | 3 +- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 app/Action/TaskAssignDueDateOnCreation.php create mode 100644 tests/units/Action/TaskAssignDueDateOnCreationTest.php diff --git a/ChangeLog b/ChangeLog index ee57c86c..01ad5fbd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,7 +3,9 @@ Version 1.0.32 (unreleased) New features: -* New automated action to close tasks without activity in a specific column +* New automated actions: + - Close tasks without activity in a specific column + - Set due date automatically * Added internal task links to activity stream * Added new event for removed comments * Added search filter for task priority @@ -11,6 +13,7 @@ New features: Improvements: +* Internal events management refactoring * Handle header X-Real-IP to get IP address * Display project name for task auto-complete fields * Make search attributes not case sensitive diff --git a/app/Action/TaskAssignDueDateOnCreation.php b/app/Action/TaskAssignDueDateOnCreation.php new file mode 100644 index 00000000..79ff765c --- /dev/null +++ b/app/Action/TaskAssignDueDateOnCreation.php @@ -0,0 +1,96 @@ + t('Duration in days') + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'task' => array( + 'project_id', + ), + ); + } + + /** + * Execute the action (set the task color) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $values = array( + 'id' => $data['task_id'], + 'date_due' => strtotime('+'.$this->getParam('duration').'days'), + ); + + return $this->taskModificationModel->update($values, false); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return true; + } +} diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index 9383be12..c76555fa 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -3,6 +3,7 @@ namespace Kanboard\ServiceProvider; use Kanboard\Action\TaskAssignColorPriority; +use Kanboard\Action\TaskAssignDueDateOnCreation; use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\Action\ActionManager; @@ -80,6 +81,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); $container['actionManager']->register(new TaskOpen($container)); $container['actionManager']->register(new TaskUpdateStartDate($container)); + $container['actionManager']->register(new TaskAssignDueDateOnCreation($container)); return $container; } diff --git a/tests/units/Action/TaskAssignDueDateOnCreationTest.php b/tests/units/Action/TaskAssignDueDateOnCreationTest.php new file mode 100644 index 00000000..26c0584e --- /dev/null +++ b/tests/units/Action/TaskAssignDueDateOnCreationTest.php @@ -0,0 +1,37 @@ +container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->buildEvent(); + + $action = new TaskAssignDueDateOnCreation($this->container); + $action->setProjectId(1); + $action->setParam('duration', 4); + + $this->assertTrue($action->execute($event, TaskModel::EVENT_CREATE)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(date('Y-m-d', strtotime('+4days')), date('Y-m-d', $task['date_due'])); + } +} diff --git a/tests/units/Action/TaskUpdateStartDateTest.php b/tests/units/Action/TaskUpdateStartDateTest.php index 8d609b3e..05fac100 100644 --- a/tests/units/Action/TaskUpdateStartDateTest.php +++ b/tests/units/Action/TaskUpdateStartDateTest.php @@ -2,7 +2,6 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Event\GenericEvent; use Kanboard\Event\TaskEvent; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\TaskFinderModel; @@ -12,7 +11,7 @@ use Kanboard\Action\TaskUpdateStartDate; class TaskUpdateStartDateTest extends Base { - public function testClose() + public function testAction() { $projectModel = new ProjectModel($this->container); $taskCreationModel = new TaskCreationModel($this->container); -- cgit v1.2.3 From 9b2a32af78ef8fb5424398dc57e3c3f906026272 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 18:33:31 -0400 Subject: Add new automatic action to move a task to another column when closed --- ChangeLog | 1 + app/Action/Base.php | 2 +- app/Action/CommentCreation.php | 2 +- app/Action/CommentCreationMoveTaskColumn.php | 2 +- app/Action/TaskAssignCategoryColor.php | 2 +- app/Action/TaskAssignCategoryLabel.php | 2 +- app/Action/TaskAssignCategoryLink.php | 2 +- app/Action/TaskAssignColorCategory.php | 2 +- app/Action/TaskAssignColorColumn.php | 2 +- app/Action/TaskAssignColorLink.php | 2 +- app/Action/TaskAssignColorPriority.php | 2 +- app/Action/TaskAssignColorUser.php | 2 +- app/Action/TaskAssignCurrentUser.php | 2 +- app/Action/TaskAssignCurrentUserColumn.php | 2 +- app/Action/TaskAssignDueDateOnCreation.php | 2 +- app/Action/TaskAssignSpecificUser.php | 2 +- app/Action/TaskAssignUser.php | 2 +- app/Action/TaskClose.php | 2 +- app/Action/TaskCloseColumn.php | 2 +- app/Action/TaskCloseNoActivity.php | 2 +- app/Action/TaskCloseNoActivityColumn.php | 2 +- app/Action/TaskCreation.php | 2 +- app/Action/TaskDuplicateAnotherProject.php | 2 +- app/Action/TaskEmail.php | 2 +- app/Action/TaskEmailNoActivity.php | 2 +- app/Action/TaskMoveAnotherProject.php | 2 +- app/Action/TaskMoveColumnAssigned.php | 2 +- app/Action/TaskMoveColumnCategoryChange.php | 2 +- app/Action/TaskMoveColumnClosed.php | 102 ++++++++++++++++++++++++ app/Action/TaskMoveColumnUnAssigned.php | 2 +- app/Action/TaskOpen.php | 2 +- app/Action/TaskUpdateStartDate.php | 2 +- app/Model/TaskPositionModel.php | 19 ++--- app/ServiceProvider/ActionProvider.php | 2 + tests/units/Action/TaskMoveColumnClosedTest.php | 91 +++++++++++++++++++++ 35 files changed, 236 insertions(+), 39 deletions(-) create mode 100644 app/Action/TaskMoveColumnClosed.php create mode 100644 tests/units/Action/TaskMoveColumnClosedTest.php diff --git a/ChangeLog b/ChangeLog index 01ad5fbd..c9aebc48 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,7 @@ New features: * New automated actions: - Close tasks without activity in a specific column - Set due date automatically + - Move a task to another column when closed * Added internal task links to activity stream * Added new event for removed comments * Added search filter for task priority diff --git a/app/Action/Base.php b/app/Action/Base.php index e0ed8bde..9a502a08 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -7,7 +7,7 @@ use Kanboard\Event\GenericEvent; /** * Base class for automatic actions * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ abstract class Base extends \Kanboard\Core\Base diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php index 60ca24f7..301d2cf9 100644 --- a/app/Action/CommentCreation.php +++ b/app/Action/CommentCreation.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Create automatically a comment from a webhook * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class CommentCreation extends Base diff --git a/app/Action/CommentCreationMoveTaskColumn.php b/app/Action/CommentCreationMoveTaskColumn.php index 8ab792ad..d5bdd807 100644 --- a/app/Action/CommentCreationMoveTaskColumn.php +++ b/app/Action/CommentCreationMoveTaskColumn.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Add a comment of the triggering event to the task description. * - * @package action + * @package Kanboard\Action * @author Oren Ben-Kiki */ class CommentCreationMoveTaskColumn extends Base diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php index 2df90b2c..9228e1ff 100644 --- a/app/Action/TaskAssignCategoryColor.php +++ b/app/Action/TaskAssignCategoryColor.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Set a category automatically according to the color * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignCategoryColor extends Base diff --git a/app/Action/TaskAssignCategoryLabel.php b/app/Action/TaskAssignCategoryLabel.php index 48299010..c390414e 100644 --- a/app/Action/TaskAssignCategoryLabel.php +++ b/app/Action/TaskAssignCategoryLabel.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Set a category automatically according to a label * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignCategoryLabel extends Base diff --git a/app/Action/TaskAssignCategoryLink.php b/app/Action/TaskAssignCategoryLink.php index d4a4c0ec..6c4b6c96 100644 --- a/app/Action/TaskAssignCategoryLink.php +++ b/app/Action/TaskAssignCategoryLink.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskLinkModel; /** * Set a category automatically according to a task link * - * @package action + * @package Kanboard\Action * @author Olivier Maridat * @author Frederic Guillot */ diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php index 91860be4..a136ffd2 100644 --- a/app/Action/TaskAssignColorCategory.php +++ b/app/Action/TaskAssignColorCategory.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a color to a specific category * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignColorCategory extends Base diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php index 6c674b1f..da6e3aed 100644 --- a/app/Action/TaskAssignColorColumn.php +++ b/app/Action/TaskAssignColorColumn.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a color to a task * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignColorColumn extends Base diff --git a/app/Action/TaskAssignColorLink.php b/app/Action/TaskAssignColorLink.php index 9759f622..19c37afe 100644 --- a/app/Action/TaskAssignColorLink.php +++ b/app/Action/TaskAssignColorLink.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskLinkModel; /** * Assign a color to a specific task link * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignColorLink extends Base diff --git a/app/Action/TaskAssignColorPriority.php b/app/Action/TaskAssignColorPriority.php index 57000ba8..37f7ffed 100644 --- a/app/Action/TaskAssignColorPriority.php +++ b/app/Action/TaskAssignColorPriority.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a color to a priority * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignColorPriority extends Base diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php index 385db793..468d0198 100644 --- a/app/Action/TaskAssignColorUser.php +++ b/app/Action/TaskAssignColorUser.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a color to a specific user * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignColorUser extends Base diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php index 997aa98f..dee5e7db 100644 --- a/app/Action/TaskAssignCurrentUser.php +++ b/app/Action/TaskAssignCurrentUser.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a task to the logged user * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignCurrentUser extends Base diff --git a/app/Action/TaskAssignCurrentUserColumn.php b/app/Action/TaskAssignCurrentUserColumn.php index e4eade33..60ada7ef 100644 --- a/app/Action/TaskAssignCurrentUserColumn.php +++ b/app/Action/TaskAssignCurrentUserColumn.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a task to the logged user on column change * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignCurrentUserColumn extends Base diff --git a/app/Action/TaskAssignDueDateOnCreation.php b/app/Action/TaskAssignDueDateOnCreation.php index 79ff765c..5c6e2b61 100644 --- a/app/Action/TaskAssignDueDateOnCreation.php +++ b/app/Action/TaskAssignDueDateOnCreation.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Set the due date of task * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignDueDateOnCreation extends Base diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php index 2c7dcacd..daf9e1df 100644 --- a/app/Action/TaskAssignSpecificUser.php +++ b/app/Action/TaskAssignSpecificUser.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Assign a task to a specific user * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskAssignSpecificUser extends Base diff --git a/app/Action/TaskAssignUser.php b/app/Action/TaskAssignUser.php index 9ea22986..8727b672 100644 --- a/app/Action/TaskAssignUser.php +++ b/app/Action/TaskAssignUser.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Assign a task to someone * - * @package action + * @package Kanboard\Actionv * @author Frederic Guillot */ class TaskAssignUser extends Base diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php index 91e8cf43..e476e9ba 100644 --- a/app/Action/TaskClose.php +++ b/app/Action/TaskClose.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Close automatically a task * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskClose extends Base diff --git a/app/Action/TaskCloseColumn.php b/app/Action/TaskCloseColumn.php index 4f1ffc92..523996f4 100644 --- a/app/Action/TaskCloseColumn.php +++ b/app/Action/TaskCloseColumn.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Close automatically a task in a specific column * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskCloseColumn extends Base diff --git a/app/Action/TaskCloseNoActivity.php b/app/Action/TaskCloseNoActivity.php index 5a10510f..ea724d8c 100644 --- a/app/Action/TaskCloseNoActivity.php +++ b/app/Action/TaskCloseNoActivity.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Close automatically a task after when inactive * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskCloseNoActivity extends Base diff --git a/app/Action/TaskCloseNoActivityColumn.php b/app/Action/TaskCloseNoActivityColumn.php index 7af0b7fc..b2ee5224 100644 --- a/app/Action/TaskCloseNoActivityColumn.php +++ b/app/Action/TaskCloseNoActivityColumn.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Close automatically a task after inactive and in an defined column * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskCloseNoActivityColumn extends Base diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php index 0620afd3..01d91228 100644 --- a/app/Action/TaskCreation.php +++ b/app/Action/TaskCreation.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Create automatically a task from a webhook * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskCreation extends Base diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index d6d8d51f..0ad7713c 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Duplicate a task to another project * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskDuplicateAnotherProject extends Base diff --git a/app/Action/TaskEmail.php b/app/Action/TaskEmail.php index 526e9aa8..fdfe7987 100644 --- a/app/Action/TaskEmail.php +++ b/app/Action/TaskEmail.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Email a task to someone * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskEmail extends Base diff --git a/app/Action/TaskEmailNoActivity.php b/app/Action/TaskEmailNoActivity.php index c60702fb..cac4281e 100644 --- a/app/Action/TaskEmailNoActivity.php +++ b/app/Action/TaskEmailNoActivity.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Email a task with no activity * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskEmailNoActivity extends Base diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index 148b6b0c..0fa22b1b 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Move a task to another project * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskMoveAnotherProject extends Base diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php index 1c1f657a..1cfe6743 100644 --- a/app/Action/TaskMoveColumnAssigned.php +++ b/app/Action/TaskMoveColumnAssigned.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Move a task to another column when an assignee is set * - * @package action + * @package Kanboard\Action * @author Francois Ferrand */ class TaskMoveColumnAssigned extends Base diff --git a/app/Action/TaskMoveColumnCategoryChange.php b/app/Action/TaskMoveColumnCategoryChange.php index 4c2b289a..13d6ee4f 100644 --- a/app/Action/TaskMoveColumnCategoryChange.php +++ b/app/Action/TaskMoveColumnCategoryChange.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Move a task to another column when the category is changed * - * @package action + * @package Kanboard\Action * @author Francois Ferrand */ class TaskMoveColumnCategoryChange extends Base diff --git a/app/Action/TaskMoveColumnClosed.php b/app/Action/TaskMoveColumnClosed.php new file mode 100644 index 00000000..3f3e2124 --- /dev/null +++ b/app/Action/TaskMoveColumnClosed.php @@ -0,0 +1,102 @@ + t('Destination column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'task' => array( + 'project_id', + 'column_id', + 'swimlane_id', + 'is_active', + ) + ); + } + + /** + * Execute the action (move the task to another column) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return $this->taskPositionModel->movePosition( + $data['task']['project_id'], + $data['task']['id'], + $this->getParam('dest_column_id'), + 1, + $data['task']['swimlane_id'], + false, + false + ); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['task']['column_id'] != $this->getParam('dest_column_id') && $data['task']['is_active'] == 0; + } +} diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php index 0e9a8a16..ab63d624 100644 --- a/app/Action/TaskMoveColumnUnAssigned.php +++ b/app/Action/TaskMoveColumnUnAssigned.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Move a task to another column when an assignee is cleared * - * @package action + * @package Kanboard\Action * @author Francois Ferrand */ class TaskMoveColumnUnAssigned extends Base diff --git a/app/Action/TaskOpen.php b/app/Action/TaskOpen.php index 8e847b8e..49017831 100644 --- a/app/Action/TaskOpen.php +++ b/app/Action/TaskOpen.php @@ -5,7 +5,7 @@ namespace Kanboard\Action; /** * Open automatically a task * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskOpen extends Base diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php index cc016da1..160f6ee5 100644 --- a/app/Action/TaskUpdateStartDate.php +++ b/app/Action/TaskUpdateStartDate.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskModel; /** * Set the start date of task * - * @package action + * @package Kanboard\Action * @author Frederic Guillot */ class TaskUpdateStartDate extends Base diff --git a/app/Model/TaskPositionModel.php b/app/Model/TaskPositionModel.php index d6d2a0af..3d95a763 100644 --- a/app/Model/TaskPositionModel.php +++ b/app/Model/TaskPositionModel.php @@ -16,15 +16,16 @@ class TaskPositionModel extends Base * Move a task to another column or to another position * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be >= 1) - * @param integer $swimlane_id Swimlane id - * @param boolean $fire_events Fire events - * @return boolean + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @param boolean $fire_events Fire events + * @param bool $onlyOpen Do not move closed tasks + * @return bool */ - public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true) + public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true, $onlyOpen = true) { if ($position < 1) { return false; @@ -32,7 +33,7 @@ class TaskPositionModel extends Base $task = $this->taskFinderModel->getById($task_id); - if ($task['is_active'] == TaskModel::STATUS_CLOSED) { + if ($onlyOpen && $task['is_active'] == TaskModel::STATUS_CLOSED) { return true; } diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index c76555fa..cbc60679 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -4,6 +4,7 @@ namespace Kanboard\ServiceProvider; use Kanboard\Action\TaskAssignColorPriority; use Kanboard\Action\TaskAssignDueDateOnCreation; +use Kanboard\Action\TaskMoveColumnClosed; use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\Action\ActionManager; @@ -78,6 +79,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskMoveAnotherProject($container)); $container['actionManager']->register(new TaskMoveColumnAssigned($container)); $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); + $container['actionManager']->register(new TaskMoveColumnClosed($container)); $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); $container['actionManager']->register(new TaskOpen($container)); $container['actionManager']->register(new TaskUpdateStartDate($container)); diff --git a/tests/units/Action/TaskMoveColumnClosedTest.php b/tests/units/Action/TaskMoveColumnClosedTest.php new file mode 100644 index 00000000..318b995d --- /dev/null +++ b/tests/units/Action/TaskMoveColumnClosedTest.php @@ -0,0 +1,91 @@ +container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($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' => 'test', 'is_active' => 0))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->buildEvent(); + + $action = new TaskMoveColumnClosed($this->container); + $action->setProjectId(1); + $action->setParam('dest_column_id', 2); + + $this->assertTrue($action->execute($event, TaskModel::EVENT_CLOSE)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals('test', $task['title']); + $this->assertEquals(2, $task['column_id']); + } + + public function testWhenTaskIsOpen() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($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' => 'test'))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->buildEvent(); + + $action = new TaskMoveColumnClosed($this->container); + $action->setProjectId(1); + $action->setParam('dest_column_id', 2); + + $this->assertFalse($action->execute($event, TaskModel::EVENT_CLOSE)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals('test', $task['title']); + $this->assertEquals(1, $task['column_id']); + } + + public function testWhenTaskIsAlreadyInDestinationColumn() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($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' => 'test', 'is_active' => 0, 'column_id' => 2))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->buildEvent(); + + $action = new TaskMoveColumnClosed($this->container); + $action->setProjectId(1); + $action->setParam('dest_column_id', 2); + + $this->assertFalse($action->execute($event, TaskModel::EVENT_CLOSE)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals('test', $task['title']); + $this->assertEquals(2, $task['column_id']); + } +} -- cgit v1.2.3 From ca45b5592b17d3675a22b7aca49ea49dd9dd57ea Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 18:59:00 -0400 Subject: Add new automatic action to move the task to another column when not moved --- ChangeLog | 1 + app/Action/TaskMoveColumnNotMovedPeriod.php | 104 +++++++++++++++++++++ app/ServiceProvider/ActionProvider.php | 2 + .../Action/TaskMoveColumnNotMovedPeriodTest.php | 50 ++++++++++ 4 files changed, 157 insertions(+) create mode 100644 app/Action/TaskMoveColumnNotMovedPeriod.php create mode 100644 tests/units/Action/TaskMoveColumnNotMovedPeriodTest.php diff --git a/ChangeLog b/ChangeLog index c9aebc48..1bc0eed3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,7 @@ New features: - Close tasks without activity in a specific column - Set due date automatically - Move a task to another column when closed + - Move a task to another column when not moved during a given period * Added internal task links to activity stream * Added new event for removed comments * Added search filter for task priority diff --git a/app/Action/TaskMoveColumnNotMovedPeriod.php b/app/Action/TaskMoveColumnNotMovedPeriod.php new file mode 100644 index 00000000..87e7e405 --- /dev/null +++ b/app/Action/TaskMoveColumnNotMovedPeriod.php @@ -0,0 +1,104 @@ + t('Duration in days'), + 'src_column_id' => t('Source column'), + 'dest_column_id' => t('Destination column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('tasks'); + } + + /** + * Execute the action (close the task) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $results = array(); + $max = $this->getParam('duration') * 86400; + + foreach ($data['tasks'] as $task) { + $duration = time() - $task['date_moved']; + + if ($duration > $max && $task['column_id'] == $this->getParam('src_column_id')) { + $results[] = $this->taskPositionModel->movePosition( + $task['project_id'], + $task['id'], + $this->getParam('dest_column_id'), + 1, + $task['swimlane_id'], + false + ); + } + } + + return in_array(true, $results, true); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return count($data['tasks']) > 0; + } +} diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index cbc60679..946fbf41 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -5,6 +5,7 @@ namespace Kanboard\ServiceProvider; use Kanboard\Action\TaskAssignColorPriority; use Kanboard\Action\TaskAssignDueDateOnCreation; use Kanboard\Action\TaskMoveColumnClosed; +use Kanboard\Action\TaskMoveColumnNotMovedPeriod; use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\Action\ActionManager; @@ -80,6 +81,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskMoveColumnAssigned($container)); $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); $container['actionManager']->register(new TaskMoveColumnClosed($container)); + $container['actionManager']->register(new TaskMoveColumnNotMovedPeriod($container)); $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); $container['actionManager']->register(new TaskOpen($container)); $container['actionManager']->register(new TaskUpdateStartDate($container)); diff --git a/tests/units/Action/TaskMoveColumnNotMovedPeriodTest.php b/tests/units/Action/TaskMoveColumnNotMovedPeriodTest.php new file mode 100644 index 00000000..7fa16cf2 --- /dev/null +++ b/tests/units/Action/TaskMoveColumnNotMovedPeriodTest.php @@ -0,0 +1,50 @@ +container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 3))); + $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2))); + + $this->container['db']->table(TaskModel::TABLE)->in('id', array(2, 3))->update(array('date_moved' => strtotime('-10days'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskMoveColumnNotMovedPeriod($this->container); + $action->setProjectId(1); + $action->setParam('duration', 2); + $action->setParam('src_column_id', 2); + $action->setParam('dest_column_id', 3); + + $this->assertTrue($action->execute($event, TaskModel::EVENT_DAILY_CRONJOB)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['column_id']); + + $task = $taskFinderModel->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(3, $task['column_id']); + + $task = $taskFinderModel->getById(3); + $this->assertNotEmpty($task); + $this->assertEquals(3, $task['column_id']); + } +} -- 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(-) 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 adb5023cfc075ce5d6f73a4ba5b4ab51f6c500c0 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 20:30:06 -0400 Subject: Add unit test for ProjectMetricJob --- app/Core/Base.php | 1 + app/ServiceProvider/JobProvider.php | 5 +++ app/Subscriber/ProjectDailySummarySubscriber.php | 7 +--- tests/units/Job/ProjectMetricJobTest.php | 47 ++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 tests/units/Job/ProjectMetricJobTest.php diff --git a/app/Core/Base.php b/app/Core/Base.php index 6931d93a..41f5d2e0 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -157,6 +157,7 @@ use Pimple\Container; * @property \Kanboard\Job\TaskLinkEventJob $taskLinkEventJob * @property \Kanboard\Job\ProjectFileEventJob $projectFileEventJob * @property \Kanboard\Job\NotificationJob $notificationJob + * @property \Kanboard\Job\ProjectMetricJob $projectMetricJob * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher diff --git a/app/ServiceProvider/JobProvider.php b/app/ServiceProvider/JobProvider.php index 5b42794b..2194b11c 100644 --- a/app/ServiceProvider/JobProvider.php +++ b/app/ServiceProvider/JobProvider.php @@ -5,6 +5,7 @@ namespace Kanboard\ServiceProvider; use Kanboard\Job\CommentEventJob; use Kanboard\Job\NotificationJob; use Kanboard\Job\ProjectFileEventJob; +use Kanboard\Job\ProjectMetricJob; use Kanboard\Job\SubtaskEventJob; use Kanboard\Job\TaskEventJob; use Kanboard\Job\TaskFileEventJob; @@ -57,6 +58,10 @@ class JobProvider implements ServiceProviderInterface return new NotificationJob($c); }); + $container['projectMetricJob'] = $container->factory(function ($c) { + return new ProjectMetricJob($c); + }); + return $container; } } diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php index 7e3c11c3..eaa9d468 100644 --- a/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -3,7 +3,6 @@ namespace Kanboard\Subscriber; use Kanboard\Event\TaskEvent; -use Kanboard\Job\ProjectMetricJob; use Kanboard\Model\TaskModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -22,9 +21,7 @@ class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubsc public function execute(TaskEvent $event) { - if (isset($event['project_id'])) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->queueManager->push(ProjectMetricJob::getInstance($this->container)->withParams($event['project_id'])); - } + $this->logger->debug('Subscriber executed: '.__METHOD__); + $this->queueManager->push($this->projectMetricJob->withParams($event['task']['project_id'])); } } diff --git a/tests/units/Job/ProjectMetricJobTest.php b/tests/units/Job/ProjectMetricJobTest.php new file mode 100644 index 00000000..e5b0474d --- /dev/null +++ b/tests/units/Job/ProjectMetricJobTest.php @@ -0,0 +1,47 @@ +container); + $projectMetricJob->withParams(123); + + $this->assertSame( + array(123), + $projectMetricJob->getJobParams() + ); + } + + public function testJob() + { + $this->container['projectDailyColumnStatsModel'] = $this + ->getMockBuilder('\Kanboard\Model\ProjectDailyColumnStatsModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('updateTotals')) + ->getMock(); + + $this->container['projectDailyStatsModel'] = $this + ->getMockBuilder('\Kanboard\Model\ProjectDailyStatsModel') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('updateTotals')) + ->getMock(); + + $this->container['projectDailyColumnStatsModel'] + ->expects($this->once()) + ->method('updateTotals') + ->with(42, date('Y-m-d')); + + $this->container['projectDailyStatsModel'] + ->expects($this->once()) + ->method('updateTotals') + ->with(42, date('Y-m-d')); + + $job = new ProjectMetricJob($this->container); + $job->execute(42); + } +} -- cgit v1.2.3 From 220bc9cdcc483e71d5df629e9c7eb26c562b969f Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 20:58:16 -0400 Subject: Add unit test RecurringTaskSubscriber --- app/Subscriber/RecurringTaskSubscriber.php | 14 +- .../Subscriber/RecurringTaskSubscriberTest.php | 164 +++++++++++++++++++++ 2 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 tests/units/Subscriber/RecurringTaskSubscriberTest.php diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php index 21cd3996..3e2848f8 100644 --- a/app/Subscriber/RecurringTaskSubscriber.php +++ b/app/Subscriber/RecurringTaskSubscriber.php @@ -19,12 +19,13 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI public function onMove(TaskEvent $event) { $this->logger->debug('Subscriber executed: '.__METHOD__); + $task = $event['task']; - if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { - if ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_FIRST_COLUMN && $this->columnModel->getFirstColumnId($event['project_id']) == $event['src_column_id']) { - $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); - } elseif ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_LAST_COLUMN && $this->columnModel->getLastColumnId($event['project_id']) == $event['dst_column_id']) { - $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); + if ($task['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { + if ($task['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_FIRST_COLUMN && $this->columnModel->getFirstColumnId($task['project_id']) == $event['src_column_id']) { + $this->taskRecurrenceModel->duplicateRecurringTask($task['id']); + } elseif ($task['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_LAST_COLUMN && $this->columnModel->getLastColumnId($task['project_id']) == $event['dst_column_id']) { + $this->taskRecurrenceModel->duplicateRecurringTask($task['id']); } } } @@ -32,8 +33,9 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI public function onClose(TaskEvent $event) { $this->logger->debug('Subscriber executed: '.__METHOD__); + $task = $event['task']; - if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_CLOSE) { + if ($task['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING && $task['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_CLOSE) { $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } } diff --git a/tests/units/Subscriber/RecurringTaskSubscriberTest.php b/tests/units/Subscriber/RecurringTaskSubscriberTest.php new file mode 100644 index 00000000..d6aba7cf --- /dev/null +++ b/tests/units/Subscriber/RecurringTaskSubscriberTest.php @@ -0,0 +1,164 @@ +container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(1, $taskFinderModel->countByProjectId(1)); + } + + public function testWithRecurrenceFirstColumn() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array( + 'title' => 'test', + 'project_id' => 1, + 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING, + 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, + ))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->withValues(array('src_column_id' => 1)) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(2, $taskFinderModel->countByProjectId(1)); + } + + public function testWithRecurrenceFirstColumnWithWrongColumn() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array( + 'title' => 'test', + 'project_id' => 1, + 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING, + 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, + 'column_id' => 2, + ))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->withValues(array('src_column_id' => 2)) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(1, $taskFinderModel->countByProjectId(1)); + } + + public function testWithRecurrenceLastColumn() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array( + 'title' => 'test', + 'project_id' => 1, + 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING, + 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_LAST_COLUMN, + ))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->withValues(array('dst_column_id' => 4)) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(2, $taskFinderModel->countByProjectId(1)); + } + + public function testWithRecurrenceLastColumnWithWrongColumn() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array( + 'title' => 'test', + 'project_id' => 1, + 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING, + 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_LAST_COLUMN, + ))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->withValues(array('dst_column_id' => 2)) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(1, $taskFinderModel->countByProjectId(1)); + } + + public function testWithRecurrenceOnClose() + { + $projectModel = new ProjectModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $taskFinderModel = new TaskFinderModel($this->container); + $subscriber = new RecurringTaskSubscriber($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array( + 'title' => 'test', + 'project_id' => 1, + 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING, + 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_CLOSE, + ))); + + $event = TaskEventBuilder::getInstance($this->container) + ->withTaskId(1) + ->withChanges(array('is_active' => 0)) + ->buildEvent(); + + $subscriber->onMove($event); + $subscriber->onClose($event); + + $this->assertEquals(2, $taskFinderModel->countByProjectId(1)); + } +} -- cgit v1.2.3 From 2a7ca0405cdafe26578326c12cdd6b072e8d90ae Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 21:14:33 -0400 Subject: Create new class SubtaskPositionModel --- app/Controller/SubtaskController.php | 2 +- app/Core/Base.php | 1 + app/Model/SubtaskModel.php | 33 ----------- app/Model/SubtaskPositionModel.php | 47 ++++++++++++++++ app/ServiceProvider/ClassProvider.php | 1 + tests/units/Model/SubtaskModelTest.php | 65 ---------------------- tests/units/Model/SubtaskPositionModelTest.php | 77 ++++++++++++++++++++++++++ 7 files changed, 127 insertions(+), 99 deletions(-) create mode 100644 app/Model/SubtaskPositionModel.php create mode 100644 tests/units/Model/SubtaskPositionModelTest.php diff --git a/app/Controller/SubtaskController.php b/app/Controller/SubtaskController.php index 93dab5cd..7502d84f 100644 --- a/app/Controller/SubtaskController.php +++ b/app/Controller/SubtaskController.php @@ -168,7 +168,7 @@ class SubtaskController extends BaseController $values = $this->request->getJson(); if (! empty($values) && $this->helper->user->hasProjectAccess('SubtaskController', 'movePosition', $project_id)) { - $result = $this->subtaskModel->changePosition($task_id, $values['subtask_id'], $values['position']); + $result = $this->subtaskPositionModel->changePosition($task_id, $values['subtask_id'], $values['position']); $this->response->json(array('result' => $result)); } else { throw new AccessForbiddenException(); diff --git a/app/Core/Base.php b/app/Core/Base.php index 41f5d2e0..0230b671 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -90,6 +90,7 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel * @property \Kanboard\Model\SubtaskModel $subtaskModel + * @property \Kanboard\Model\SubtaskPositionModel $subtaskPositionModel * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel * @property \Kanboard\Model\SwimlaneModel $swimlaneModel * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index f3fc72ba..5a4e87a2 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -272,39 +272,6 @@ class SubtaskModel extends Base return $this->db->table(self::TABLE)->eq('task_id', $task_id)->update(array('status' => self::STATUS_DONE)); } - /** - * Save subtask position - * - * @access public - * @param integer $task_id - * @param integer $subtask_id - * @param integer $position - * @return boolean - */ - public function changePosition($task_id, $subtask_id, $position) - { - if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) { - return false; - } - - $subtask_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id'); - $offset = 1; - $results = array(); - - foreach ($subtask_ids as $current_subtask_id) { - if ($offset == $position) { - $offset++; - } - - $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); - $offset++; - } - - $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); - - return !in_array(false, $results, true); - } - /** * Change the status of subtask * diff --git a/app/Model/SubtaskPositionModel.php b/app/Model/SubtaskPositionModel.php new file mode 100644 index 00000000..3c26465d --- /dev/null +++ b/app/Model/SubtaskPositionModel.php @@ -0,0 +1,47 @@ + $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->count()) { + return false; + } + + $subtask_ids = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id'); + $offset = 1; + $results = array(); + + foreach ($subtask_ids as $current_subtask_id) { + if ($offset == $position) { + $offset++; + } + + $results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); + $offset++; + } + + $results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); + } +} diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index e32c0d43..d1415d8c 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -60,6 +60,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectUserRoleModel', 'RememberMeSessionModel', 'SubtaskModel', + 'SubtaskPositionModel', 'SubtaskTimeTrackingModel', 'SwimlaneModel', 'TagDuplicationModel', diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php index 7e438651..3b25bb3b 100644 --- a/tests/units/Model/SubtaskModelTest.php +++ b/tests/units/Model/SubtaskModelTest.php @@ -229,71 +229,6 @@ class SubtaskModelTest extends Base $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); diff --git a/tests/units/Model/SubtaskPositionModelTest.php b/tests/units/Model/SubtaskPositionModelTest.php new file mode 100644 index 00000000..92412392 --- /dev/null +++ b/tests/units/Model/SubtaskPositionModelTest.php @@ -0,0 +1,77 @@ +container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskPositionModel = new SubtaskPositionModel($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($subtaskPositionModel->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($subtaskPositionModel->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($subtaskPositionModel->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($subtaskPositionModel->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($subtaskPositionModel->changePosition(1, 2, 0)); + $this->assertFalse($subtaskPositionModel->changePosition(1, 2, 4)); + } +} -- cgit v1.2.3 From f216e345ba2ad7486037c393c0475a1371ca2b00 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 21:22:24 -0400 Subject: Create new class SubtaskTaskConversionModel --- app/Controller/SubtaskConverterController.php | 2 +- app/Core/Base.php | 1 + app/Model/SubtaskModel.php | 27 -------------- app/Model/SubtaskTaskConversionModel.php | 41 ++++++++++++++++++++++ app/ServiceProvider/ClassProvider.php | 1 + tests/units/Model/SubtaskModelTest.php | 24 ------------- .../units/Model/SubtaskTaskConversionModelTest.php | 37 +++++++++++++++++++ 7 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 app/Model/SubtaskTaskConversionModel.php create mode 100644 tests/units/Model/SubtaskTaskConversionModelTest.php diff --git a/app/Controller/SubtaskConverterController.php b/app/Controller/SubtaskConverterController.php index 65bcd2da..404c50d0 100644 --- a/app/Controller/SubtaskConverterController.php +++ b/app/Controller/SubtaskConverterController.php @@ -26,7 +26,7 @@ class SubtaskConverterController extends BaseController $project = $this->getProject(); $subtask = $this->getSubtask(); - $task_id = $this->subtaskModel->convertToTask($project['id'], $subtask['id']); + $task_id = $this->subtaskTaskConversionModel->convertToTask($project['id'], $subtask['id']); if ($task_id !== false) { $this->flash->success(t('Subtask converted to task successfully.')); diff --git a/app/Core/Base.php b/app/Core/Base.php index 0230b671..8b9bf085 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -91,6 +91,7 @@ use Pimple\Container; * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel * @property \Kanboard\Model\SubtaskModel $subtaskModel * @property \Kanboard\Model\SubtaskPositionModel $subtaskPositionModel + * @property \Kanboard\Model\SubtaskTaskConversionModel $subtaskTaskConversionModel * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel * @property \Kanboard\Model\SwimlaneModel $swimlaneModel * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index 5a4e87a2..2ac6095c 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -368,31 +368,4 @@ class SubtaskModel extends Base } }); } - - /** - * Convert a subtask to a task - * - * @access public - * @param integer $project_id - * @param integer $subtask_id - * @return integer - */ - public function convertToTask($project_id, $subtask_id) - { - $subtask = $this->getById($subtask_id); - - $task_id = $this->taskCreationModel->create(array( - 'project_id' => $project_id, - 'title' => $subtask['title'], - 'time_estimated' => $subtask['time_estimated'], - 'time_spent' => $subtask['time_spent'], - 'owner_id' => $subtask['user_id'], - )); - - if ($task_id !== false) { - $this->remove($subtask_id); - } - - return $task_id; - } } diff --git a/app/Model/SubtaskTaskConversionModel.php b/app/Model/SubtaskTaskConversionModel.php new file mode 100644 index 00000000..8bf83d76 --- /dev/null +++ b/app/Model/SubtaskTaskConversionModel.php @@ -0,0 +1,41 @@ +subtaskModel->getById($subtask_id); + + $task_id = $this->taskCreationModel->create(array( + 'project_id' => $project_id, + 'title' => $subtask['title'], + 'time_estimated' => $subtask['time_estimated'], + 'time_spent' => $subtask['time_spent'], + 'owner_id' => $subtask['user_id'], + )); + + if ($task_id !== false) { + $this->subtaskModel->remove($subtask_id); + } + + return $task_id; + } +} diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index d1415d8c..ad69d5fb 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -61,6 +61,7 @@ class ClassProvider implements ServiceProviderInterface 'RememberMeSessionModel', 'SubtaskModel', 'SubtaskPositionModel', + 'SubtaskTaskConversionModel', 'SubtaskTimeTrackingModel', 'SwimlaneModel', 'TagDuplicationModel', diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php index 3b25bb3b..d270e177 100644 --- a/tests/units/Model/SubtaskModelTest.php +++ b/tests/units/Model/SubtaskModelTest.php @@ -229,30 +229,6 @@ class SubtaskModelTest extends Base $this->assertEquals(2, $subtasks[1]['position']); } - 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); diff --git a/tests/units/Model/SubtaskTaskConversionModelTest.php b/tests/units/Model/SubtaskTaskConversionModelTest.php new file mode 100644 index 00000000..51a623b2 --- /dev/null +++ b/tests/units/Model/SubtaskTaskConversionModelTest.php @@ -0,0 +1,37 @@ +container); + $taskFinderModel = new TaskFinderModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + $subtaskConversion = new SubtaskTaskConversionModel($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 = $subtaskConversion->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']); + } +} -- cgit v1.2.3 From 24555080fd3ca8607f0a798b5a0e4be98ff131f8 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 21:48:59 -0400 Subject: Create new class SubtaskStatusModel --- app/Controller/SubtaskRestrictionController.php | 2 +- app/Controller/SubtaskStatusController.php | 2 +- app/Core/Base.php | 1 + app/Model/SubtaskModel.php | 184 +++++----------- app/Model/SubtaskStatusModel.php | 85 ++++++++ app/Model/TaskStatusModel.php | 2 +- app/ServiceProvider/ClassProvider.php | 1 + app/Subscriber/BootstrapSubscriber.php | 2 +- tests/units/Model/SubtaskModelTest.php | 110 ---------- tests/units/Model/SubtaskStatusModelTest.php | 123 +++++++++++ tests/units/Model/SubtaskTimeTrackingModelTest.php | 240 +++++++++++++++++++++ tests/units/Model/SubtaskTimeTrackingTest.php | 240 --------------------- 12 files changed, 506 insertions(+), 486 deletions(-) create mode 100644 app/Model/SubtaskStatusModel.php create mode 100644 tests/units/Model/SubtaskStatusModelTest.php create mode 100644 tests/units/Model/SubtaskTimeTrackingModelTest.php delete mode 100644 tests/units/Model/SubtaskTimeTrackingTest.php diff --git a/app/Controller/SubtaskRestrictionController.php b/app/Controller/SubtaskRestrictionController.php index 084fc0d9..cb642e1c 100644 --- a/app/Controller/SubtaskRestrictionController.php +++ b/app/Controller/SubtaskRestrictionController.php @@ -27,7 +27,7 @@ class SubtaskRestrictionController extends BaseController SubtaskModel::STATUS_TODO => t('Todo'), SubtaskModel::STATUS_DONE => t('Done'), ), - 'subtask_inprogress' => $this->subtaskModel->getSubtaskInProgress($this->userSession->getId()), + 'subtask_inprogress' => $this->subtaskStatusModel->getSubtaskInProgress($this->userSession->getId()), 'subtask' => $subtask, 'task' => $task, ))); diff --git a/app/Controller/SubtaskStatusController.php b/app/Controller/SubtaskStatusController.php index 699951fe..d4d356c3 100644 --- a/app/Controller/SubtaskStatusController.php +++ b/app/Controller/SubtaskStatusController.php @@ -20,7 +20,7 @@ class SubtaskStatusController extends BaseController $task = $this->getTask(); $subtask = $this->getSubtask(); - $status = $this->subtaskModel->toggleStatus($subtask['id']); + $status = $this->subtaskStatusModel->toggleStatus($subtask['id']); if ($this->request->getIntegerParam('refresh-table') === 0) { $subtask['status'] = $status; diff --git a/app/Core/Base.php b/app/Core/Base.php index 8b9bf085..563013bd 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -91,6 +91,7 @@ use Pimple\Container; * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel * @property \Kanboard\Model\SubtaskModel $subtaskModel * @property \Kanboard\Model\SubtaskPositionModel $subtaskPositionModel + * @property \Kanboard\Model\SubtaskStatusModel $subtaskStatusModel * @property \Kanboard\Model\SubtaskTaskConversionModel $subtaskTaskConversionModel * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel * @property \Kanboard\Model\SwimlaneModel $swimlaneModel diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index 2ac6095c..568e27a4 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -21,25 +21,13 @@ class SubtaskModel extends Base const TABLE = 'subtasks'; /** - * Task "done" status - * - * @var integer - */ - const STATUS_DONE = 2; - - /** - * Task "in progress" status - * - * @var integer - */ - const STATUS_INPROGRESS = 1; - - /** - * Task "todo" status + * Subtask status * * @var integer */ const STATUS_TODO = 0; + const STATUS_INPROGRESS = 1; + const STATUS_DONE = 2; /** * Events @@ -81,26 +69,6 @@ class SubtaskModel extends Base ); } - /** - * Add subtask status status to the resultset - * - * @access public - * @param array $subtasks Subtasks - * @return array - */ - public function addStatusName(array $subtasks) - { - $status = $this->getStatusList(); - - foreach ($subtasks as &$subtask) { - $subtask['status_name'] = $status[$subtask['status']]; - $subtask['timer_start_date'] = isset($subtask['timer_start_date']) ? $subtask['timer_start_date'] : 0; - $subtask['is_timer_started'] = ! empty($subtask['timer_start_date']); - } - - return $subtasks; - } - /** * Get the query to fetch subtasks assigned to a user * @@ -176,35 +144,6 @@ class SubtaskModel extends Base return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); } - /** - * Prepare data before insert/update - * - * @access public - * @param array $values Form values - */ - public function prepare(array &$values) - { - $this->helper->model->removeFields($values, array('another_subtask')); - $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); - } - - /** - * Prepare data before insert - * - * @access public - * @param array $values Form values - */ - public function prepareCreation(array &$values) - { - $this->prepare($values); - - $values['position'] = $this->getLastPosition($values['task_id']) + 1; - $values['status'] = isset($values['status']) ? $values['status'] : self::STATUS_TODO; - $values['time_estimated'] = isset($values['time_estimated']) ? $values['time_estimated'] : 0; - $values['time_spent'] = isset($values['time_spent']) ? $values['time_spent'] : 0; - $values['user_id'] = isset($values['user_id']) ? $values['user_id'] : 0; - } - /** * Get the position of the last column for a given project * @@ -260,74 +199,6 @@ class SubtaskModel extends Base return $result; } - /** - * Close all subtasks of a task - * - * @access public - * @param integer $task_id - * @return boolean - */ - public function closeAll($task_id) - { - return $this->db->table(self::TABLE)->eq('task_id', $task_id)->update(array('status' => self::STATUS_DONE)); - } - - /** - * Change the status of subtask - * - * @access public - * @param integer $subtask_id - * @return boolean|integer - */ - public function toggleStatus($subtask_id) - { - $subtask = $this->getById($subtask_id); - $status = ($subtask['status'] + 1) % 3; - - $values = array( - 'id' => $subtask['id'], - 'status' => $status, - 'task_id' => $subtask['task_id'], - ); - - if (empty($subtask['user_id']) && $this->userSession->isLogged()) { - $values['user_id'] = $this->userSession->getId(); - } - - return $this->update($values) ? $status : false; - } - - /** - * Get the subtask in progress for this user - * - * @access public - * @param integer $user_id - * @return array - */ - public function getSubtaskInProgress($user_id) - { - return $this->db->table(self::TABLE) - ->eq('status', self::STATUS_INPROGRESS) - ->eq('user_id', $user_id) - ->findOne(); - } - - /** - * Return true if the user have a subtask in progress - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function hasSubtaskInProgress($user_id) - { - return $this->configModel->get('subtask_restriction') == 1 && - $this->db->table(self::TABLE) - ->eq('status', self::STATUS_INPROGRESS) - ->eq('user_id', $user_id) - ->exists(); - } - /** * Remove * @@ -368,4 +239,53 @@ class SubtaskModel extends Base } }); } + + /** + * Prepare data before insert/update + * + * @access protected + * @param array $values Form values + */ + protected function prepare(array &$values) + { + $this->helper->model->removeFields($values, array('another_subtask')); + $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); + } + + /** + * Prepare data before insert + * + * @access protected + * @param array $values Form values + */ + protected function prepareCreation(array &$values) + { + $this->prepare($values); + + $values['position'] = $this->getLastPosition($values['task_id']) + 1; + $values['status'] = isset($values['status']) ? $values['status'] : self::STATUS_TODO; + $values['time_estimated'] = isset($values['time_estimated']) ? $values['time_estimated'] : 0; + $values['time_spent'] = isset($values['time_spent']) ? $values['time_spent'] : 0; + $values['user_id'] = isset($values['user_id']) ? $values['user_id'] : 0; + } + + /** + * Add subtask status status to the resultset + * + * @access public + * @param array $subtasks Subtasks + * @return array + */ + public function addStatusName(array $subtasks) + { + $status = $this->getStatusList(); + + foreach ($subtasks as &$subtask) { + $subtask['status_name'] = $status[$subtask['status']]; + $subtask['timer_start_date'] = isset($subtask['timer_start_date']) ? $subtask['timer_start_date'] : 0; + $subtask['is_timer_started'] = ! empty($subtask['timer_start_date']); + } + + return $subtasks; + } } diff --git a/app/Model/SubtaskStatusModel.php b/app/Model/SubtaskStatusModel.php new file mode 100644 index 00000000..26cbb67d --- /dev/null +++ b/app/Model/SubtaskStatusModel.php @@ -0,0 +1,85 @@ +db->table(SubtaskModel::TABLE) + ->eq('status', SubtaskModel::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->findOne(); + } + + /** + * Return true if the user have a subtask in progress + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function hasSubtaskInProgress($user_id) + { + return $this->configModel->get('subtask_restriction') == 1 && + $this->db->table(SubtaskModel::TABLE) + ->eq('status', SubtaskModel::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->exists(); + } + + /** + * Change the status of subtask + * + * @access public + * @param integer $subtask_id + * @return boolean|integer + */ + public function toggleStatus($subtask_id) + { + $subtask = $this->subtaskModel->getById($subtask_id); + $status = ($subtask['status'] + 1) % 3; + + $values = array( + 'id' => $subtask['id'], + 'status' => $status, + 'task_id' => $subtask['task_id'], + ); + + if (empty($subtask['user_id']) && $this->userSession->isLogged()) { + $values['user_id'] = $this->userSession->getId(); + } + + return $this->subtaskModel->update($values) ? $status : false; + } + + /** + * Close all subtasks of a task + * + * @access public + * @param integer $task_id + * @return boolean + */ + public function closeAll($task_id) + { + return $this->db + ->table(SubtaskModel::TABLE) + ->eq('task_id', $task_id) + ->update(array('status' => SubtaskModel::STATUS_DONE)); + } +} diff --git a/app/Model/TaskStatusModel.php b/app/Model/TaskStatusModel.php index ea304beb..dc114698 100644 --- a/app/Model/TaskStatusModel.php +++ b/app/Model/TaskStatusModel.php @@ -45,7 +45,7 @@ class TaskStatusModel extends Base */ public function close($task_id) { - $this->subtaskModel->closeAll($task_id); + $this->subtaskStatusModel->closeAll($task_id); return $this->changeStatus($task_id, TaskModel::STATUS_CLOSED, time(), TaskModel::EVENT_CLOSE); } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index ad69d5fb..9a71148b 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -61,6 +61,7 @@ class ClassProvider implements ServiceProviderInterface 'RememberMeSessionModel', 'SubtaskModel', 'SubtaskPositionModel', + 'SubtaskStatusModel', 'SubtaskTaskConversionModel', 'SubtaskTimeTrackingModel', 'SwimlaneModel', diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php index 7d12e9ae..3618f30f 100644 --- a/app/Subscriber/BootstrapSubscriber.php +++ b/app/Subscriber/BootstrapSubscriber.php @@ -21,7 +21,7 @@ class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInter $this->actionManager->attachEvents(); if ($this->userSession->isLogged()) { - $this->sessionStorage->hasSubtaskInProgress = $this->subtaskModel->hasSubtaskInProgress($this->userSession->getId()); + $this->sessionStorage->hasSubtaskInProgress = $this->subtaskStatusModel->hasSubtaskInProgress($this->userSession->getId()); } } diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php index d270e177..23183d22 100644 --- a/tests/units/Model/SubtaskModelTest.php +++ b/tests/units/Model/SubtaskModelTest.php @@ -5,7 +5,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\SubtaskModel; use Kanboard\Model\ProjectModel; -use Kanboard\Model\TaskFinderModel; class SubtaskModelTest extends Base { @@ -74,115 +73,6 @@ class SubtaskModelTest extends Base $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); diff --git a/tests/units/Model/SubtaskStatusModelTest.php b/tests/units/Model/SubtaskStatusModelTest.php new file mode 100644 index 00000000..af4c3955 --- /dev/null +++ b/tests/units/Model/SubtaskStatusModelTest.php @@ -0,0 +1,123 @@ +container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskStatusModel = new SubtaskStatusModel($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, $subtaskStatusModel->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, $subtaskStatusModel->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, $subtaskStatusModel->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); + $subtaskStatusModel = new SubtaskStatusModel($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, $subtaskStatusModel->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, $subtaskStatusModel->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, $subtaskStatusModel->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); + $subtaskStatusModel = new SubtaskStatusModel($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($subtaskStatusModel->closeAll(1)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertNotEmpty($subtasks); + + foreach ($subtasks as $subtask) { + $this->assertEquals(SubtaskModel::STATUS_DONE, $subtask['status']); + } + } +} diff --git a/tests/units/Model/SubtaskTimeTrackingModelTest.php b/tests/units/Model/SubtaskTimeTrackingModelTest.php new file mode 100644 index 00000000..cfee5b14 --- /dev/null +++ b/tests/units/Model/SubtaskTimeTrackingModelTest.php @@ -0,0 +1,240 @@ +container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); + + $this->assertFalse($subtaskTimeTrackingModel->hasTimer(1, 1)); + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(1, 1)); + $this->assertTrue($subtaskTimeTrackingModel->hasTimer(1, 1)); + $this->assertFalse($subtaskTimeTrackingModel->logStartTime(1, 1)); + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(1, 1)); + $this->assertFalse($subtaskTimeTrackingModel->hasTimer(1, 1)); + } + + public function testGetTimerStatus() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($this->container); + $projectModel = new ProjectModel($this->container); + + $this->container['sessionStorage']->user = array('id' => 1); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test 1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1, 'user_id' => 1))); + + // Nothing started + $subtasks = $subtaskModel->getAll(1); + $this->assertNotEmpty($subtasks); + $this->assertEquals(0, $subtasks[0]['timer_start_date']); + $this->assertFalse($subtasks[0]['is_timer_started']); + + $subtask = $subtaskModel->getById(1, true); + $this->assertNotEmpty($subtask); + $this->assertEquals(0, $subtask['timer_start_date']); + $this->assertFalse($subtask['is_timer_started']); + + // Start the clock + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(1, 1)); + + $subtasks = $subtaskModel->getAll(1); + $this->assertNotEmpty($subtasks); + $this->assertEquals(time(), $subtasks[0]['timer_start_date'], '', 3); + $this->assertTrue($subtasks[0]['is_timer_started']); + + $subtask = $subtaskModel->getById(1, true); + $this->assertNotEmpty($subtask); + $this->assertEquals(time(), $subtask['timer_start_date'], '', 3); + $this->assertTrue($subtask['is_timer_started']); + + // Stop the clock + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(1, 1)); + $subtasks = $subtaskModel->getAll(1); + $this->assertNotEmpty($subtasks); + $this->assertEquals(0, $subtasks[0]['timer_start_date']); + $this->assertFalse($subtasks[0]['is_timer_started']); + + $subtask = $subtaskModel->getById(1, true); + $this->assertNotEmpty($subtask); + $this->assertEquals(0, $subtask['timer_start_date']); + $this->assertFalse($subtask['is_timer_started']); + } + + public function testLogStartTime() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); + + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(1, 1)); + + $timesheet = $subtaskTimeTrackingModel->getUserTimesheet(1); + $this->assertNotEmpty($timesheet); + $this->assertCount(1, $timesheet); + $this->assertNotEmpty($timesheet[0]['start']); + $this->assertEmpty($timesheet[0]['end']); + $this->assertEquals(1, $timesheet[0]['user_id']); + $this->assertEquals(1, $timesheet[0]['subtask_id']); + } + + public function testLogStartEnd() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); + + // No start time + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(1, 1)); + $timesheet = $subtaskTimeTrackingModel->getUserTimesheet(1); + $this->assertEmpty($timesheet); + + // Log start and end time + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(1, 1)); + sleep(1); + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(1, 1)); + + $timesheet = $subtaskTimeTrackingModel->getUserTimesheet(1); + $this->assertNotEmpty($timesheet); + $this->assertCount(1, $timesheet); + $this->assertNotEmpty($timesheet[0]['start']); + $this->assertNotEmpty($timesheet[0]['end']); + $this->assertEquals(1, $timesheet[0]['user_id']); + $this->assertEquals(1, $timesheet[0]['subtask_id']); + $this->assertNotEquals($timesheet[0]['start'], $timesheet[0]['end']); + } + + public function testCalculateSubtaskTime() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 2.2, 'time_estimated' => 3.3))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 1.1, 'time_estimated' => 4.4))); + + $time = $subtaskTimeTrackingModel->calculateSubtaskTime(1); + $this->assertCount(2, $time); + $this->assertEquals(3.3, $time['time_spent'], 'Total spent', 0.01); + $this->assertEquals(7.7, $time['time_estimated'], 'Total estimated', 0.01); + } + + public function testUpdateSubtaskTimeSpent() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 2.2))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1))); + + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(1, 1)); + $this->assertTrue($subtaskTimeTrackingModel->logStartTime(2, 1)); + + // Fake start time + $this->container['db']->table(SubtaskTimeTrackingModel::TABLE)->update(array('start' => time() - 3600)); + + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(1, 1)); + $this->assertTrue($subtaskTimeTrackingModel->logEndTime(2, 1)); + + $timesheet = $subtaskTimeTrackingModel->getUserTimesheet(1); + $this->assertNotEmpty($timesheet); + $this->assertCount(2, $timesheet); + $this->assertEquals(3600, $timesheet[0]['end'] - $timesheet[0]['start'], 'Wrong timestamps', 1); + $this->assertEquals(3600, $timesheet[1]['end'] - $timesheet[1]['start'], 'Wrong timestamps', 1); + + $time = $subtaskTimeTrackingModel->calculateSubtaskTime(1); + $this->assertEquals(4.2, $time['time_spent'], 'Total spent', 0.01); + $this->assertEquals(0, $time['time_estimated'], 'Total estimated', 0.01); + + $time = $subtaskTimeTrackingModel->calculateSubtaskTime(2); + $this->assertEquals(0, $time['time_spent'], 'Total spent', 0.01); + $this->assertEquals(0, $time['time_estimated'], 'Total estimated', 0.01); + } + + public function testUpdateTaskTimeTracking() + { + $taskFinderModel = new TaskFinderModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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(2, $taskCreationModel->create(array('title' => 'test 2', 'project_id' => 1, 'time_estimated' => 1.5, 'time_spent' => 0.5))); + $this->assertEquals(3, $taskCreationModel->create(array('title' => 'test 3', 'project_id' => 1, 'time_estimated' => 4, 'time_spent' => 2))); + + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_spent' => 2.2))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => 1))); + + $this->assertEquals(3, $subtaskModel->create(array('title' => 'subtask #3', 'task_id' => 2, 'time_spent' => 3.4))); + $this->assertEquals(4, $subtaskModel->create(array('title' => 'subtask #4', 'task_id' => 2, 'time_estimated' => 1.25))); + + $this->assertEquals(5, $subtaskModel->create(array('title' => 'subtask #5', 'task_id' => 3, 'time_spent' => 8))); + + $subtaskTimeTrackingModel->updateTaskTimeTracking(1); + $subtaskTimeTrackingModel->updateTaskTimeTracking(2); + $subtaskTimeTrackingModel->updateTaskTimeTracking(3); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(2.2, $task['time_spent'], 'Total spent', 0.01); + $this->assertEquals(1, $task['time_estimated'], 'Total estimated', 0.01); + + $task = $taskFinderModel->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(3.4, $task['time_spent'], 'Total spent', 0.01); + $this->assertEquals(1.25, $task['time_estimated'], 'Total estimated', 0.01); + + $task = $taskFinderModel->getById(3); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['time_estimated']); + $this->assertEquals(8, $task['time_spent']); + + $this->assertTrue($subtaskModel->remove(3)); + $this->assertTrue($subtaskModel->remove(4)); + + $subtaskTimeTrackingModel->updateTaskTimeTracking(2); + + $task = $taskFinderModel->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['time_estimated']); + $this->assertEquals(0, $task['time_spent']); + } +} diff --git a/tests/units/Model/SubtaskTimeTrackingTest.php b/tests/units/Model/SubtaskTimeTrackingTest.php deleted file mode 100644 index d5ae62ae..00000000 --- a/tests/units/Model/SubtaskTimeTrackingTest.php +++ /dev/null @@ -1,240 +0,0 @@ -container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); - - $this->assertFalse($st->hasTimer(1, 1)); - $this->assertTrue($st->logStartTime(1, 1)); - $this->assertTrue($st->hasTimer(1, 1)); - $this->assertFalse($st->logStartTime(1, 1)); - $this->assertTrue($st->logEndTime(1, 1)); - $this->assertFalse($st->hasTimer(1, 1)); - } - - public function testGetTimerStatus() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($this->container); - $p = new ProjectModel($this->container); - - $this->container['sessionStorage']->user = array('id' => 1); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'user_id' => 1))); - - // Nothing started - $subtasks = $s->getAll(1); - $this->assertNotEmpty($subtasks); - $this->assertEquals(0, $subtasks[0]['timer_start_date']); - $this->assertFalse($subtasks[0]['is_timer_started']); - - $subtask = $s->getById(1, true); - $this->assertNotEmpty($subtask); - $this->assertEquals(0, $subtask['timer_start_date']); - $this->assertFalse($subtask['is_timer_started']); - - // Start the clock - $this->assertTrue($st->logStartTime(1, 1)); - - $subtasks = $s->getAll(1); - $this->assertNotEmpty($subtasks); - $this->assertEquals(time(), $subtasks[0]['timer_start_date'], '', 3); - $this->assertTrue($subtasks[0]['is_timer_started']); - - $subtask = $s->getById(1, true); - $this->assertNotEmpty($subtask); - $this->assertEquals(time(), $subtask['timer_start_date'], '', 3); - $this->assertTrue($subtask['is_timer_started']); - - // Stop the clock - $this->assertTrue($st->logEndTime(1, 1)); - $subtasks = $s->getAll(1); - $this->assertNotEmpty($subtasks); - $this->assertEquals(0, $subtasks[0]['timer_start_date']); - $this->assertFalse($subtasks[0]['is_timer_started']); - - $subtask = $s->getById(1, true); - $this->assertNotEmpty($subtask); - $this->assertEquals(0, $subtask['timer_start_date']); - $this->assertFalse($subtask['is_timer_started']); - } - - public function testLogStartTime() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); - - $this->assertTrue($st->logStartTime(1, 1)); - - $timesheet = $st->getUserTimesheet(1); - $this->assertNotEmpty($timesheet); - $this->assertCount(1, $timesheet); - $this->assertNotEmpty($timesheet[0]['start']); - $this->assertEmpty($timesheet[0]['end']); - $this->assertEquals(1, $timesheet[0]['user_id']); - $this->assertEquals(1, $timesheet[0]['subtask_id']); - } - - public function testLogStartEnd() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); - - // No start time - $this->assertTrue($st->logEndTime(1, 1)); - $timesheet = $st->getUserTimesheet(1); - $this->assertEmpty($timesheet); - - // Log start and end time - $this->assertTrue($st->logStartTime(1, 1)); - sleep(1); - $this->assertTrue($st->logEndTime(1, 1)); - - $timesheet = $st->getUserTimesheet(1); - $this->assertNotEmpty($timesheet); - $this->assertCount(1, $timesheet); - $this->assertNotEmpty($timesheet[0]['start']); - $this->assertNotEmpty($timesheet[0]['end']); - $this->assertEquals(1, $timesheet[0]['user_id']); - $this->assertEquals(1, $timesheet[0]['subtask_id']); - $this->assertNotEquals($timesheet[0]['start'], $timesheet[0]['end']); - } - - public function testCalculateSubtaskTime() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 2.2, 'time_estimated' => 3.3))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 1.1, 'time_estimated' => 4.4))); - - $time = $st->calculateSubtaskTime(1); - $this->assertCount(2, $time); - $this->assertEquals(3.3, $time['time_spent'], 'Total spent', 0.01); - $this->assertEquals(7.7, $time['time_estimated'], 'Total estimated', 0.01); - } - - public function testUpdateSubtaskTimeSpent() - { - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_spent' => 2.2))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1))); - - $this->assertTrue($st->logStartTime(1, 1)); - $this->assertTrue($st->logStartTime(2, 1)); - - // Fake start time - $this->container['db']->table(SubtaskTimeTrackingModel::TABLE)->update(array('start' => time() - 3600)); - - $this->assertTrue($st->logEndTime(1, 1)); - $this->assertTrue($st->logEndTime(2, 1)); - - $timesheet = $st->getUserTimesheet(1); - $this->assertNotEmpty($timesheet); - $this->assertCount(2, $timesheet); - $this->assertEquals(3600, $timesheet[0]['end'] - $timesheet[0]['start'], 'Wrong timestamps', 1); - $this->assertEquals(3600, $timesheet[1]['end'] - $timesheet[1]['start'], 'Wrong timestamps', 1); - - $time = $st->calculateSubtaskTime(1); - $this->assertEquals(4.2, $time['time_spent'], 'Total spent', 0.01); - $this->assertEquals(0, $time['time_estimated'], 'Total estimated', 0.01); - - $time = $st->calculateSubtaskTime(2); - $this->assertEquals(0, $time['time_spent'], 'Total spent', 0.01); - $this->assertEquals(0, $time['time_estimated'], 'Total estimated', 0.01); - } - - public function testUpdateTaskTimeTracking() - { - $tf = new TaskFinderModel($this->container); - $tc = new TaskCreationModel($this->container); - $s = new SubtaskModel($this->container); - $st = new SubtaskTimeTrackingModel($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(2, $tc->create(array('title' => 'test 2', 'project_id' => 1, 'time_estimated' => 1.5, 'time_spent' => 0.5))); - $this->assertEquals(3, $tc->create(array('title' => 'test 3', 'project_id' => 1, 'time_estimated' => 4, 'time_spent' => 2))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_spent' => 2.2))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => 1))); - - $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 2, 'time_spent' => 3.4))); - $this->assertEquals(4, $s->create(array('title' => 'subtask #4', 'task_id' => 2, 'time_estimated' => 1.25))); - - $this->assertEquals(5, $s->create(array('title' => 'subtask #5', 'task_id' => 3, 'time_spent' => 8))); - - $st->updateTaskTimeTracking(1); - $st->updateTaskTimeTracking(2); - $st->updateTaskTimeTracking(3); - - $task = $tf->getById(1); - $this->assertNotEmpty($task); - $this->assertEquals(2.2, $task['time_spent'], 'Total spent', 0.01); - $this->assertEquals(1, $task['time_estimated'], 'Total estimated', 0.01); - - $task = $tf->getById(2); - $this->assertNotEmpty($task); - $this->assertEquals(3.4, $task['time_spent'], 'Total spent', 0.01); - $this->assertEquals(1.25, $task['time_estimated'], 'Total estimated', 0.01); - - $task = $tf->getById(3); - $this->assertNotEmpty($task); - $this->assertEquals(0, $task['time_estimated']); - $this->assertEquals(8, $task['time_spent']); - - $this->assertTrue($s->remove(3)); - $this->assertTrue($s->remove(4)); - - $st->updateTaskTimeTracking(2); - - $task = $tf->getById(2); - $this->assertNotEmpty($task); - $this->assertEquals(0, $task['time_estimated']); - $this->assertEquals(0, $task['time_spent']); - } -} -- cgit v1.2.3 From 5884c65a02a13dd396525d0b8d1720d1c062a96e Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 22:50:20 -0400 Subject: Remove SubtaskTimeTrackingSubscriber --- app/Model/SubtaskModel.php | 15 ++++--- app/Model/SubtaskStatusModel.php | 3 ++ app/Model/SubtaskTimeTrackingModel.php | 23 ++++++++++- app/ServiceProvider/EventDispatcherProvider.php | 2 - app/Subscriber/SubtaskTimeTrackingSubscriber.php | 48 ---------------------- tests/units/Job/SubtaskEventJobTest.php | 2 +- tests/units/Model/SubtaskModelTest.php | 42 ++++++++++++++++++- tests/units/Model/SubtaskTimeTrackingModelTest.php | 37 +++++++++++++++++ 8 files changed, 114 insertions(+), 58 deletions(-) delete mode 100644 app/Subscriber/SubtaskTimeTrackingSubscriber.php diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index 568e27a4..608ffce7 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -173,6 +173,7 @@ class SubtaskModel extends Base $subtask_id = $this->db->table(self::TABLE)->persist($values); if ($subtask_id !== false) { + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($values['task_id']); $this->queueManager->push($this->subtaskEventJob->withParams($subtask_id, self::EVENT_CREATE)); } @@ -183,17 +184,21 @@ class SubtaskModel extends Base * Update * * @access public - * @param array $values Form values - * @param bool $fire_events If true, will be called an event + * @param array $values + * @param bool $fire_event * @return bool */ - public function update(array $values, $fire_events = true) + public function update(array $values, $fire_event = true) { $this->prepare($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); - if ($result && $fire_events) { - $this->queueManager->push($this->subtaskEventJob->withParams($values['id'], self::EVENT_UPDATE, $values)); + if ($result) { + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($values['task_id']); + + if ($fire_event) { + $this->queueManager->push($this->subtaskEventJob->withParams($values['id'], self::EVENT_UPDATE, $values)); + } } return $result; diff --git a/app/Model/SubtaskStatusModel.php b/app/Model/SubtaskStatusModel.php index 26cbb67d..c99d6055 100644 --- a/app/Model/SubtaskStatusModel.php +++ b/app/Model/SubtaskStatusModel.php @@ -63,8 +63,11 @@ class SubtaskStatusModel extends Base if (empty($subtask['user_id']) && $this->userSession->isLogged()) { $values['user_id'] = $this->userSession->getId(); + $subtask['user_id'] = $values['user_id']; } + $this->subtaskTimeTrackingModel->toggleTimer($subtask_id, $subtask['user_id'], $status); + return $this->subtaskModel->update($values) ? $status : false; } diff --git a/app/Model/SubtaskTimeTrackingModel.php b/app/Model/SubtaskTimeTrackingModel.php index 062e594a..3b1b97e4 100644 --- a/app/Model/SubtaskTimeTrackingModel.php +++ b/app/Model/SubtaskTimeTrackingModel.php @@ -159,6 +159,28 @@ class SubtaskTimeTrackingModel extends Base return $this->db->table(self::TABLE)->eq('subtask_id', $subtask_id)->eq('user_id', $user_id)->eq('end', 0)->exists(); } + /** + * Start or stop timer according to subtask status + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @param integer $status + * @return boolean + */ + public function toggleTimer($subtask_id, $user_id, $status) + { + if ($this->configModel->get('subtask_time_tracking') == 1) { + if ($status == SubtaskModel::STATUS_INPROGRESS) { + return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); + } elseif ($status == SubtaskModel::STATUS_DONE) { + return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id); + } + } + + return false; + } + /** * Log start time * @@ -252,7 +274,6 @@ class SubtaskTimeTrackingModel extends Base { $subtask = $this->subtaskModel->getById($subtask_id); - // Fire the event subtask.update return $this->subtaskModel->update(array( 'id' => $subtask['id'], 'time_spent' => $subtask['time_spent'] + $time_spent, diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php index 57543fe4..ebf42cbf 100644 --- a/app/ServiceProvider/EventDispatcherProvider.php +++ b/app/ServiceProvider/EventDispatcherProvider.php @@ -11,7 +11,6 @@ use Kanboard\Subscriber\BootstrapSubscriber; use Kanboard\Subscriber\NotificationSubscriber; use Kanboard\Subscriber\ProjectDailySummarySubscriber; use Kanboard\Subscriber\ProjectModificationDateSubscriber; -use Kanboard\Subscriber\SubtaskTimeTrackingSubscriber; use Kanboard\Subscriber\TransitionSubscriber; use Kanboard\Subscriber\RecurringTaskSubscriber; @@ -31,7 +30,6 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new ProjectDailySummarySubscriber($container)); $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); - $container['dispatcher']->addSubscriber(new SubtaskTimeTrackingSubscriber($container)); $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); diff --git a/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/app/Subscriber/SubtaskTimeTrackingSubscriber.php deleted file mode 100644 index 7e39c126..00000000 --- a/app/Subscriber/SubtaskTimeTrackingSubscriber.php +++ /dev/null @@ -1,48 +0,0 @@ - 'updateTaskTime', - SubtaskModel::EVENT_DELETE => 'updateTaskTime', - SubtaskModel::EVENT_UPDATE => array( - array('logStartEnd', 10), - array('updateTaskTime', 0), - ) - ); - } - - public function updateTaskTime(SubtaskEvent $event) - { - if (isset($event['task_id'])) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->subtaskTimeTrackingModel->updateTaskTimeTracking($event['task_id']); - } - } - - public function logStartEnd(SubtaskEvent $event) - { - if (isset($event['status']) && $this->configModel->get('subtask_time_tracking') == 1) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $subtask = $this->subtaskModel->getById($event['id']); - - if (empty($subtask['user_id'])) { - return false; - } - - if ($subtask['status'] == SubtaskModel::STATUS_INPROGRESS) { - return $this->subtaskTimeTrackingModel->logStartTime($subtask['id'], $subtask['user_id']); - } else { - return $this->subtaskTimeTrackingModel->logEndTime($subtask['id'], $subtask['user_id']); - } - } - } -} diff --git a/tests/units/Job/SubtaskEventJobTest.php b/tests/units/Job/SubtaskEventJobTest.php index 66c3db05..bdc30b51 100644 --- a/tests/units/Job/SubtaskEventJobTest.php +++ b/tests/units/Job/SubtaskEventJobTest.php @@ -41,7 +41,7 @@ class SubtaskEventJobTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1))); $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'before'))); - $this->assertTrue($subtaskModel->update(array('id' => 1, 'title' => 'after'))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'task_id' => 1, 'title' => 'after'))); $this->assertTrue($subtaskModel->remove(1)); $called = $this->container['dispatcher']->getCalledListeners(); diff --git a/tests/units/Model/SubtaskModelTest.php b/tests/units/Model/SubtaskModelTest.php index 23183d22..eed37cf3 100644 --- a/tests/units/Model/SubtaskModelTest.php +++ b/tests/units/Model/SubtaskModelTest.php @@ -5,6 +5,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\SubtaskModel; use Kanboard\Model\ProjectModel; +use Kanboard\Model\TaskFinderModel; class SubtaskModelTest extends Base { @@ -30,6 +31,24 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $subtask['position']); } + public function testCreationUpdateTaskTimeTracking() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($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, 'time_estimated' => 2, 'time_spent' => 1))); + $this->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => 5, 'time_spent' => 5))); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(7, $task['time_estimated']); + $this->assertEquals(6, $task['time_spent']); + } + public function testModification() { $taskCreationModel = new TaskCreationModel($this->container); @@ -40,7 +59,7 @@ class SubtaskModelTest extends Base $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->assertTrue($subtaskModel->update(array('id' => 1, 'user_id' => 1, 'status' => SubtaskModel::STATUS_INPROGRESS))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'task_id' => 1, 'user_id' => 1, 'status' => SubtaskModel::STATUS_INPROGRESS))); $subtask = $subtaskModel->getById(1); $this->assertNotEmpty($subtask); @@ -54,6 +73,27 @@ class SubtaskModelTest extends Base $this->assertEquals(1, $subtask['position']); } + public function testModificationUpdateTaskTimeTracking() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $projectModel = new ProjectModel($this->container); + $taskFinderModel = new TaskFinderModel($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->assertEquals(2, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'task_id' => 1, 'time_estimated' => 2, 'time_spent' => 1))); + $this->assertTrue($subtaskModel->update(array('id' => 2, 'task_id' => 1, 'time_estimated' => 2, 'time_spent' => 1))); + $this->assertTrue($subtaskModel->update(array('id' => 1, 'task_id' => 1, 'time_estimated' => 5, 'time_spent' => 5))); + + $task = $taskFinderModel->getById(1); + $this->assertEquals(7, $task['time_estimated']); + $this->assertEquals(6, $task['time_spent']); + } + public function testRemove() { $taskCreationModel = new TaskCreationModel($this->container); diff --git a/tests/units/Model/SubtaskTimeTrackingModelTest.php b/tests/units/Model/SubtaskTimeTrackingModelTest.php index cfee5b14..8b0fe698 100644 --- a/tests/units/Model/SubtaskTimeTrackingModelTest.php +++ b/tests/units/Model/SubtaskTimeTrackingModelTest.php @@ -2,6 +2,7 @@ require_once __DIR__.'/../Base.php'; +use Kanboard\Model\ConfigModel; use Kanboard\Model\TaskFinderModel; use Kanboard\Model\TaskCreationModel; use Kanboard\Model\SubtaskModel; @@ -10,6 +11,42 @@ use Kanboard\Model\ProjectModel; class SubtaskTimeTrackingModelTest extends Base { + public function testToggleTimer() + { + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); + + $this->assertFalse($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_TODO)); + $this->assertTrue($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_INPROGRESS)); + $this->assertTrue($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_DONE)); + } + + public function testToggleTimerWhenFeatureDisabled() + { + $configModel = new ConfigModel($this->container); + $configModel->save(array('subtask_time_tracking' => '0')); + $this->container['memoryCache']->flush(); + + $taskCreationModel = new TaskCreationModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskTimeTrackingModel = new SubtaskTimeTrackingModel($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, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('title' => 'subtask #2', 'task_id' => 1, 'user_id' => 1))); + + $this->assertFalse($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_TODO)); + $this->assertFalse($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_INPROGRESS)); + $this->assertFalse($subtaskTimeTrackingModel->toggleTimer(1, 1, SubtaskModel::STATUS_DONE)); + } + public function testHasTimer() { $taskCreationModel = new TaskCreationModel($this->container); -- cgit v1.2.3 From df57b0f2c8b73959b6bcf237027d1c44670f961e Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 23 Jul 2016 23:06:51 -0400 Subject: Simplify mail subject for notifications --- app/Notification/MailNotification.php | 80 +++-------------------- tests/units/Notification/MailNotificationTest.php | 4 +- 2 files changed, 10 insertions(+), 74 deletions(-) diff --git a/app/Notification/MailNotification.php b/app/Notification/MailNotification.php index 2d27179c..a5f51b89 100644 --- a/app/Notification/MailNotification.php +++ b/app/Notification/MailNotification.php @@ -4,10 +4,6 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; use Kanboard\Core\Notification\NotificationInterface; -use Kanboard\Model\TaskModel; -use Kanboard\Model\TaskFileModel; -use Kanboard\Model\CommentModel; -use Kanboard\Model\SubtaskModel; /** * Email Notification @@ -76,76 +72,16 @@ class MailNotification extends Base implements NotificationInterface * Get the mail subject for a given template name * * @access public - * @param string $event_name Event name - * @param array $event_data Event data - * @return string - */ - public function getMailSubject($event_name, array $event_data) - { - switch ($event_name) { - case TaskFileModel::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New attachment'), $event_data); - break; - case CommentModel::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New comment'), $event_data); - break; - case CommentModel::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Comment updated'), $event_data); - break; - case SubtaskModel::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New subtask'), $event_data); - break; - case SubtaskModel::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Subtask updated'), $event_data); - break; - case TaskModel::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New task'), $event_data); - break; - case TaskModel::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Task updated'), $event_data); - break; - case TaskModel::EVENT_CLOSE: - $subject = $this->getStandardMailSubject(e('Task closed'), $event_data); - break; - case TaskModel::EVENT_OPEN: - $subject = $this->getStandardMailSubject(e('Task opened'), $event_data); - break; - case TaskModel::EVENT_MOVE_COLUMN: - $subject = $this->getStandardMailSubject(e('Column change'), $event_data); - break; - case TaskModel::EVENT_MOVE_POSITION: - $subject = $this->getStandardMailSubject(e('Position change'), $event_data); - break; - case TaskModel::EVENT_MOVE_SWIMLANE: - $subject = $this->getStandardMailSubject(e('Swimlane change'), $event_data); - break; - case TaskModel::EVENT_ASSIGNEE_CHANGE: - $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); - break; - case TaskModel::EVENT_USER_MENTION: - case CommentModel::EVENT_USER_MENTION: - $subject = $this->getStandardMailSubject(e('Mentioned'), $event_data); - break; - case TaskModel::EVENT_OVERDUE: - $subject = e('[%s] Overdue tasks', $event_data['project_name']); - break; - default: - $subject = e('Notification'); - } - - return $subject; - } - - /** - * Get the mail subject for a given label - * - * @access private - * @param string $label Label - * @param array $data Template data + * @param string $eventName Event name + * @param array $eventData Event data * @return string */ - private function getStandardMailSubject($label, array $data) + public function getMailSubject($eventName, array $eventData) { - return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']); + return sprintf( + '[%s] %s', + $eventData['task']['project_name'], + $this->notificationModel->getTitleWithoutAuthor($eventName, $eventData) + ); } } diff --git a/tests/units/Notification/MailNotificationTest.php b/tests/units/Notification/MailNotificationTest.php index 6579d9bc..05f1f882 100644 --- a/tests/units/Notification/MailNotificationTest.php +++ b/tests/units/Notification/MailNotificationTest.php @@ -56,7 +56,7 @@ class MailNotificationTest extends Base 'changes' => array() ); $this->assertNotEmpty($mailNotification->getMailContent($eventName, $eventData)); - $this->assertNotEmpty($mailNotification->getMailSubject($eventName, $eventData)); + $this->assertStringStartsWith('[test] ', $mailNotification->getMailSubject($eventName, $eventData)); } } @@ -84,7 +84,7 @@ class MailNotificationTest extends Base ->with( $this->equalTo('test@localhost'), $this->equalTo('admin'), - $this->equalTo('[test][New task] test (#1)'), + $this->equalTo('[test] New task #1: test'), $this->stringContains('test') ); -- cgit v1.2.3 From 506ebf3bac302a63be7c32a03b872a9eefa689fc Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 24 Jul 2016 10:08:57 -0400 Subject: Fixed typo in template that prevent project permissions to be duplicated --- ChangeLog | 1 + app/Template/project_creation/create.php | 2 +- app/Template/project_view/duplicate.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1bc0eed3..6da73200 100644 --- a/ChangeLog +++ b/ChangeLog @@ -24,6 +24,7 @@ Improvements: Bug fixes: +* Fixed typo in template that prevent project permissions to be duplicated * Fixed search query with multiple assignees (nested OR conditions) * Fixed Markdown editor auto-grow on the task form (Safari) * Fixed compatibility issue with PHP 5.3 for OAuthUserProvider class diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php index d00883ba..b90b15c4 100644 --- a/app/Template/project_creation/create.php +++ b/app/Template/project_creation/create.php @@ -19,7 +19,7 @@

- form->checkbox('projectPermission', t('Permissions'), 1, true) ?> + form->checkbox('projectPermissionModel', t('Permissions'), 1, true) ?> form->checkbox('categoryModel', t('Categories'), 1, true) ?> diff --git a/app/Template/project_view/duplicate.php b/app/Template/project_view/duplicate.php index d66ff591..561378d1 100644 --- a/app/Template/project_view/duplicate.php +++ b/app/Template/project_view/duplicate.php @@ -11,7 +11,7 @@ form->csrf() ?> - form->checkbox('projectPermission', t('Permissions'), 1, true) ?> + form->checkbox('projectPermissionModel', t('Permissions'), 1, true) ?> form->checkbox('categoryModel', t('Categories'), 1, true) ?> -- cgit v1.2.3 From 51b2193fc43a25f309a8510b64027d40bf21e12d Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 24 Jul 2016 12:09:41 -0400 Subject: Move dashboard pagination into separate classes --- app/Controller/DashboardController.php | 72 ++------------------ app/Controller/UserListController.php | 7 +- app/Core/Base.php | 4 ++ app/Model/ProjectDuplicationModel.php | 2 +- app/Model/ProjectModel.php | 2 +- app/Model/TaskFinderModel.php | 86 ++++++++++++------------ app/Pagination/ProjectPagination.php | 35 ++++++++++ app/Pagination/SubtaskPagination.php | 36 ++++++++++ app/Pagination/TaskPagination.php | 35 ++++++++++ app/Pagination/UserPagination.php | 32 +++++++++ app/ServiceProvider/ClassProvider.php | 6 ++ app/Template/dashboard/projects.php | 4 +- app/Template/dashboard/subtasks.php | 4 +- app/Template/dashboard/tasks.php | 8 +-- tests/units/Model/TaskFinderModelTest.php | 2 +- tests/units/Pagination/ProjectPaginationTest.php | 35 ++++++++++ tests/units/Pagination/SubtaskPaginationTest.php | 36 ++++++++++ tests/units/Pagination/TaskPaginationTest.php | 30 +++++++++ tests/units/Pagination/UserPaginationTest.php | 27 ++++++++ 19 files changed, 337 insertions(+), 126 deletions(-) create mode 100644 app/Pagination/ProjectPagination.php create mode 100644 app/Pagination/SubtaskPagination.php create mode 100644 app/Pagination/TaskPagination.php create mode 100644 app/Pagination/UserPagination.php create mode 100644 tests/units/Pagination/ProjectPaginationTest.php create mode 100644 tests/units/Pagination/SubtaskPaginationTest.php create mode 100644 tests/units/Pagination/TaskPaginationTest.php create mode 100644 tests/units/Pagination/UserPaginationTest.php diff --git a/app/Controller/DashboardController.php b/app/Controller/DashboardController.php index 44874546..0133499f 100644 --- a/app/Controller/DashboardController.php +++ b/app/Controller/DashboardController.php @@ -2,9 +2,6 @@ namespace Kanboard\Controller; -use Kanboard\Model\ProjectModel; -use Kanboard\Model\SubtaskModel; - /** * Dashboard Controller * @@ -13,63 +10,6 @@ use Kanboard\Model\SubtaskModel; */ class DashboardController extends BaseController { - /** - * Get project pagination - * - * @access private - * @param integer $user_id - * @param string $action - * @param integer $max - * @return \Kanboard\Core\Paginator - */ - private function getProjectPaginator($user_id, $action, $max) - { - return $this->paginator - ->setUrl('DashboardController', $action, array('pagination' => 'projects', 'user_id' => $user_id)) - ->setMax($max) - ->setOrder(ProjectModel::TABLE.'.name') - ->setQuery($this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))) - ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); - } - - /** - * Get task pagination - * - * @access private - * @param integer $user_id - * @param string $action - * @param integer $max - * @return \Kanboard\Core\Paginator - */ - private function getTaskPaginator($user_id, $action, $max) - { - return $this->paginator - ->setUrl('DashboardController', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) - ->setMax($max) - ->setOrder('tasks.id') - ->setQuery($this->taskFinderModel->getUserQuery($user_id)) - ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); - } - - /** - * Get subtask pagination - * - * @access private - * @param integer $user_id - * @param string $action - * @param integer $max - * @return \Kanboard\Core\Paginator - */ - private function getSubtaskPaginator($user_id, $action, $max) - { - return $this->paginator - ->setUrl('DashboardController', $action, array('pagination' => 'subtasks', 'user_id' => $user_id)) - ->setMax($max) - ->setOrder('tasks.id') - ->setQuery($this->subtaskModel->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))) - ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); - } - /** * Dashboard overview * @@ -81,9 +21,9 @@ class DashboardController extends BaseController $this->response->html($this->helper->layout->dashboard('dashboard/show', array( 'title' => t('Dashboard'), - 'project_paginator' => $this->getProjectPaginator($user['id'], 'show', 10), - 'task_paginator' => $this->getTaskPaginator($user['id'], 'show', 10), - 'subtask_paginator' => $this->getSubtaskPaginator($user['id'], 'show', 10), + 'project_paginator' => $this->projectPagination->getDashboardPaginator($user['id'], 'show', 10), + 'task_paginator' => $this->taskPagination->getDashboardPaginator($user['id'], 'show', 10), + 'subtask_paginator' => $this->subtaskPagination->getDashboardPaginator($user['id'], 'show', 10), 'user' => $user, ))); } @@ -99,7 +39,7 @@ class DashboardController extends BaseController $this->response->html($this->helper->layout->dashboard('dashboard/tasks', array( 'title' => t('My tasks'), - 'paginator' => $this->getTaskPaginator($user['id'], 'tasks', 50), + 'paginator' => $this->taskPagination->getDashboardPaginator($user['id'], 'tasks', 50), 'user' => $user, ))); } @@ -115,7 +55,7 @@ class DashboardController extends BaseController $this->response->html($this->helper->layout->dashboard('dashboard/subtasks', array( 'title' => t('My subtasks'), - 'paginator' => $this->getSubtaskPaginator($user['id'], 'subtasks', 50), + 'paginator' => $this->subtaskPagination->getDashboardPaginator($user['id'], 'subtasks', 50), 'user' => $user, ))); } @@ -131,7 +71,7 @@ class DashboardController extends BaseController $this->response->html($this->helper->layout->dashboard('dashboard/projects', array( 'title' => t('My projects'), - 'paginator' => $this->getProjectPaginator($user['id'], 'projects', 25), + 'paginator' => $this->projectPagination->getDashboardPaginator($user['id'], 'projects', 25), 'user' => $user, ))); } diff --git a/app/Controller/UserListController.php b/app/Controller/UserListController.php index 31fcdd44..888583fa 100644 --- a/app/Controller/UserListController.php +++ b/app/Controller/UserListController.php @@ -17,12 +17,7 @@ class UserListController extends BaseController */ public function show() { - $paginator = $this->paginator - ->setUrl('UserListController', 'show') - ->setMax(30) - ->setOrder('username') - ->setQuery($this->userModel->getQuery()) - ->calculate(); + $paginator = $this->userPagination->getListingPaginator(); $this->response->html($this->helper->layout->app('user_list/show', array( 'title' => t('Users').' ('.$paginator->getTotal().')', diff --git a/app/Core/Base.php b/app/Core/Base.php index 563013bd..68604785 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -122,6 +122,10 @@ use Pimple\Container; * @property \Kanboard\Model\UserNotificationFilterModel $userNotificationFilterModel * @property \Kanboard\Model\UserUnreadNotificationModel $userUnreadNotificationModel * @property \Kanboard\Model\UserMetadataModel $userMetadataModel + * @property \Kanboard\Pagination\TaskPagination $taskPagination + * @property \Kanboard\Pagination\SubtaskPagination $subtaskPagination + * @property \Kanboard\Pagination\ProjectPagination $projectPagination + * @property \Kanboard\Pagination\UserPagination $userPagination * @property \Kanboard\Validator\ActionValidator $actionValidator * @property \Kanboard\Validator\AuthValidator $authValidator * @property \Kanboard\Validator\ColumnValidator $columnValidator diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php index 94b83c80..d32fa367 100644 --- a/app/Model/ProjectDuplicationModel.php +++ b/app/Model/ProjectDuplicationModel.php @@ -159,7 +159,7 @@ class ProjectDuplicationModel extends Base } /** - * Make sure that the creator of the duplicated project is alsp owner + * Make sure that the creator of the duplicated project is also owner * * @access private * @param integer $dst_project_id diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php index 850531c9..d2019b72 100644 --- a/app/Model/ProjectModel.php +++ b/app/Model/ProjectModel.php @@ -318,7 +318,7 @@ class ProjectModel extends Base public function getQueryColumnStats(array $project_ids) { if (empty($project_ids)) { - return $this->db->table(ProjectModel::TABLE)->limit(0); + return $this->db->table(ProjectModel::TABLE)->eq(ProjectModel::TABLE.'.id', 0); } return $this->db diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php index 7268052c..924f339b 100644 --- a/app/Model/TaskFinderModel.php +++ b/app/Model/TaskFinderModel.php @@ -63,19 +63,19 @@ class TaskFinderModel extends Base return $this->db ->table(TaskModel::TABLE) ->columns( - 'tasks.id', - 'tasks.title', - 'tasks.date_due', - 'tasks.date_creation', - 'tasks.project_id', - 'tasks.color_id', - 'tasks.priority', - 'tasks.time_spent', - 'tasks.time_estimated', - 'tasks.is_active', - 'tasks.creator_id', - 'projects.name AS project_name', - 'columns.title AS column_title' + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.date_due', + TaskModel::TABLE.'.date_creation', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.priority', + TaskModel::TABLE.'.time_spent', + TaskModel::TABLE.'.time_estimated', + TaskModel::TABLE.'.is_active', + TaskModel::TABLE.'.creator_id', + ProjectModel::TABLE.'.name AS project_name', + ColumnModel::TABLE.'.title AS column_title' ) ->join(ProjectModel::TABLE, 'id', 'project_id') ->join(ColumnModel::TABLE, 'id', 'column_id') @@ -103,36 +103,36 @@ class TaskFinderModel extends Base '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links', '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links', '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone', - 'tasks.id', - 'tasks.reference', - 'tasks.title', - 'tasks.description', - 'tasks.date_creation', - 'tasks.date_modification', - 'tasks.date_completed', - 'tasks.date_started', - 'tasks.date_due', - 'tasks.color_id', - 'tasks.project_id', - 'tasks.column_id', - 'tasks.swimlane_id', - 'tasks.owner_id', - 'tasks.creator_id', - 'tasks.position', - 'tasks.is_active', - 'tasks.score', - 'tasks.category_id', - 'tasks.priority', - 'tasks.date_moved', - 'tasks.recurrence_status', - 'tasks.recurrence_trigger', - 'tasks.recurrence_factor', - 'tasks.recurrence_timeframe', - 'tasks.recurrence_basedate', - 'tasks.recurrence_parent', - 'tasks.recurrence_child', - 'tasks.time_estimated', - 'tasks.time_spent', + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.reference', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.description', + TaskModel::TABLE.'.date_creation', + TaskModel::TABLE.'.date_modification', + TaskModel::TABLE.'.date_completed', + TaskModel::TABLE.'.date_started', + TaskModel::TABLE.'.date_due', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.column_id', + TaskModel::TABLE.'.swimlane_id', + TaskModel::TABLE.'.owner_id', + TaskModel::TABLE.'.creator_id', + TaskModel::TABLE.'.position', + TaskModel::TABLE.'.is_active', + TaskModel::TABLE.'.score', + TaskModel::TABLE.'.category_id', + TaskModel::TABLE.'.priority', + TaskModel::TABLE.'.date_moved', + TaskModel::TABLE.'.recurrence_status', + TaskModel::TABLE.'.recurrence_trigger', + TaskModel::TABLE.'.recurrence_factor', + TaskModel::TABLE.'.recurrence_timeframe', + TaskModel::TABLE.'.recurrence_basedate', + TaskModel::TABLE.'.recurrence_parent', + TaskModel::TABLE.'.recurrence_child', + TaskModel::TABLE.'.time_estimated', + TaskModel::TABLE.'.time_spent', UserModel::TABLE.'.username AS assignee_username', UserModel::TABLE.'.name AS assignee_name', UserModel::TABLE.'.email AS assignee_email', diff --git a/app/Pagination/ProjectPagination.php b/app/Pagination/ProjectPagination.php new file mode 100644 index 00000000..8f1fa87c --- /dev/null +++ b/app/Pagination/ProjectPagination.php @@ -0,0 +1,35 @@ +paginator + ->setUrl('DashboardController', $method, array('pagination' => 'projects', 'user_id' => $user_id)) + ->setMax($max) + ->setOrder(ProjectModel::TABLE.'.name') + ->setQuery($this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); + } +} diff --git a/app/Pagination/SubtaskPagination.php b/app/Pagination/SubtaskPagination.php new file mode 100644 index 00000000..f0cd6148 --- /dev/null +++ b/app/Pagination/SubtaskPagination.php @@ -0,0 +1,36 @@ +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))) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + } +} diff --git a/app/Pagination/TaskPagination.php b/app/Pagination/TaskPagination.php new file mode 100644 index 00000000..a395ab84 --- /dev/null +++ b/app/Pagination/TaskPagination.php @@ -0,0 +1,35 @@ +paginator + ->setUrl('DashboardController', $method, array('pagination' => 'tasks', 'user_id' => $user_id)) + ->setMax($max) + ->setOrder(TaskModel::TABLE.'.id') + ->setQuery($this->taskFinderModel->getUserQuery($user_id)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); + } +} diff --git a/app/Pagination/UserPagination.php b/app/Pagination/UserPagination.php new file mode 100644 index 00000000..430b7d2f --- /dev/null +++ b/app/Pagination/UserPagination.php @@ -0,0 +1,32 @@ +paginator + ->setUrl('UserListController', 'show') + ->setMax(30) + ->setOrder(UserModel::TABLE.'.username') + ->setQuery($this->userModel->getQuery()) + ->calculate(); + } +} diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 9a71148b..aab41c74 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -122,6 +122,12 @@ class ClassProvider implements ServiceProviderInterface 'TaskExport', 'TransitionExport', ), + 'Pagination' => array( + 'TaskPagination', + 'SubtaskPagination', + 'ProjectPagination', + 'UserPagination', + ), 'Core' => array( 'DateParser', 'Lexer', diff --git a/app/Template/dashboard/projects.php b/app/Template/dashboard/projects.php index 962e4d83..3a7f1d86 100644 --- a/app/Template/dashboard/projects.php +++ b/app/Template/dashboard/projects.php @@ -6,8 +6,8 @@ - - + + diff --git a/app/Template/dashboard/subtasks.php b/app/Template/dashboard/subtasks.php index 8e0aa3ce..ca550e4c 100644 --- a/app/Template/dashboard/subtasks.php +++ b/app/Template/dashboard/subtasks.php @@ -6,10 +6,10 @@
order('Id', 'id') ?>order('', 'is_private') ?>order('Id', \Kanboard\Model\ProjectModel::TABLE.'.id') ?>order('', \Kanboard\Model\ProjectModel::TABLE.'.is_private') ?> order(t('Project'), \Kanboard\Model\ProjectModel::TABLE.'.name') ?>
- + - + getCollection() as $subtask): ?> diff --git a/app/Template/dashboard/tasks.php b/app/Template/dashboard/tasks.php index b3257c33..d9cb4f9e 100644 --- a/app/Template/dashboard/tasks.php +++ b/app/Template/dashboard/tasks.php @@ -6,12 +6,12 @@
order('Id', 'tasks.id') ?>order('Id', \Kanboard\Model\TaskModel::TABLE.'.id') ?> order(t('Project'), 'project_name') ?> order(t('Task'), 'task_name') ?>order(t('Subtask'), 'title') ?>order(t('Subtask'), \Kanboard\Model\SubtaskModel::TABLE.'.title') ?>
- + - - + + - + getCollection() as $task): ?> diff --git a/tests/units/Model/TaskFinderModelTest.php b/tests/units/Model/TaskFinderModelTest.php index 72da3b6d..b2e2bd84 100644 --- a/tests/units/Model/TaskFinderModelTest.php +++ b/tests/units/Model/TaskFinderModelTest.php @@ -9,7 +9,7 @@ use Kanboard\Model\ProjectModel; class TaskFinderModelTest extends Base { - public function testGetTasksForDashboard() + public function testGetTasksForDashboardWithHiddenColumn() { $taskCreationModel = new TaskCreationModel($this->container); $taskFinderModel = new TaskFinderModel($this->container); diff --git a/tests/units/Pagination/ProjectPaginationTest.php b/tests/units/Pagination/ProjectPaginationTest.php new file mode 100644 index 00000000..35532d0d --- /dev/null +++ b/tests/units/Pagination/ProjectPaginationTest.php @@ -0,0 +1,35 @@ +container); + $projectUserRoleModel = new ProjectUserRoleModel($this->container); + $userModel = new UserModel($this->container); + $projectPagination = new ProjectPagination($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2', 'is_private' => 1))); + $this->assertEquals(3, $projectModel->create(array('name' => 'Project #3'))); + $this->assertEquals(4, $projectModel->create(array('name' => 'Project #4', 'is_private' => 1))); + + $this->assertEquals(2, $userModel->create(array('username' => 'test'))); + $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MANAGER)); + $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MANAGER)); + + $this->assertCount(2, $projectPagination->getDashboardPaginator(2, 'projects', 5)->getCollection()); + $this->assertCount(0, $projectPagination->getDashboardPaginator(3, 'projects', 5)->getCollection()); + $this->assertCount(2, $projectPagination->getDashboardPaginator(2, 'projects', 5)->setOrder(ProjectModel::TABLE.'.id')->getCollection()); + $this->assertCount(2, $projectPagination->getDashboardPaginator(2, 'projects', 5)->setOrder(ProjectModel::TABLE.'.is_private')->getCollection()); + $this->assertCount(2, $projectPagination->getDashboardPaginator(2, 'projects', 5)->setOrder(ProjectModel::TABLE.'.name')->getCollection()); + } +} diff --git a/tests/units/Pagination/SubtaskPaginationTest.php b/tests/units/Pagination/SubtaskPaginationTest.php new file mode 100644 index 00000000..26a51a8b --- /dev/null +++ b/tests/units/Pagination/SubtaskPaginationTest.php @@ -0,0 +1,36 @@ +container); + $projectModel = new ProjectModel($this->container); + $subtaskModel = new SubtaskModel($this->container); + $subtaskPagination = new SubtaskPagination($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); + $this->assertEquals(1, $subtaskModel->create(array('task_id' => 1, 'title' => 'subtask #1', 'user_id' => 1))); + $this->assertEquals(2, $subtaskModel->create(array('task_id' => 2, 'title' => 'subtask #1', 'user_id' => 1))); + $this->assertEquals(3, $subtaskModel->create(array('task_id' => 1, 'title' => 'subtask #1', 'user_id' => 1))); + $this->assertEquals(4, $subtaskModel->create(array('task_id' => 2, 'title' => 'subtask #1'))); + $this->assertEquals(5, $subtaskModel->create(array('task_id' => 1, 'title' => 'subtask #1'))); + + $this->assertCount(3, $subtaskPagination->getDashboardPaginator(1, 'subtasks', 5)->getCollection()); + $this->assertCount(0, $subtaskPagination->getDashboardPaginator(2, 'subtasks', 5)->getCollection()); + $this->assertCount(3, $subtaskPagination->getDashboardPaginator(1, 'subtasks', 5)->setOrder(TaskModel::TABLE.'.id')->getCollection()); + $this->assertCount(3, $subtaskPagination->getDashboardPaginator(1, 'subtasks', 5)->setOrder('project_name')->getCollection()); + $this->assertCount(3, $subtaskPagination->getDashboardPaginator(1, 'subtasks', 5)->setOrder('task_name')->getCollection()); + $this->assertCount(3, $subtaskPagination->getDashboardPaginator(1, 'subtasks', 5)->setOrder(SubtaskModel::TABLE.'.title')->getCollection()); + } +} diff --git a/tests/units/Pagination/TaskPaginationTest.php b/tests/units/Pagination/TaskPaginationTest.php new file mode 100644 index 00000000..027212e2 --- /dev/null +++ b/tests/units/Pagination/TaskPaginationTest.php @@ -0,0 +1,30 @@ +container); + $projectModel = new ProjectModel($this->container); + $taskPagination = new TaskPagination($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); + + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->getCollection()); + $this->assertCount(0, $taskPagination->getDashboardPaginator(2, 'tasks', 5)->getCollection()); + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->setOrder(TaskModel::TABLE.'.id')->getCollection()); + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->setOrder('project_name')->getCollection()); + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->setOrder(TaskModel::TABLE.'.title')->getCollection()); + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->setOrder(TaskModel::TABLE.'.priority')->getCollection()); + $this->assertCount(1, $taskPagination->getDashboardPaginator(1, 'tasks', 5)->setOrder(TaskModel::TABLE.'.date_due')->getCollection()); + } +} diff --git a/tests/units/Pagination/UserPaginationTest.php b/tests/units/Pagination/UserPaginationTest.php new file mode 100644 index 00000000..c475aacd --- /dev/null +++ b/tests/units/Pagination/UserPaginationTest.php @@ -0,0 +1,27 @@ +container); + $userPagination = new UserPagination($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'test1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'test2'))); + + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('id')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('username')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('name')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('email')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('role')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('twofactor_activated')->setDirection('DESC')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('is_ldap_user')->getCollection()); + $this->assertCount(3, $userPagination->getListingPaginator()->setOrder('is_active')->getCollection()); + } +} -- cgit v1.2.3 From a6d22bf2715347d4f340376efee75dc57176c8b6 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 24 Jul 2016 13:00:59 -0400 Subject: Remove username for dashboard sidebar and change titles --- app/Controller/DashboardController.php | 18 ++++++++++-------- app/Template/dashboard/sidebar.php | 1 - 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/Controller/DashboardController.php b/app/Controller/DashboardController.php index 0133499f..f32f8552 100644 --- a/app/Controller/DashboardController.php +++ b/app/Controller/DashboardController.php @@ -20,7 +20,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/show', array( - 'title' => t('Dashboard'), + 'title' => t('Dashboard for %s', $this->helper->user->getFullname($user)), 'project_paginator' => $this->projectPagination->getDashboardPaginator($user['id'], 'show', 10), 'task_paginator' => $this->taskPagination->getDashboardPaginator($user['id'], 'show', 10), 'subtask_paginator' => $this->subtaskPagination->getDashboardPaginator($user['id'], 'show', 10), @@ -38,7 +38,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/tasks', array( - 'title' => t('My tasks'), + 'title' => t('Tasks overview for %s', $this->helper->user->getFullname($user)), 'paginator' => $this->taskPagination->getDashboardPaginator($user['id'], 'tasks', 50), 'user' => $user, ))); @@ -54,7 +54,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/subtasks', array( - 'title' => t('My subtasks'), + 'title' => t('Subtasks overview for %s', $this->helper->user->getFullname($user)), 'paginator' => $this->subtaskPagination->getDashboardPaginator($user['id'], 'subtasks', 50), 'user' => $user, ))); @@ -70,7 +70,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/projects', array( - 'title' => t('My projects'), + 'title' => t('Projects overview for %s', $this->helper->user->getFullname($user)), 'paginator' => $this->projectPagination->getDashboardPaginator($user['id'], 'projects', 25), 'user' => $user, ))); @@ -86,7 +86,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/activity', array( - 'title' => t('My activity stream'), + 'title' => t('Activity stream for %s', $this->helper->user->getFullname($user)), 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermissionModel->getActiveProjectIds($user['id']), 100), 'user' => $user, ))); @@ -99,9 +99,11 @@ class DashboardController extends BaseController */ public function calendar() { + $user = $this->getUser(); + $this->response->html($this->helper->layout->dashboard('dashboard/calendar', array( - 'title' => t('My calendar'), - 'user' => $this->getUser(), + 'title' => t('Calendar for %s', $this->helper->user->getFullname($user)), + 'user' => $user, ))); } @@ -115,7 +117,7 @@ class DashboardController extends BaseController $user = $this->getUser(); $this->response->html($this->helper->layout->dashboard('dashboard/notifications', array( - 'title' => t('My notifications'), + 'title' => t('Notifications for %s', $this->helper->user->getFullname($user)), 'notifications' => $this->userUnreadNotificationModel->getAll($user['id']), 'user' => $user, ))); diff --git a/app/Template/dashboard/sidebar.php b/app/Template/dashboard/sidebar.php index 86cc20f8..df4e91a5 100644 --- a/app/Template/dashboard/sidebar.php +++ b/app/Template/dashboard/sidebar.php @@ -1,5 +1,4 @@
order('Id', 'tasks.id') ?>order('Id', \Kanboard\Model\TaskModel::TABLE.'.id') ?> order(t('Project'), 'project_name') ?>order(t('Task'), 'title') ?>order(t('Priority'), 'tasks.priority') ?>order(t('Task'), \Kanboard\Model\TaskModel::TABLE.'.title') ?>order(t('Priority'), \Kanboard\Model\TaskModel::TABLE.'.priority') ?> order(t('Due date'), 'date_due') ?>order(t('Due date'), \Kanboard\Model\TaskModel::TABLE.'.date_due') ?> order(t('Column'), 'column_title') ?>
+ " + >
diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php index 79a1b389..f8901289 100644 --- a/app/Template/project_header/dropdown.php +++ b/app/Template/project_header/dropdown.php @@ -20,14 +20,6 @@ "> -
  • - - - - -
  • user->hasProjectAccess('TaskCreationController', 'show', $project['id'])): ?> diff --git a/assets/css/app.min.css b/assets/css/app.min.css index 2ddd4a8b..daf3fb24 100644 --- a/assets/css/app.min.css +++ b/assets/css/app.min.css @@ -1 +1 @@ -a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.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,.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}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;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:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.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-date,input.form-datetime{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-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.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-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.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;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.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-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:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.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}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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%}#main .confirm{max-width:700px;font-size:1.1em}.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;font-size:.95em;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;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:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#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:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}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:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;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:#666}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 silver;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:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.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:flex;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{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.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,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;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}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file +a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.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}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.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,.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}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;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:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.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-date,input.form-datetime{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-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.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-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.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;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.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-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:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.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}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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%}#main .confirm{max-width:700px;font-size:1.1em}.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;font-size:.95em;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;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:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#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:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}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:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;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:#666}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 silver;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:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.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:flex;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{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.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,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;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}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file diff --git a/assets/css/print.min.css b/assets/css/print.min.css index 080626b3..d63b1216 100644 --- a/assets/css/print.min.css +++ b/assets/css/print.min.css @@ -1 +1 @@ -a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.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,.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}.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-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}td.board-column-task-collapsed{font-weight:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;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}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.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{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.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}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file +a:hover,th a{text-decoration:none;color:#333}.table-fixed td,.table-fixed th{overflow:hidden}#board td,td{vertical-align:top}#comments form,.board-column-collapsed,.page-header,.sidebar,header{display:none}.table-fixed td,.task-board-collapsed{white-space:nowrap;text-overflow:ellipsis}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none;border:1px dotted #aaa}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.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,.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}.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-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}td.board-column-task-collapsed{font-weight:700;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:150%;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:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{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}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.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}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}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:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.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;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.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{font-size:.9em;color:#666}.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;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.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}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}.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}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.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:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left: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;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555} \ No newline at end of file diff --git a/assets/css/src/board.css b/assets/css/src/board.css index 95e04108..ff3844b7 100644 --- a/assets/css/src/board.css +++ b/assets/css/src/board.css @@ -126,7 +126,6 @@ a.board-swimlane-toggle:focus { /* board task list */ .board-task-list { - overflow: auto; min-height: 60px; } diff --git a/assets/js/app.min.js b/assets/js/app.min.js index 2a8178e4..7f3ba893 100644 --- a/assets/js/app.min.js +++ b/assets/js/app.min.js @@ -1,2 +1,2 @@ -"use strict";var Kanboard={};Kanboard.Accordion=function(t){this.app=t},Kanboard.Accordion.prototype.listen=function(){$(document).on("click",".accordion-toggle",function(t){var e=$(this).parents(".accordion-section");t.preventDefault(),e.hasClass("accordion-collapsed")?(e.find(".accordion-content").show(),e.removeClass("accordion-collapsed")):(e.find(".accordion-content").hide(),e.addClass("accordion-collapsed"))})},Kanboard.App=function(){this.controllers={}},Kanboard.App.prototype.get=function(t){return this.controllers[t]},Kanboard.App.prototype.execute=function(){for(var t in Kanboard)if("App"!==t){var e=new Kanboard[t](this);this.controllers[t]=e,"function"==typeof e.execute&&e.execute(),"function"==typeof e.listen&&e.listen(),"function"==typeof e.focus&&e.focus(),"function"==typeof e.keyboardShortcuts&&e.keyboardShortcuts()}this.focus(),this.chosen(),this.keyboardShortcuts(),this.datePicker(),this.autoComplete(),this.tagAutoComplete()},Kanboard.App.prototype.keyboardShortcuts=function(){var t=this;Mousetrap.bindGlobal("mod+enter",function(){var e=$("form");1==e.length?e.submit():e.length>1&&("INPUT"===document.activeElement.tagName||"TEXTAREA"===document.activeElement.tagName?$(document.activeElement).parents("form").submit():t.get("Popover").isOpen()&&$("#popover-container form").submit())}),Mousetrap.bind("b",function(t){t.preventDefault(),$("#board-selector").trigger("chosen:open")}),Mousetrap.bindGlobal("esc",function(){t.get("Popover").close(),t.get("Dropdown").close()}),Mousetrap.bind("?",function(){t.get("Popover").open($("body").data("keyboard-shortcut-url"))})},Kanboard.App.prototype.focus=function(){$(document).on("focus",".auto-select",function(){$(this).select()}),$(document).on("mouseup",".auto-select",function(t){t.preventDefault()})},Kanboard.App.prototype.chosen=function(){$(".chosen-select").each(function(){var t=$(this).data("search-threshold");void 0===t&&(t=10),$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:t})}),$(".select-auto-redirect").change(function(){var t=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(t,$(this).val())})},Kanboard.App.prototype.datePicker=function(){var t=$("body"),e=t.data("js-date-format"),a=t.data("js-time-format"),o=t.data("js-lang");$.datepicker.setDefaults($.datepicker.regional[o]),$.timepicker.setDefaults($.timepicker.regional[o]),$(".form-date").datepicker({showOtherMonths:!0,selectOtherMonths:!0,dateFormat:e,constrainInput:!1}),$(".form-datetime").datetimepicker({dateFormat:e,timeFormat:a,constrainInput:!1})},Kanboard.App.prototype.tagAutoComplete=function(){$(".tag-autocomplete").select2({tags:!0})},Kanboard.App.prototype.autoComplete=function(){$(".autocomplete").each(function(){var t=$(this),e=t.data("dst-field"),a=t.data("dst-extra-field");""==$("#form-"+e).val()&&t.parent().find("button[type=submit]").attr("disabled","disabled"),t.autocomplete({source:t.data("search-url"),minLength:1,select:function(o,n){$("input[name="+e+"]").val(n.item.id),a&&$("input[name="+a+"]").val(n.item[a]),t.parent().find("button[type=submit]").removeAttr("disabled")}})})},Kanboard.App.prototype.hasId=function(t){return!!document.getElementById(t)},Kanboard.App.prototype.showLoadingIcon=function(){$("body").append(' ')},Kanboard.App.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()},Kanboard.App.prototype.formatDuration=function(t){return t>=86400?Math.round(t/86400)+"d":t>=3600?Math.round(t/3600)+"h":t>=60?Math.round(t/60)+"m":t+"s"},Kanboard.App.prototype.isVisible=function(){var t="";return"undefined"!=typeof document.hidden?t="visibilityState":"undefined"!=typeof document.mozHidden?t="mozVisibilityState":"undefined"!=typeof document.msHidden?t="msVisibilityState":"undefined"!=typeof document.webkitHidden&&(t="webkitVisibilityState"),""!=t?"visible"==document[t]:!0},Kanboard.AvgTimeColumnChart=function(t){this.app=t},Kanboard.AvgTimeColumnChart.prototype.execute=function(){this.app.hasId("analytic-avg-time-column")&&this.show()},Kanboard.AvgTimeColumnChart.prototype.show=function(){var t=$("#chart"),e=t.data("metrics"),a=[t.data("label")],o=[];for(var n in e)a.push(e[n].average),o.push(e[n].title);c3.generate({data:{columns:[a],type:"bar"},bar:{width:{ratio:.5}},axis:{x:{type:"category",categories:o},y:{tick:{format:this.app.formatDuration}}},legend:{show:!1}})},Kanboard.BoardCollapsedMode=function(t){this.app=t},Kanboard.BoardCollapsedMode.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("s",function(){t.toggle()})},Kanboard.BoardCollapsedMode.prototype.toggle=function(){var t=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(e){$(".filter-display-mode").toggle(),t.app.get("BoardDragAndDrop").refresh(e)}})},Kanboard.BoardColumnScrolling=function(t){this.app=t},Kanboard.BoardColumnScrolling.prototype.execute=function(){this.app.hasId("board")&&(this.render(),$(window).on("load",this.render),$(window).resize(this.render))},Kanboard.BoardColumnScrolling.prototype.listen=function(){var t=this;$(document).on("click",".filter-toggle-height",function(e){e.preventDefault(),t.toggle()})},Kanboard.BoardColumnScrolling.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardColumnScrolling.prototype.toggle=function(){var t=localStorage.getItem("column_scroll");void 0==t&&(t=1),localStorage.setItem("column_scroll",0==t?1:0),this.render()},Kanboard.BoardColumnScrolling.prototype.render=function(){var t=$(".board-task-list"),e=$(".board-rotation-wrapper"),a=$(".filter-max-height"),o=$(".filter-min-height");if(0==localStorage.getItem("column_scroll")){var n=80;a.show(),o.hide(),e.css("min-height",""),t.each(function(){var t=$(this).height();t>n&&(n=t)}),t.css("min-height",n),t.css("height","")}else if(a.hide(),o.show(),$(".board-swimlane").length>1)t.each(function(){$(this).height()>500?$(this).css("height",500):($(this).css("min-height",320),e.css("min-height",320))});else{var n=$(window).height()-170;t.css("height",n),e.css("min-height",n)}},Kanboard.BoardColumnView=function(t){this.app=t},Kanboard.BoardColumnView.prototype.execute=function(){this.app.hasId("board")&&this.render()},Kanboard.BoardColumnView.prototype.listen=function(){var t=this;$(document).on("click",".board-toggle-column-view",function(){t.toggle($(this).data("column-id"))})},Kanboard.BoardColumnView.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardColumnView.prototype.render=function(){var t=this;$(".board-column-header").each(function(){var e=$(this).data("column-id");localStorage.getItem("hidden_column_"+e)&&t.hideColumn(e)})},Kanboard.BoardColumnView.prototype.toggle=function(t){localStorage.getItem("hidden_column_"+t)?this.showColumn(t):this.hideColumn(t)},Kanboard.BoardColumnView.prototype.hideColumn=function(t){$(".board-column-"+t+" .board-column-expanded").hide(),$(".board-column-"+t+" .board-column-collapsed").show(),$(".board-column-header-"+t+" .board-column-expanded").hide(),$(".board-column-header-"+t+" .board-column-collapsed").show(),$(".board-column-header-"+t).each(function(){$(this).removeClass("board-column-compact"),$(this).addClass("board-column-header-collapsed")}),$(".board-column-"+t).each(function(){$(this).addClass("board-column-task-collapsed")}),$(".board-column-"+t+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+t).height())}),localStorage.setItem("hidden_column_"+t,1)},Kanboard.BoardColumnView.prototype.showColumn=function(t){$(".board-column-"+t+" .board-column-expanded").show(),$(".board-column-"+t+" .board-column-collapsed").hide(),$(".board-column-header-"+t+" .board-column-expanded").show(),$(".board-column-header-"+t+" .board-column-collapsed").hide(),$(".board-column-header-"+t).removeClass("board-column-header-collapsed"),$(".board-column-"+t).removeClass("board-column-task-collapsed"),0==localStorage.getItem("horizontal_scroll")&&$(".board-column-header-"+t).addClass("board-column-compact"),localStorage.removeItem("hidden_column_"+t)},Kanboard.BoardHorizontalScrolling=function(t){this.app=t},Kanboard.BoardHorizontalScrolling.prototype.execute=function(){this.app.hasId("board")&&this.render()},Kanboard.BoardHorizontalScrolling.prototype.listen=function(){var t=this;$(document).on("click",".filter-toggle-scrolling",function(e){e.preventDefault(),t.toggle()})},Kanboard.BoardHorizontalScrolling.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("c",function(){t.toggle()})},Kanboard.BoardHorizontalScrolling.prototype.onBoardRendered=function(){this.render()},Kanboard.BoardHorizontalScrolling.prototype.toggle=function(){var t=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",0==t?1:0),this.render()},Kanboard.BoardHorizontalScrolling.prototype.render=function(){0==localStorage.getItem("horizontal_scroll")?($(".filter-wide").show(),$(".filter-compact").hide(),$("#board-container").addClass("board-container-compact"),$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")):($(".filter-wide").hide(),$(".filter-compact").show(),$("#board-container").removeClass("board-container-compact"),$("#board th").removeClass("board-column-compact"))},Kanboard.BoardPolling=function(t){this.app=t},Kanboard.BoardPolling.prototype.execute=function(){if(this.app.hasId("board")){var t=parseInt($("#board").attr("data-check-interval"));t>0&&window.setInterval(this.check.bind(this),1e3*t)}},Kanboard.BoardPolling.prototype.check=function(){if(this.app.isVisible()&&!this.app.get("BoardDragAndDrop").savingInProgress){var t=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:$("#board").data("check-url"),statusCode:{200:function(e){t.app.get("BoardDragAndDrop").refresh(e)},304:function(){t.app.hideLoadingIcon()}}})}},Kanboard.BoardTask=function(t){this.app=t},Kanboard.BoardTask.prototype.listen=function(){var t=this;$(document).on("click",".task-board-change-assignee",function(e){e.preventDefault(),e.stopPropagation(),t.app.get("Popover").open($(this).data("url"))}),$(document).on("click",".task-board",function(t){"A"!=t.target.tagName&&"IMG"!=t.target.tagName&&(window.location=$(this).data("task-url"))})},Kanboard.BoardTask.prototype.keyboardShortcuts=function(){var t=this;t.app.hasId("board")&&Mousetrap.bind("n",function(){t.app.get("Popover").open($("#board").data("task-creation-url"))})},Kanboard.BurndownChart=function(t){this.app=t},Kanboard.BurndownChart.prototype.execute=function(){this.app.hasId("analytic-burndown")&&this.show()},Kanboard.BurndownChart.prototype.show=function(){for(var t=$("#chart"),e=t.data("metrics"),a=[[t.data("label-total")]],o=[],n=d3.time.format("%Y-%m-%d"),r=d3.time.format(t.data("date-format")),i=0;i0&&(void 0==a[0][i]&&a[0].push(0),a[0][i]+=e[i][s]),0==s&&o.push(r(n.parse(e[i][s]))));c3.generate({data:{columns:a},axis:{x:{type:"category",categories:o}}})},Kanboard.Calendar=function(t){this.app=t},Kanboard.Calendar.prototype.execute=function(){var t=$("#calendar");1==t.length&&this.show(t)},Kanboard.Calendar.prototype.show=function(t){t.fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(e){$.ajax({cache:!1,url:t.data("save-url"),contentType:"application/json",type:"POST",processData:!1,data:JSON.stringify({task_id:e.id,date_due:e.start.format()})})},viewRender:function(){var e=t.data("check-url"),a={start:t.fullCalendar("getView").start.format(),end:t.fullCalendar("getView").end.format()};for(var o in a)e+="&"+o+"="+a[o];$.getJSON(e,function(e){t.fullCalendar("removeEvents"),t.fullCalendar("addEventSource",e),t.fullCalendar("rerenderEvents")})}})},Kanboard.Column=function(t){this.app=t},Kanboard.Column.prototype.listen=function(){this.dragAndDrop()},Kanboard.Column.prototype.dragAndDrop=function(){var t=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")}),$(".columns-table tbody").sortable({forcePlaceholderSize:!0,handle:"td:first i",helper:function(t,e){return e.children().each(function(){$(this).width($(this).width())}),e},stop:function(e,a){var o=a.item;o.removeClass("draggable-item-selected"),t.savePosition(o.data("column-id"),o.index()+1)},start:function(t,e){e.item.addClass("draggable-item-selected")}}).disableSelection()},Kanboard.Column.prototype.savePosition=function(t,e){var a=$(".columns-table").data("save-position-url"),o=this;this.app.showLoadingIcon(),$.ajax({cache:!1,url:a,contentType:"application/json",type:"POST",processData:!1,data:JSON.stringify({column_id:t,position:e}),complete:function(){o.app.hideLoadingIcon()}})},Kanboard.CompareHoursColumnChart=function(t){this.app=t},Kanboard.CompareHoursColumnChart.prototype.execute=function(){this.app.hasId("analytic-compare-hours")&&this.show()},Kanboard.CompareHoursColumnChart.prototype.show=function(){var t=$("#chart"),e=t.data("metrics"),a=t.data("label-open"),o=t.data("label-closed"),n=[t.data("label-spent")],r=[t.data("label-estimated")],i=[];for(var s in e)n.push(parseFloat(e[s].time_spent)),r.push(parseFloat(e[s].time_estimated)),i.push("open"==s?a:o);c3.generate({data:{columns:[n,r],type:"bar"},bar:{width:{ratio:.2}},axis:{x:{type:"category",categories:i}},legend:{show:!0}})},Kanboard.CumulativeFlowDiagram=function(t){this.app=t},Kanboard.CumulativeFlowDiagram.prototype.execute=function(){this.app.hasId("analytic-cfd")&&this.show()},Kanboard.CumulativeFlowDiagram.prototype.show=function(){for(var t=$("#chart"),e=t.data("metrics"),a=[],o=[],n=[],r=d3.time.format("%Y-%m-%d"),i=d3.time.format(t.data("date-format")),s=0;s0&&o.push(e[s][d])):(a[d].push(e[s][d]),0==d&&n.push(i(r.parse(e[s][d]))));c3.generate({data:{columns:a,type:"area-spline",groups:[o]},axis:{x:{type:"category",categories:n}}})},Kanboard.Dropdown=function(t){this.app=t},Kanboard.Dropdown.prototype.listen=function(){var t=this;$(document).on("click",function(){t.close()}),$(document).on("click",".dropdown-menu",function(e){e.preventDefault(),e.stopImmediatePropagation(),t.close();var a=$(this).next("ul"),o=$(this).offset();$("body").append(jQuery("
    ",{id:"dropdown"})),a.clone().appendTo("#dropdown");var n=$("#dropdown ul");n.addClass("dropdown-submenu-open");var r=n.outerHeight(),i=n.outerWidth();o.top+r-$(window).scrollTop()<$(window).height()||$(window).scrollTop()+o.top$(window).width()?n.css("left",o.left-i+$(this).outerWidth()):n.css("left",o.left)}),$(document).on("click",".dropdown-submenu-open li",function(t){$(t.target).is("li")&&$(this).find("a:visible")[0].click()})},Kanboard.Dropdown.prototype.close=function(){$("#dropdown").remove()},Kanboard.Dropdown.prototype.onPopoverOpened=function(){this.close()},Kanboard.FileUpload=function(t){this.app=t,this.files=[],this.currentFile=0},Kanboard.FileUpload.prototype.onPopoverOpened=function(){var t=document.getElementById("file-dropzone"),e=this;t&&(t.ondragover=t.ondragenter=function(t){t.stopPropagation(),t.preventDefault()},t.ondrop=function(t){t.stopPropagation(),t.preventDefault(),e.files=t.dataTransfer.files,e.show(),$("#file-error-max-size").hide()},$(document).on("click","#file-browser",function(t){t.preventDefault(),$("#file-form-element").get(0).click()}),$(document).on("click","#file-upload-button",function(t){t.preventDefault(),e.currentFile=0,e.checkFiles()}),$("#file-form-element").change(function(){e.files=document.getElementById("file-form-element").files,e.show(),$("#file-error-max-size").hide()}))},Kanboard.FileUpload.prototype.show=function(){if($("#file-list").remove(),this.files.length>0){$("#file-upload-button").prop("disabled",!1),$("#file-dropzone-inner").hide();for(var t=jQuery("