diff options
640 files changed, 16918 insertions, 10985 deletions
diff --git a/.docker/crontab/kanboard b/.docker/crontab/kanboard new file mode 100644 index 00000000..91ad044e --- /dev/null +++ b/.docker/crontab/kanboard @@ -0,0 +1 @@ +1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1 diff --git a/.docker/kanboard/config.php b/.docker/kanboard/config.php new file mode 100644 index 00000000..fa1c5971 --- /dev/null +++ b/.docker/kanboard/config.php @@ -0,0 +1,3 @@ +<?php + +define('ENABLE_URL_REWRITE', true); diff --git a/.docker/nginx/nginx.conf b/.docker/nginx/nginx.conf new file mode 100644 index 00000000..88c532fe --- /dev/null +++ b/.docker/nginx/nginx.conf @@ -0,0 +1,71 @@ +user nginx; +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + server_tokens off; + access_log off; + error_log /dev/stderr; + + server { + listen 80; + server_name localhost; + index index.php; + root /var/www/kanboard; + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/var/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + } + + location /data { + return 404; + } + + location ~* ^.+\.(log|sqlite)$ { + return 404; + } + + location ~ /\.ht { + return 404; + } + + location ~* ^.+\.(ico|jpg|gif|png|css|js|svg|eot|ttf|woff|woff2|otf)$ { + expires 7d; + etag on; + } + + client_max_body_size 32M; + gzip on; + gzip_comp_level 3; + gzip_disable "msie6"; + gzip_vary on; + gzip_types + text/javascript + application/javascript + application/json + text/xml + application/xml + application/rss+xml + text/css + text/plain; + } +} diff --git a/.docker/php/conf.d/local.ini b/.docker/php/conf.d/local.ini new file mode 100644 index 00000000..77b6daf0 --- /dev/null +++ b/.docker/php/conf.d/local.ini @@ -0,0 +1,15 @@ +expose_php = Off +error_reporting = E_ALL +display_errors = Off +log_errors = On +error_log = /dev/stderr +date.timezone = UTC +allow_url_fopen = On +post_max_size = 30M +upload_max_filesize = 30M +opcache.max_accelerated_files = 7963 +opcache.validate_timestamps = Off +opcache.save_comments = 0 +opcache.load_comments = 0 +opcache.fast_shutdown = 1 +opcache.enable_file_override = On
\ No newline at end of file diff --git a/.docker/php/php-fpm.conf b/.docker/php/php-fpm.conf new file mode 100644 index 00000000..5d84c2ae --- /dev/null +++ b/.docker/php/php-fpm.conf @@ -0,0 +1,23 @@ +[global] +error_log = /dev/stderr +log_level = error +daemonize = no + +[www] +env[DATABASE_URL] = $DATABASE_URL +env[DEBUG] = $DEBUG +env[DEBUG_FILE] = $DEBUG_FILE +env[ENABLE_SYSLOG] = $ENABLE_SYSLOG + +catch_workers_output = yes +user = nginx +group = nginx +listen.owner = nginx +listen.group = nginx +listen = /var/run/php-fpm.sock +pm = dynamic +pm.max_children = 20 +pm.start_servers = 1 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 2048 diff --git a/.docker/services.d/.s6-svscan/finish b/.docker/services.d/.s6-svscan/finish new file mode 100755 index 00000000..fc3b1e93 --- /dev/null +++ b/.docker/services.d/.s6-svscan/finish @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/true
\ No newline at end of file diff --git a/.docker/services.d/cron/run b/.docker/services.d/cron/run new file mode 100755 index 00000000..da378099 --- /dev/null +++ b/.docker/services.d/cron/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +crond -f
\ No newline at end of file diff --git a/.docker/services.d/nginx/run b/.docker/services.d/nginx/run new file mode 100755 index 00000000..40a8b54a --- /dev/null +++ b/.docker/services.d/nginx/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +nginx -g "daemon off;"
\ No newline at end of file diff --git a/.docker/services.d/php/run b/.docker/services.d/php/run new file mode 100755 index 00000000..e7d2dadd --- /dev/null +++ b/.docker/services.d/php/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +php-fpm -F
\ No newline at end of file @@ -18,4 +18,5 @@ config.php data/files data/cache /vendor -*.bak
\ No newline at end of file +*.bak +!.docker/kanboard/config.php diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 651f2618..f4576949 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -20,28 +20,34 @@ Contributors: - [Cluxter](https://github.com/cluxter) - [Cmer](https://github.com/chncsu) - [Colin Williams](https://github.com/crwilliams) +- [Connor Bode](https://github.com/connorbode) - [Crash5](https://github.com/crash5) - [Creador30](https://github.com/creador30) - [Cynthia Pereira](https://github.com/cynthiapereira) +- [d4rk5eed](https://github.com/d4rk5eed) - [Damian](https://github.com/dromek) - [Daniel Raknes](https://github.com/danielraknes) - [David-Norris](https://github.com/David-Norris) - [Dmitry](https://github.com/dmkcv) +- [Djpadz](https://github.com/djpadz) - [Draza (bdpsoft)](https://github.com/bdpsoft) - [Eskiso](https://github.com/eSkiSo) - [Esteban Monge](https://github.com/EstebanMonge) +- [Eugene (JohnBat26)](https://github.com/JohnBat26) - [Fábio Hideki](https://github.com/fabiohxcx) - [Fabiano Castro Pereira](https://github.com/fabiano-pereira) - [Federico Lazcano](https://github.com/lazki) - [Fengchao](https://github.com/fengchao) - [Floaltvater](https://github.com/floaltvater) - [Gavlepeter](https://github.com/gavlepeter) +- [Gerardo Zamudio](https://github.com/gerardozamudio) - [Goofy](https://github.com/goofy-bz) - [Hendrik Stocker](https://github.com/hendrik-stoker) - [Iterate From 0](https://github.com/freebsd-kanboard) - [Jan Dittrich](https://github.com/jdittrich) - [Janne Mäntyharju](https://github.com/JanneMantyharju) - [Jean-François Magnier](https://github.com/lefakir) +- [Jeff Guillou](https://github.com/jf-guillou) - [Jesusaplsoft](https://github.com/jesusaplsoft) - [Jesús Marín](https://github.com/alu0100502114) - [Jules Verhaeren](https://github.com/julesverhaeren) @@ -61,6 +67,7 @@ Contributors: - [Max Kamashev](https://github.com/ukko) - [mfoucrier](https://github.com/mfoucrier) - [Matthew Cillo](https://github.com/peripatetic-sojourner) +- [Maxime Corteel](https://github.com/mcorteel) - [Mgro](https://github.com/mgro) - [Michael Lüpkes](https://github.com/mluepkes) - [Mihailov Vasilievic Filho](https://github.com/mihailov-vf) @@ -69,6 +76,7 @@ Contributors: - [Nala Ginrut](https://github.com/NalaGinrut) - [Nekohayo](https://github.com/nekohayo) - [Nicolas Lœuillet](https://github.com/nicosomb) +- [Nick Blackledge](https://github.com/nttict-nick-b) - [Norcnorc](https://github.com/norcnorc) - [Nramel](https://github.com/nramel) - [Null-Kelvin](https://github.com/Null-Kelvin) @@ -78,6 +86,7 @@ Contributors: - [Oliver Jakoubek](https://github.com/jakoubek) - [Olivier Maridat](https://github.com/oliviermaridat) - [Oren Ben-Kiki](https://github.com/orenbenkiki) +- [Patrick Van Elk](https://github.com/patrickvanelk) - Paolo Mainieri - [Pavel Roušar](https://github.com/rousarp) - [Peller Zoltan](https://github.com/PierP) @@ -89,10 +98,12 @@ Contributors: - [Rafaelrossa](https://github.com/rafaelrossa) - [Raphaël Doursenaud](https://github.com/rdoursenaud) - [René Stoltenberg](https://github.com/rstoltenberg) +- [Renothing](https://github.com/renothing) - [Rzeka](https://github.com/rzeka) - [Sebastien Pacilly](https://github.com/spacilly) - [Sebastian Reese](https://github.com/ReeseSebastian) - [Semyon Novikov](https://github.com/semka) +- [StavrosKa](https://github.com/StavrosKa) - [Sylvain Veyrié](https://github.com/turb) - [Thomas Lutz](https://github.com/phoen1x) - [Timo](https://github.com/BlueTeck) @@ -100,6 +111,7 @@ Contributors: - [Tomáš Votruba](https://github.com/TomasVotruba) - [Toomyem](https://github.com/Toomyem) - [Tony G. Bolaño](https://github.com/tonybolanyo) +- [Trapulo](https://github.com/Trapulo) - [Torsten](https://github.com/misterfu) - [Troloo](https://github.com/troloo) - [Typz](https://github.com/Typz) @@ -1,10 +1,102 @@ -Version 1.0.25 (unreleased) +Version 1.0.27 (unreleased) -------------- +Improvements: + +* Always display project name and task title in task views +* Improve automatic action creation +* Move notifications to the bottom of the screen +* Added the possibility to import automatic actions from another project +* Added Ajax loading icon for submit buttons +* Added support for HTTP header "X-Forwarded-Proto: https" + +Security issues: + +* Access allowed to any tasks from the shared public board by changing the URL parameters + +Bug fixes: + +* Ambiguous column name with very old version of Sqlite + +Version 1.0.26 +-------------- + +Breaking changes: + +* API procedures: + - "moveColumnUp" and "moveColumnDown" are replaced by "changeColumnPosition" + - "moveSwimlaneUp" and "moveSwimlaneDown" are replaced by "changeSwimlanePosition" + +New features: + +* Add drag and drop to change subtasks, swimlanes and columns positions +* Add file drag and drop and asynchronous upload +* Enable/Disable users +* Add setting option to disable private projects +* Add new config option to disable logout + +Improvements: + +* Use inline popup to create new columns +* Improve filter box design +* Improve image thumbnails and files table +* Add confirmation inline popup to remove custom filter +* Increase client_max_body_size value for Nginx +* Split Board model into multiple classes +* Improve logging for the Docker image + +Bug fixes: + +* Fix PHP notices during creation of first project and in subtasks table +* Fix filter dropdown not accessible when there are too many items +* Fix regression: unable to change project in "task move/duplicate to another project" + +Version 1.0.25 +-------------- + +Breaking changes: + +* Core functionalities moved to external plugins: + - Google Auth: https://github.com/kanboard/plugin-google-auth + - Github Auth: https://github.com/kanboard/plugin-github-auth + - Gitlab Auth: https://github.com/kanboard/plugin-gitlab-auth + New features: +* When creating a new project, have the possibility to select another project to duplicate +* Add a "Me" button to assignee form element +* Add external links for tasks with plugin api * Add project owner (Directly Responsible Individual) * Add configurable task priority +* Add Greek translation +* Add automatic actions to close tasks with no activity +* Add automatic actions to send an email when there is no activity on a task +* Regroup all daily background tasks in one command: "cronjob" +* Add task dropdown menu on listing pages + +Improvements: + +* New Dockerfile based on Alpine Linux and Nginx/PHP-FPM +* The date time format can be chosen in application settings +* Export only open tasks in iCal feed +* Remove time form on task summary page and move that to task edit form +* Replace box shadow by a larger border width when a task is recently modified +* Do not refresh the whole page when changing subtask status +* Add dropdown menu with inline popup for all task actions +* Change sidebar style +* Change task summary layout +* Use inline popup for subtasks, categories, swimlanes, actions and columns +* Move homepage menus to the user dropdown +* Have a new task assigned to the creator by default instead of "no assignee" +* Show progress for task links in board tooltips +* Simplify code to handle ajax popover and redirects +* Simplify layout and templates generation +* Move task form elements to Task helper + +Bug fixes: + +* Category label is broken on the board if there's a url in the description +* Fix pagination on task time tracking page Version 1.0.24 -------------- @@ -1,23 +1,33 @@ -FROM ubuntu:14.04 +FROM gliderlabs/alpine:latest MAINTAINER Frederic Guillot <fred@kanboard.net> -RUN apt-get update && apt-get install -y apache2 php5 php5-gd php5-ldap php5-sqlite git curl && apt-get clean -RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && a2enmod rewrite -RUN sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf -RUN curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer -RUN cd /var/www && git clone --depth 1 https://github.com/fguillot/kanboard.git -RUN cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install -RUN rm -rf /var/www/html && mv /var/www/kanboard /var/www/html -RUN chown -R www-data:www-data /var/www/html/data +RUN apk-install nginx bash ca-certificates s6 curl \ + php-fpm php-json php-zlib php-xml php-dom php-ctype php-opcache php-zip \ + php-pdo php-pdo_mysql php-pdo_sqlite php-pdo_pgsql php-ldap \ + php-gd php-mcrypt php-openssl php-phar \ + && curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer -VOLUME /var/www/html/data +RUN cd /var/www \ + && curl -LO https://github.com/fguillot/kanboard/archive/master.zip \ + && unzip -qq master.zip \ + && rm -f *.zip \ + && mv kanboard-master kanboard \ + && cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install \ + && chown -R nginx:nginx /var/www/kanboard \ + && chown -R nginx:nginx /var/lib/nginx + +COPY .docker/services.d /etc/services.d +COPY .docker/php/conf.d/local.ini /etc/php/conf.d/ +COPY .docker/php/php-fpm.conf /etc/php/ +COPY .docker/nginx/nginx.conf /etc/nginx/ +COPY .docker/kanboard/config.php /var/www/kanboard/ +COPY .docker/kanboard/config.php /var/www/kanboard/ +COPY .docker/crontab/kanboard /var/spool/cron/crontabs/nginx EXPOSE 80 -ENV APACHE_RUN_USER www-data -ENV APACHE_RUN_GROUP www-data -ENV APACHE_LOG_DIR /var/log/apache2 -ENV APACHE_LOCK_DIR /var/lock/apache2 -ENV APACHE_PID_FILE /var/run/apache2.pid +VOLUME /var/www/kanboard/data +VOLUME /var/www/kanboard/plugins -CMD /usr/sbin/apache2ctl -D FOREGROUND +ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] +CMD [] @@ -1,12 +1,12 @@ BUILD_DIR = /tmp -CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown screenshot filters gantt)) -CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown)) +CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask tasklink markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown upload filters gantt project files views)) +CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask tasklink markdown)) CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min)) -JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task Project TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router)) +JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot FileUpload Calendar Board Column Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router)) JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete)) -JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn)) +JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn)) all: css js @@ -62,6 +62,7 @@ archive: @ rm -rf ${BUILD_DIR}/kanboard/*.markdown @ rm -rf ${BUILD_DIR}/kanboard/*.lock @ rm -rf ${BUILD_DIR}/kanboard/*.json + @ rm -rf ${BUILD_DIR}/kanboard/.docker @ cd ${BUILD_DIR}/kanboard && find ./vendor -name doc -type d -exec rm -rf {} +; @ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +; @ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +; @@ -87,8 +88,11 @@ test-archive: @ rm -rf ${BUILD_DIR}/kanboard/.*.yml @ rm -rf ${BUILD_DIR}/kanboard/*.md @ rm -rf ${BUILD_DIR}/kanboard/*.markdown - @ rm -rf ${BUILD_DIR}/kanboard/*.lock - @ rm -rf ${BUILD_DIR}/kanboard/*.json + @ rm -rf ${BUILD_DIR}/kanboard/Dockerfile + @ rm -rf ${BUILD_DIR}/kanboard/.docker + @ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile + @ rm -rf ${BUILD_DIR}/kanboard/app.json + @ rm -rf ${BUILD_DIR}/kanboard/plugins/.gitignore @ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +; @ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +; @ cd ${BUILD_DIR}/kanboard && find ./vendor -name tests -type d -exec rm -rf {} +; @@ -117,7 +121,7 @@ test-postgres: unittest: test-sqlite test-mysql test-postgres sql: - @ pg_dump --schema-only --no-owner --file app/Schema/Sql/postgres.sql kanboard + @ pg_dump --schema-only --no-owner --no-privileges --file app/Schema/Sql/postgres.sql kanboard @ pg_dump -d kanboard --column-inserts --data-only --table settings >> app/Schema/Sql/postgres.sql @ pg_dump -d kanboard --column-inserts --data-only --table links >> app/Schema/Sql/postgres.sql @@ -134,4 +138,13 @@ sql: @ let pg_version=`psql -U postgres -A -c 'copy(select version from schema_version) to stdout;' kanboard` ;\ echo "INSERT INTO schema_version VALUES ('$$pg_version');" >> app/Schema/Sql/postgres.sql +docker-image: + @ docker build -t kanboard/kanboard:latest . + +docker-push: + @ docker push kanboard/kanboard:latest + +docker-run: + @ docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:latest + .PHONY: all diff --git a/app/Action/Base.php b/app/Action/Base.php index efc52f04..e5c65a17 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -125,13 +125,14 @@ abstract class Base extends \Kanboard\Core\Base $params[] = $key.'='.var_export($value, true); } - return $this->getName().'('.implode('|', $params).'])'; + return $this->getName().'('.implode('|', $params).')'; } /** * Set project id * * @access public + * @param integer $project_id * @return Base */ public function setProjectId($project_id) @@ -154,10 +155,10 @@ abstract class Base extends \Kanboard\Core\Base /** * Set an user defined parameter * - * @access public - * @param string $name Parameter name - * @param mixed $value Value - * @param Base + * @access public + * @param string $name Parameter name + * @param mixed $value Value + * @return Base */ public function setParam($name, $value) { @@ -271,6 +272,7 @@ abstract class Base extends \Kanboard\Core\Base * @access public * @param string $event * @param string $description + * @return Base */ public function addEvent($event, $description = '') { diff --git a/app/Action/CommentCreationMoveTaskColumn.php b/app/Action/CommentCreationMoveTaskColumn.php index 4473cf91..11224d67 100644 --- a/app/Action/CommentCreationMoveTaskColumn.php +++ b/app/Action/CommentCreationMoveTaskColumn.php @@ -71,7 +71,7 @@ class CommentCreationMoveTaskColumn extends Base return false; } - $column = $this->board->getColumn($data['column_id']); + $column = $this->column->getById($data['column_id']); return (bool) $this->comment->create(array( 'comment' => t('Moved to column %s', $column['title']), diff --git a/app/Action/TaskCloseNoActivity.php b/app/Action/TaskCloseNoActivity.php new file mode 100644 index 00000000..59f7f56a --- /dev/null +++ b/app/Action/TaskCloseNoActivity.php @@ -0,0 +1,95 @@ +<?php + +namespace Kanboard\Action; + +use Kanboard\Model\Task; + +/** + * Close automatically a task after when inactive + * + * @package action + * @author Frederic Guillot + */ +class TaskCloseNoActivity extends Base +{ + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task when there is no activity'); + } + + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array(Task::EVENT_DAILY_CRONJOB); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'duration' => t('Duration in days') + ); + } + + /** + * 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_modification']; + + if ($duration > $max) { + $results[] = $this->taskStatus->close($task['id']); + } + } + + 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/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index 5bcdce08..5f05136e 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -74,7 +74,7 @@ class TaskDuplicateAnotherProject extends Base */ public function doAction(array $data) { - $destination_column_id = $this->board->getFirstColumn($this->getParam('project_id')); + $destination_column_id = $this->column->getFirstColumnId($this->getParam('project_id')); return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } diff --git a/app/Action/TaskEmailNoActivity.php b/app/Action/TaskEmailNoActivity.php new file mode 100644 index 00000000..c5d7a797 --- /dev/null +++ b/app/Action/TaskEmailNoActivity.php @@ -0,0 +1,124 @@ +<?php + +namespace Kanboard\Action; + +use Kanboard\Model\Task; + +/** + * Email a task with no activity + * + * @package action + * @author Frederic Guillot + */ +class TaskEmailNoActivity extends Base +{ + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Send email when there is no activity on a task'); + } + + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_DAILY_CRONJOB, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'user_id' => t('User that will receive the email'), + 'subject' => t('Email subject'), + 'duration' => t('Duration in days'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('tasks'); + } + + /** + * 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; + } + + /** + * 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) + { + $results = array(); + $max = $this->getParam('duration') * 86400; + $user = $this->user->getById($this->getParam('user_id')); + + if (! empty($user['email'])) { + foreach ($data['tasks'] as $task) { + $duration = time() - $task['date_modification']; + + if ($duration > $max) { + $results[] = $this->sendEmail($task['id'], $user); + } + } + } + + return in_array(true, $results, true); + } + + /** + * Send email + * + * @access private + * @param integer $task_id + * @param array $user + * @return boolean + */ + private function sendEmail($task_id, array $user) + { + $task = $this->taskFinder->getDetails($task_id); + + $this->emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + $this->getParam('subject'), + $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url'))) + ); + + return true; + } +} diff --git a/app/Analytic/AverageLeadCycleTimeAnalytic.php b/app/Analytic/AverageLeadCycleTimeAnalytic.php index fd85f864..5a7a3c0f 100644 --- a/app/Analytic/AverageLeadCycleTimeAnalytic.php +++ b/app/Analytic/AverageLeadCycleTimeAnalytic.php @@ -99,6 +99,7 @@ class AverageLeadCycleTimeAnalytic extends Base * Get the 1000 last created tasks * * @access private + * @param integer $project_id * @return array */ private function getTasks($project_id) diff --git a/app/Analytic/AverageTimeSpentColumnAnalytic.php b/app/Analytic/AverageTimeSpentColumnAnalytic.php index c3cff548..11078323 100644 --- a/app/Analytic/AverageTimeSpentColumnAnalytic.php +++ b/app/Analytic/AverageTimeSpentColumnAnalytic.php @@ -40,7 +40,7 @@ class AverageTimeSpentColumnAnalytic extends Base private function initialize($project_id) { $stats = array(); - $columns = $this->board->getColumnsList($project_id); + $columns = $this->column->getList($project_id); foreach ($columns as $column_id => $column_title) { $stats[$column_id] = array( @@ -126,6 +126,7 @@ class AverageTimeSpentColumnAnalytic extends Base * * @access private * @param array $task + * @return integer */ private function getTaskTimeSpentInCurrentColumn(array &$task) { diff --git a/app/Analytic/TaskDistributionAnalytic.php b/app/Analytic/TaskDistributionAnalytic.php index 614c5f72..838652e3 100644 --- a/app/Analytic/TaskDistributionAnalytic.php +++ b/app/Analytic/TaskDistributionAnalytic.php @@ -23,7 +23,7 @@ class TaskDistributionAnalytic extends Base { $metrics = array(); $total = 0; - $columns = $this->board->getColumns($project_id); + $columns = $this->column->getAll($project_id); foreach ($columns as $column) { $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); diff --git a/app/Api/Board.php b/app/Api/Board.php index d615b1dc..185ac51a 100644 --- a/app/Api/Board.php +++ b/app/Api/Board.php @@ -15,39 +15,4 @@ class Board extends Base $this->checkProjectPermission($project_id); return $this->board->getBoard($project_id); } - - public function getColumns($project_id) - { - return $this->board->getColumns($project_id); - } - - public function getColumn($column_id) - { - return $this->board->getColumn($column_id); - } - - public function moveColumnUp($project_id, $column_id) - { - return $this->board->moveUp($project_id, $column_id); - } - - public function moveColumnDown($project_id, $column_id) - { - return $this->board->moveDown($project_id, $column_id); - } - - public function updateColumn($column_id, $title, $task_limit = 0, $description = '') - { - return $this->board->updateColumn($column_id, $title, $task_limit, $description); - } - - public function addColumn($project_id, $title, $task_limit = 0, $description = '') - { - return $this->board->addColumn($project_id, $title, $task_limit, $description); - } - - public function removeColumn($column_id) - { - return $this->board->removeColumn($column_id); - } } diff --git a/app/Api/Column.php b/app/Api/Column.php new file mode 100644 index 00000000..ddc3a5d0 --- /dev/null +++ b/app/Api/Column.php @@ -0,0 +1,42 @@ +<?php + +namespace Kanboard\Api; + +/** + * Column API controller + * + * @package api + * @author Frederic Guillot + */ +class Column extends Base +{ + public function getColumns($project_id) + { + return $this->column->getAll($project_id); + } + + public function getColumn($column_id) + { + return $this->column->getById($column_id); + } + + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') + { + return $this->column->update($column_id, $title, $task_limit, $description); + } + + public function addColumn($project_id, $title, $task_limit = 0, $description = '') + { + return $this->column->create($project_id, $title, $task_limit, $description); + } + + public function removeColumn($column_id) + { + return $this->column->remove($column_id); + } + + public function changeColumnPosition($project_id, $column_id, $position) + { + return $this->column->changePosition($project_id, $column_id, $position); + } +} diff --git a/app/Api/File.php b/app/Api/File.php index 269803e1..71c31c76 100644 --- a/app/Api/File.php +++ b/app/Api/File.php @@ -10,22 +10,22 @@ use Kanboard\Core\ObjectStorage\ObjectStorageException; * @package api * @author Frederic Guillot */ -class File extends \Kanboard\Core\Base +class File extends Base { - public function getFile($file_id) + public function getTaskFile($file_id) { - return $this->file->getById($file_id); + return $this->taskFile->getById($file_id); } - public function getAllFiles($task_id) + public function getAllTaskFiles($task_id) { - return $this->file->getAll($task_id); + return $this->taskFile->getAll($task_id); } - public function downloadFile($file_id) + public function downloadTaskFile($file_id) { try { - $file = $this->file->getById($file_id); + $file = $this->taskFile->getById($file_id); if (! empty($file)) { return base64_encode($this->objectStorage->get($file['path'])); @@ -36,23 +36,55 @@ class File extends \Kanboard\Core\Base } } - public function createFile($project_id, $task_id, $filename, $blob) + public function createTaskFile($project_id, $task_id, $filename, $blob) { try { - return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + return $this->taskFile->uploadContent($task_id, $filename, $blob); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); return false; } } + public function removeTaskFile($file_id) + { + return $this->taskFile->remove($file_id); + } + + public function removeAllTaskFiles($task_id) + { + return $this->taskFile->removeAll($task_id); + } + + // Deprecated procedures + + public function getFile($file_id) + { + return $this->getTaskFile($file_id); + } + + public function getAllFiles($task_id) + { + return $this->getAllTaskFiles($task_id); + } + + public function downloadFile($file_id) + { + return $this->downloadTaskFile($file_id); + } + + public function createFile($project_id, $task_id, $filename, $blob) + { + return $this->createTaskFile($project_id, $task_id, $filename, $blob); + } + public function removeFile($file_id) { - return $this->file->remove($file_id); + return $this->removeTaskFile($file_id); } public function removeAllFiles($task_id) { - return $this->file->removeAll($task_id); + return $this->removeAllTaskFiles($task_id); } } diff --git a/app/Api/Me.php b/app/Api/Me.php index df8ec070..ccc809ed 100644 --- a/app/Api/Me.php +++ b/app/Api/Me.php @@ -38,6 +38,10 @@ class Me extends Base public function createMyPrivateProject($name, $description = null) { + if ($this->config->get('disable_private_project', 0) == 1) { + return false; + } + $values = array( 'name' => $name, 'description' => $description, diff --git a/app/Api/Swimlane.php b/app/Api/Swimlane.php index 84c699ab..03a2819f 100644 --- a/app/Api/Swimlane.php +++ b/app/Api/Swimlane.php @@ -48,9 +48,11 @@ class Swimlane extends \Kanboard\Core\Base public function updateSwimlane($swimlane_id, $name, $description = null) { $values = array('id' => $swimlane_id, 'name' => $name); + if (!is_null($description)) { $values['description'] = $description; } + return $this->swimlane->update($values); } @@ -69,13 +71,8 @@ class Swimlane extends \Kanboard\Core\Base return $this->swimlane->enable($project_id, $swimlane_id); } - public function moveSwimlaneUp($project_id, $swimlane_id) - { - return $this->swimlane->moveUp($project_id, $swimlane_id); - } - - public function moveSwimlaneDown($project_id, $swimlane_id) + public function changeSwimlanePosition($project_id, $swimlane_id, $position) { - return $this->swimlane->moveDown($project_id, $swimlane_id); + return $this->swimlane->changePosition($project_id, $swimlane_id, $position); } } diff --git a/app/Api/Task.php b/app/Api/Task.php index f132bcd6..177a09c6 100644 --- a/app/Api/Task.php +++ b/app/Api/Task.php @@ -75,9 +75,9 @@ class Task extends Base } public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, - $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, - $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, - $recurrence_basedate = 0, $reference = '') + $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, + $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, + $recurrence_basedate = 0, $reference = '') { $this->checkProjectPermission($project_id); @@ -115,9 +115,9 @@ class Task extends Base } public function updateTask($id, $title = null, $color_id = null, $owner_id = null, - $date_due = null, $description = null, $category_id = null, $score = null, - $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, - $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) + $date_due = null, $description = null, $category_id = null, $score = null, + $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, + $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) { $this->checkTaskPermission($id); diff --git a/app/Api/User.php b/app/Api/User.php index 63c222fe..48337ac6 100644 --- a/app/Api/User.php +++ b/app/Api/User.php @@ -21,6 +21,11 @@ class User extends \Kanboard\Core\Base return $this->user->getById($user_id); } + public function getUserByName($username) + { + return $this->user->getByUsername($username); + } + public function getAllUsers() { return $this->user->getAll(); @@ -31,6 +36,21 @@ class User extends \Kanboard\Core\Base return $this->user->remove($user_id); } + public function disableUser($user_id) + { + return $this->user->disable($user_id); + } + + public function enableUser($user_id) + { + return $this->user->enable($user_id); + } + + public function isActiveUser($user_id) + { + return $this->user->isActive($user_id); + } + public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER) { $values = array( diff --git a/app/Auth/DatabaseAuth.php b/app/Auth/DatabaseAuth.php index 5a8ee64d..c13af687 100644 --- a/app/Auth/DatabaseAuth.php +++ b/app/Auth/DatabaseAuth.php @@ -65,6 +65,7 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa ->eq('username', $this->username) ->eq('disable_login_form', 0) ->eq('is_ldap_user', 0) + ->eq('is_active', 1) ->findOne(); if (! empty($user) && password_verify($this->password, $user['password'])) { @@ -83,7 +84,7 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa */ public function isValidSession() { - return $this->user->exists($this->userSession->getId()); + return $this->user->isActive($this->userSession->getId()); } /** diff --git a/app/Auth/GithubAuth.php b/app/Auth/GithubAuth.php deleted file mode 100644 index 83699581..00000000 --- a/app/Auth/GithubAuth.php +++ /dev/null @@ -1,143 +0,0 @@ -<?php - -namespace Kanboard\Auth; - -use Kanboard\Core\Base; -use Kanboard\Core\Security\OAuthAuthenticationProviderInterface; -use Kanboard\User\GithubUserProvider; - -/** - * Github Authentication Provider - * - * @package auth - * @author Frederic Guillot - */ -class GithubAuth extends Base implements OAuthAuthenticationProviderInterface -{ - /** - * User properties - * - * @access protected - * @var \Kanboard\User\GithubUserProvider - */ - protected $userInfo = null; - - /** - * OAuth2 instance - * - * @access protected - * @var \Kanboard\Core\Http\OAuth2 - */ - protected $service; - - /** - * OAuth2 code - * - * @access protected - * @var string - */ - protected $code = ''; - - /** - * Get authentication provider name - * - * @access public - * @return string - */ - public function getName() - { - return 'Github'; - } - - /** - * Authenticate the user - * - * @access public - * @return boolean - */ - public function authenticate() - { - $profile = $this->getProfile(); - - if (! empty($profile)) { - $this->userInfo = new GithubUserProvider($profile); - return true; - } - - return false; - } - - /** - * Set Code - * - * @access public - * @param string $code - * @return GithubAuth - */ - public function setCode($code) - { - $this->code = $code; - return $this; - } - - /** - * Get user object - * - * @access public - * @return GithubUserProvider - */ - public function getUser() - { - return $this->userInfo; - } - - /** - * Get configured OAuth2 service - * - * @access public - * @return \Kanboard\Core\Http\OAuth2 - */ - public function getService() - { - if (empty($this->service)) { - $this->service = $this->oauth->createService( - GITHUB_CLIENT_ID, - GITHUB_CLIENT_SECRET, - $this->helper->url->to('oauth', 'github', array(), '', true), - GITHUB_OAUTH_AUTHORIZE_URL, - GITHUB_OAUTH_TOKEN_URL, - array() - ); - } - - return $this->service; - } - - /** - * Get Github profile - * - * @access public - * @return array - */ - public function getProfile() - { - $this->getService()->getAccessToken($this->code); - - return $this->httpClient->getJson( - GITHUB_API_URL.'user', - array($this->getService()->getAuthorizationHeader()) - ); - } - - /** - * Unlink user - * - * @access public - * @param integer $userId - * @return bool - */ - public function unlink($userId) - { - return $this->user->update(array('id' => $userId, 'github_id' => '')); - } -} diff --git a/app/Auth/GitlabAuth.php b/app/Auth/GitlabAuth.php deleted file mode 100644 index c0a2cf9b..00000000 --- a/app/Auth/GitlabAuth.php +++ /dev/null @@ -1,143 +0,0 @@ -<?php - -namespace Kanboard\Auth; - -use Kanboard\Core\Base; -use Kanboard\Core\Security\OAuthAuthenticationProviderInterface; -use Kanboard\User\GitlabUserProvider; - -/** - * Gitlab Authentication Provider - * - * @package auth - * @author Frederic Guillot - */ -class GitlabAuth extends Base implements OAuthAuthenticationProviderInterface -{ - /** - * User properties - * - * @access private - * @var \Kanboard\User\GitlabUserProvider - */ - private $userInfo = null; - - /** - * OAuth2 instance - * - * @access protected - * @var \Kanboard\Core\Http\OAuth2 - */ - protected $service; - - /** - * OAuth2 code - * - * @access protected - * @var string - */ - protected $code = ''; - - /** - * Get authentication provider name - * - * @access public - * @return string - */ - public function getName() - { - return 'Gitlab'; - } - - /** - * Authenticate the user - * - * @access public - * @return boolean - */ - public function authenticate() - { - $profile = $this->getProfile(); - - if (! empty($profile)) { - $this->userInfo = new GitlabUserProvider($profile); - return true; - } - - return false; - } - - /** - * Set Code - * - * @access public - * @param string $code - * @return GitlabAuth - */ - public function setCode($code) - { - $this->code = $code; - return $this; - } - - /** - * Get user object - * - * @access public - * @return GitlabUserProvider - */ - public function getUser() - { - return $this->userInfo; - } - - /** - * Get configured OAuth2 service - * - * @access public - * @return \Kanboard\Core\Http\OAuth2 - */ - public function getService() - { - if (empty($this->service)) { - $this->service = $this->oauth->createService( - GITLAB_CLIENT_ID, - GITLAB_CLIENT_SECRET, - $this->helper->url->to('oauth', 'gitlab', array(), '', true), - GITLAB_OAUTH_AUTHORIZE_URL, - GITLAB_OAUTH_TOKEN_URL, - array() - ); - } - - return $this->service; - } - - /** - * Get Gitlab profile - * - * @access public - * @return array - */ - public function getProfile() - { - $this->getService()->getAccessToken($this->code); - - return $this->httpClient->getJson( - GITLAB_API_URL.'user', - array($this->getService()->getAuthorizationHeader()) - ); - } - - /** - * Unlink user - * - * @access public - * @param integer $userId - * @return bool - */ - public function unlink($userId) - { - return $this->user->update(array('id' => $userId, 'gitlab_id' => '')); - } -} diff --git a/app/Auth/GoogleAuth.php b/app/Auth/GoogleAuth.php deleted file mode 100644 index 6eacf0b0..00000000 --- a/app/Auth/GoogleAuth.php +++ /dev/null @@ -1,143 +0,0 @@ -<?php - -namespace Kanboard\Auth; - -use Kanboard\Core\Base; -use Kanboard\Core\Security\OAuthAuthenticationProviderInterface; -use Kanboard\User\GoogleUserProvider; - -/** - * Google Authentication Provider - * - * @package auth - * @author Frederic Guillot - */ -class GoogleAuth extends Base implements OAuthAuthenticationProviderInterface -{ - /** - * User properties - * - * @access protected - * @var \Kanboard\User\GoogleUserProvider - */ - protected $userInfo = null; - - /** - * OAuth2 instance - * - * @access protected - * @var \Kanboard\Core\Http\OAuth2 - */ - protected $service; - - /** - * OAuth2 code - * - * @access protected - * @var string - */ - protected $code = ''; - - /** - * Get authentication provider name - * - * @access public - * @return string - */ - public function getName() - { - return 'Google'; - } - - /** - * Authenticate the user - * - * @access public - * @return boolean - */ - public function authenticate() - { - $profile = $this->getProfile(); - - if (! empty($profile)) { - $this->userInfo = new GoogleUserProvider($profile); - return true; - } - - return false; - } - - /** - * Set Code - * - * @access public - * @param string $code - * @return GoogleAuth - */ - public function setCode($code) - { - $this->code = $code; - return $this; - } - - /** - * Get user object - * - * @access public - * @return GoogleUserProvider - */ - public function getUser() - { - return $this->userInfo; - } - - /** - * Get configured OAuth2 service - * - * @access public - * @return \Kanboard\Core\Http\OAuth2 - */ - public function getService() - { - if (empty($this->service)) { - $this->service = $this->oauth->createService( - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - $this->helper->url->to('oauth', 'google', array(), '', true), - 'https://accounts.google.com/o/oauth2/auth', - 'https://accounts.google.com/o/oauth2/token', - array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile') - ); - } - - return $this->service; - } - - /** - * Get Google profile - * - * @access public - * @return array - */ - public function getProfile() - { - $this->getService()->getAccessToken($this->code); - - return $this->httpClient->getJson( - 'https://www.googleapis.com/oauth2/v1/userinfo', - array($this->getService()->getAuthorizationHeader()) - ); - } - - /** - * Unlink user - * - * @access public - * @param integer $userId - * @return bool - */ - public function unlink($userId) - { - return $this->user->update(array('id' => $userId, 'google_id' => '')); - } -} diff --git a/app/Console/Base.php b/app/Console/Base.php index 4c5caf73..25d48e44 100644 --- a/app/Console/Base.php +++ b/app/Console/Base.php @@ -11,18 +11,19 @@ use Symfony\Component\Console\Command\Command; * @package console * @author Frederic Guillot * - * @property \Kanboard\Model\Notification $notification - * @property \Kanboard\Model\Project $project - * @property \Kanboard\Model\ProjectPermission $projectPermission - * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic - * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats - * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats - * @property \Kanboard\Model\SubtaskExport $subtaskExport - * @property \Kanboard\Model\OverdueNotification $overdueNotification - * @property \Kanboard\Model\Task $task - * @property \Kanboard\Model\TaskExport $taskExport - * @property \Kanboard\Model\TaskFinder $taskFinder - * @property \Kanboard\Model\Transition $transition + * @property \Kanboard\Export\SubtaskExport $subtaskExport + * @property \Kanboard\Export\TaskExport $taskExport + * @property \Kanboard\Export\TransitionExport $transitionExport + * @property \Kanboard\Model\Notification $notification + * @property \Kanboard\Model\Project $project + * @property \Kanboard\Model\ProjectPermission $projectPermission + * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats + * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats + * @property \Kanboard\Model\Task $task + * @property \Kanboard\Model\TaskFinder $taskFinder + * @property \Kanboard\Model\UserNotification $userNotification + * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter + * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ abstract class Base extends Command { diff --git a/app/Console/Cronjob.php b/app/Console/Cronjob.php new file mode 100644 index 00000000..3a5c5596 --- /dev/null +++ b/app/Console/Cronjob.php @@ -0,0 +1,32 @@ +<?php + +namespace Kanboard\Console; + +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\NullOutput; + +class Cronjob extends Base +{ + private $commands = array( + 'projects:daily-stats', + 'notification:overdue-tasks', + 'trigger:tasks', + ); + + protected function configure() + { + $this + ->setName('cronjob') + ->setDescription('Execute daily cronjob'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + foreach ($this->commands as $command) { + $job = $this->getApplication()->find($command); + $job->run(new ArrayInput(array('command' => $command)), new NullOutput()); + } + } +} diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotification.php index ffb9fab5..43be4df8 100644 --- a/app/Console/TaskOverdueNotification.php +++ b/app/Console/TaskOverdueNotification.php @@ -2,6 +2,7 @@ namespace Kanboard\Console; +use Kanboard\Model\Task; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -19,7 +20,7 @@ class TaskOverdueNotification extends Base protected function execute(InputInterface $input, OutputInterface $output) { - $tasks = $this->overdueNotification->sendOverdueTaskNotifications(); + $tasks = $this->sendOverdueTaskNotifications(); if ($input->getOption('show')) { $this->showTable($output, $tasks); @@ -47,4 +48,69 @@ class TaskOverdueNotification extends Base ->setRows($rows) ->render(); } + + /** + * Send overdue tasks + * + * @access public + */ + public function sendOverdueTaskNotifications() + { + $tasks = $this->taskFinder->getOverdueTasks(); + + foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { + $users = $this->userNotification->getUsersWithNotificationEnabled($project_id); + + foreach ($users as $user) { + $this->sendUserOverdueTaskNotifications($user, $project_tasks); + } + } + + return $tasks; + } + + /** + * Send overdue tasks for a given user + * + * @access public + * @param array $user + * @param array $tasks + */ + public function sendUserOverdueTaskNotifications(array $user, array $tasks) + { + $user_tasks = array(); + + foreach ($tasks as $task) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, array('task' => $task))) { + $user_tasks[] = $task; + } + } + + if (! empty($user_tasks)) { + $this->userNotification->sendUserNotification( + $user, + Task::EVENT_OVERDUE, + array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) + ); + } + } + + /** + * Group a collection of records by a column + * + * @access public + * @param array $collection + * @param string $column + * @return array + */ + public function groupByColumn(array $collection, $column) + { + $result = array(); + + foreach ($collection as $item) { + $result[$item[$column]][] = $item; + } + + return $result; + } } diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTrigger.php new file mode 100644 index 00000000..8d707211 --- /dev/null +++ b/app/Console/TaskTrigger.php @@ -0,0 +1,51 @@ +<?php + +namespace Kanboard\Console; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Kanboard\Model\Task; +use Kanboard\Event\TaskListEvent; + +class TaskTrigger extends Base +{ + protected function configure() + { + $this + ->setName('trigger:tasks') + ->setDescription('Trigger scheduler event for all tasks'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + foreach ($this->getProjectIds() as $project_id) { + $tasks = $this->taskFinder->getAll($project_id); + $nb_tasks = count($tasks); + + if ($nb_tasks > 0) { + $output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks); + $this->sendEvent($tasks, $project_id); + } + } + } + + private function getProjectIds() + { + $listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB); + $project_ids = array(); + + foreach ($listeners as $listener) { + $project_ids[] = $listener[0]->getProjectId(); + } + + return array_unique($project_ids); + } + + private function sendEvent(array &$tasks, $project_id) + { + $event = new TaskListEvent(array('project_id' => $project_id)); + $event->setTasks($tasks); + + $this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event); + } +} diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExport.php index 9ae41417..d9f805a4 100644 --- a/app/Console/TransitionExport.php +++ b/app/Console/TransitionExport.php @@ -21,7 +21,7 @@ class TransitionExport extends Base protected function execute(InputInterface $input, OutputInterface $output) { - $data = $this->transition->export( + $data = $this->transitionExport->export( $input->getArgument('project_id'), $input->getArgument('start_date'), $input->getArgument('end_date') diff --git a/app/Controller/Action.php b/app/Controller/Action.php index 645b53b7..8881e8ec 100644 --- a/app/Controller/Action.php +++ b/app/Controller/Action.php @@ -3,7 +3,7 @@ namespace Kanboard\Controller; /** - * Automatic actions management + * Automatic Actions * * @package controller * @author Frederic Guillot @@ -20,14 +20,14 @@ class Action extends Base $project = $this->getProject(); $actions = $this->action->getAllByProject($project['id']); - $this->response->html($this->projectLayout('action/index', array( + $this->response->html($this->helper->layout->project('action/index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, 'actions' => $actions, 'available_actions' => $this->actionManager->getAvailableActions(), 'available_events' => $this->eventManager->getAll(), 'available_params' => $this->actionManager->getAvailableParameters($actions), - 'columns_list' => $this->board->getColumnsList($project['id']), + 'columns_list' => $this->column->getList($project['id']), 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'colors_list' => $this->color->getList(), @@ -38,98 +38,6 @@ class Action extends Base } /** - * Choose the event according to the action (step 2) - * - * @access public - */ - public function event() - { - $project = $this->getProject(); - $values = $this->request->getValues(); - - if (empty($values['action_name']) || empty($values['project_id'])) { - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); - } - - $this->response->html($this->projectLayout('action/event', array( - 'values' => $values, - 'project' => $project, - 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), - 'title' => t('Automatic actions') - ))); - } - - /** - * Define action parameters (step 3) - * - * @access public - */ - public function params() - { - $project = $this->getProject(); - $values = $this->request->getValues(); - - if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) { - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); - } - - $action = $this->actionManager->getAction($values['action_name']); - $action_params = $action->getActionRequiredParameters(); - - if (empty($action_params)) { - $this->doCreation($project, $values + array('params' => array())); - } - - $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - unset($projects_list[$project['id']]); - - $this->response->html($this->projectLayout('action/params', array( - 'values' => $values, - 'action_params' => $action_params, - 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), - 'projects_list' => $projects_list, - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project['id']), - 'links_list' => $this->link->getList(0, false), - 'project' => $project, - 'title' => t('Automatic actions') - ))); - } - - /** - * Create a new action (last step) - * - * @access public - */ - public function create() - { - $this->doCreation($this->getProject(), $this->request->getValues()); - } - - /** - * Save the action - * - * @access private - * @param array $project Project properties - * @param array $values Form values - */ - private function doCreation(array $project, array $values) - { - list($valid, ) = $this->actionValidator->validateCreation($values); - - if ($valid) { - if ($this->action->create($values) !== false) { - $this->flash->success(t('Your automatic action have been created successfully.')); - } else { - $this->flash->failure(t('Unable to create your automatic action.')); - } - } - - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); - } - - /** * Confirmation dialog before removing an action * * @access public @@ -138,7 +46,7 @@ class Action extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('action/remove', array( + $this->response->html($this->helper->layout->project('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), 'available_events' => $this->eventManager->getAll(), 'available_actions' => $this->actionManager->getAvailableActions(), diff --git a/app/Controller/ActionCreation.php b/app/Controller/ActionCreation.php new file mode 100644 index 00000000..24a12d92 --- /dev/null +++ b/app/Controller/ActionCreation.php @@ -0,0 +1,121 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Action Creation + * + * @package controller + * @author Frederic Guillot + */ +class ActionCreation extends Base +{ + /** + * Show the form (step 1) + * + * @access public + */ + public function create() + { + $project = $this->getProject(); + + $this->response->html($this->template->render('action_creation/create', array( + 'project' => $project, + 'values' => array('project_id' => $project['id']), + 'available_actions' => $this->actionManager->getAvailableActions(), + ))); + } + + /** + * Choose the event according to the action (step 2) + * + * @access public + */ + public function event() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + if (empty($values['action_name']) || empty($values['project_id'])) { + return $this->create(); + } + + $this->response->html($this->template->render('action_creation/event', array( + 'values' => $values, + 'project' => $project, + 'available_actions' => $this->actionManager->getAvailableActions(), + 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), + ))); + } + + /** + * Define action parameters (step 3) + * + * @access public + */ + public function params() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) { + return $this->create(); + } + + $action = $this->actionManager->getAction($values['action_name']); + $action_params = $action->getActionRequiredParameters(); + + if (empty($action_params)) { + $this->doCreation($project, $values + array('params' => array())); + } + + $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + unset($projects_list[$project['id']]); + + $this->response->html($this->template->render('action_creation/params', array( + 'values' => $values, + 'action_params' => $action_params, + 'columns_list' => $this->column->getList($project['id']), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), + 'projects_list' => $projects_list, + 'colors_list' => $this->color->getList(), + 'categories_list' => $this->category->getList($project['id']), + 'links_list' => $this->link->getList(0, false), + 'project' => $project, + 'available_actions' => $this->actionManager->getAvailableActions(), + 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), + ))); + } + + /** + * Save the action (last step) + * + * @access public + */ + public function save() + { + $this->doCreation($this->getProject(), $this->request->getValues()); + } + + /** + * Common method to save the action + * + * @access private + * @param array $project Project properties + * @param array $values Form values + */ + private function doCreation(array $project, array $values) + { + list($valid, ) = $this->actionValidator->validateCreation($values); + + if ($valid) { + if ($this->action->create($values) !== false) { + $this->flash->success(t('Your automatic action have been created successfully.')); + } else { + $this->flash->failure(t('Unable to create your automatic action.')); + } + } + + $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/ActionProject.php b/app/Controller/ActionProject.php new file mode 100644 index 00000000..e5063f73 --- /dev/null +++ b/app/Controller/ActionProject.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Duplicate automatic action from another project + * + * @package controller + * @author Frederic Guillot + */ +class ActionProject extends Base +{ + public function project() + { + $project = $this->getProject(); + $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + unset($projects[$project['id']]); + + $this->response->html($this->template->render('action_project/project', array( + 'project' => $project, + 'projects_list' => $projects, + ))); + } + + public function save() + { + $project = $this->getProject(); + $src_project_id = $this->request->getValue('src_project_id'); + + if ($this->action->duplicate($src_project_id, $project['id'])) { + $this->flash->success(t('Actions duplicated successfully.')); + } else { + $this->flash->failure(t('Unable to duplicate actions.')); + } + + $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php index 38658345..db520ebe 100644 --- a/app/Controller/Activity.php +++ b/app/Controller/Activity.php @@ -19,8 +19,7 @@ class Activity extends Base { $project = $this->getProject(); - $this->response->html($this->template->layout('activity/project', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('activity/project', array( 'events' => $this->projectActivity->getProject($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) @@ -36,7 +35,7 @@ class Activity extends Base { $task = $this->getTask(); - $this->response->html($this->taskLayout('activity/task', array( + $this->response->html($this->helper->layout->task('activity/task', array( 'title' => $task['title'], 'task' => $task, 'events' => $this->projectActivity->getTask($task['id']), diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index d203fb8e..6ce062c4 100644 --- a/app/Controller/Analytic.php +++ b/app/Controller/Analytic.php @@ -1,6 +1,7 @@ <?php namespace Kanboard\Controller; + use Kanboard\Model\Task as TaskModel; /** @@ -12,22 +13,6 @@ use Kanboard\Model\Task as TaskModel; class Analytic extends Base { /** - * Common layout for analytic views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['content_for_sublayout'] = $this->template->render($template, $params); - - return $this->template->layout('analytic/layout', $params); - } - - /** * Show average Lead and Cycle time * * @access public @@ -37,7 +22,7 @@ class Analytic extends Base $project = $this->getProject(); list($from, $to) = $this->getDates(); - $this->response->html($this->layout('analytic/lead_cycle_time', array( + $this->response->html($this->helper->layout->analytic('analytic/lead_cycle_time', array( 'values' => array( 'from' => $from, 'to' => $to, @@ -46,7 +31,7 @@ class Analytic extends Base 'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']), 'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to), 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'title' => t('Lead and Cycle time for "%s"', $project['name']), ))); } @@ -69,7 +54,7 @@ class Analytic extends Base ->setQuery($query) ->calculate(); - $this->response->html($this->layout('analytic/compare_hours', array( + $this->response->html($this->helper->layout->analytic('analytic/compare_hours', array( 'project' => $project, 'paginator' => $paginator, 'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']), @@ -86,7 +71,7 @@ class Analytic extends Base { $project = $this->getProject(); - $this->response->html($this->layout('analytic/avg_time_columns', array( + $this->response->html($this->helper->layout->analytic('analytic/avg_time_columns', array( 'project' => $project, 'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']), 'title' => t('Average time spent into each column for "%s"', $project['name']), @@ -102,7 +87,7 @@ class Analytic extends Base { $project = $this->getProject(); - $this->response->html($this->layout('analytic/tasks', array( + $this->response->html($this->helper->layout->analytic('analytic/tasks', array( 'project' => $project, 'metrics' => $this->taskDistributionAnalytic->build($project['id']), 'title' => t('Task repartition for "%s"', $project['name']), @@ -118,7 +103,7 @@ class Analytic extends Base { $project = $this->getProject(); - $this->response->html($this->layout('analytic/users', array( + $this->response->html($this->helper->layout->analytic('analytic/users', array( 'project' => $project, 'metrics' => $this->userDistributionAnalytic->build($project['id']), 'title' => t('User repartition for "%s"', $project['name']), @@ -160,7 +145,7 @@ class Analytic extends Base $display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2; - $this->response->html($this->layout($template, array( + $this->response->html($this->helper->layout->analytic($template, array( 'values' => array( 'from' => $from, 'to' => $to, @@ -169,7 +154,7 @@ class Analytic extends Base 'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(), 'project' => $project, 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'title' => t($title, $project['name']), ))); } diff --git a/app/Controller/App.php b/app/Controller/App.php index bdd7fbcf..df1d3c90 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -2,6 +2,7 @@ namespace Kanboard\Controller; +use Kanboard\Model\Project as ProjectModel; use Kanboard\Model\Subtask as SubtaskModel; /** @@ -13,35 +14,20 @@ use Kanboard\Model\Subtask as SubtaskModel; class App extends Base { /** - * Common layout for dashboard views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['content_for_sublayout'] = $this->template->render($template, $params); - - return $this->template->layout('app/layout', $params); - } - - /** * 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('app', $action, array('pagination' => 'projects', 'user_id' => $user_id)) ->setMax($max) - ->setOrder('name') + ->setOrder(ProjectModel::TABLE.'.name') ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); } @@ -53,6 +39,7 @@ class App extends Base * @param integer $user_id * @param string $action * @param integer $max + * @return \Kanboard\Core\Paginator */ private function getTaskPaginator($user_id, $action, $max) { @@ -71,6 +58,7 @@ class App extends Base * @param integer $user_id * @param string $action * @param integer $max + * @return \Kanboard\Core\Paginator */ private function getSubtaskPaginator($user_id, $action, $max) { @@ -101,7 +89,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/overview', array( + $this->response->html($this->helper->layout->dashboard('app/overview', array( 'title' => t('Dashboard'), 'project_paginator' => $this->getProjectPaginator($user['id'], 'index', 10), 'task_paginator' => $this->getTaskPaginator($user['id'], 'index', 10), @@ -119,7 +107,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/tasks', array( + $this->response->html($this->helper->layout->dashboard('app/tasks', array( 'title' => t('My tasks'), 'paginator' => $this->getTaskPaginator($user['id'], 'tasks', 50), 'user' => $user, @@ -135,7 +123,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/subtasks', array( + $this->response->html($this->helper->layout->dashboard('app/subtasks', array( 'title' => t('My subtasks'), 'paginator' => $this->getSubtaskPaginator($user['id'], 'subtasks', 50), 'user' => $user, @@ -151,7 +139,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/projects', array( + $this->response->html($this->helper->layout->dashboard('app/projects', array( 'title' => t('My projects'), 'paginator' => $this->getProjectPaginator($user['id'], 'projects', 25), 'user' => $user, @@ -167,7 +155,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/activity', array( + $this->response->html($this->helper->layout->dashboard('app/activity', array( 'title' => t('My activity stream'), 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100), 'user' => $user, @@ -181,7 +169,7 @@ class App extends Base */ public function calendar() { - $this->response->html($this->layout('app/calendar', array( + $this->response->html($this->helper->layout->dashboard('app/calendar', array( 'title' => t('My calendar'), 'user' => $this->getUser(), ))); @@ -196,7 +184,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->layout('app/notifications', array( + $this->response->html($this->helper->layout->dashboard('app/notifications', array( 'title' => t('My notifications'), 'notifications' => $this->userUnreadNotification->getAll($user['id']), 'user' => $user, diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php index 5284e126..b882a720 100644 --- a/app/Controller/Auth.php +++ b/app/Controller/Auth.php @@ -14,6 +14,8 @@ class Auth extends Base * Display the form login * * @access public + * @param array $values + * @param array $errors */ public function login(array $values = array(), array $errors = array()) { @@ -21,7 +23,7 @@ class Auth extends Base $this->response->redirect($this->helper->url->to('app', 'index')); } - $this->response->html($this->template->layout('auth/index', array( + $this->response->html($this->helper->layout->app('auth/index', array( 'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']), 'errors' => $errors, 'values' => $values, @@ -55,8 +57,12 @@ class Auth extends Base */ public function logout() { - $this->sessionManager->close(); - $this->response->redirect($this->helper->url->to('auth', 'login')); + if (! DISABLE_LOGOUT) { + $this->sessionManager->close(); + $this->response->redirect($this->helper->url->to('auth', 'login')); + } else { + $this->response->redirect($this->helper->url->to('auth', 'index')); + } } /** diff --git a/app/Controller/Base.php b/app/Controller/Base.php index efeab31e..2453be18 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -131,7 +131,7 @@ abstract class Base extends \Kanboard\Core\Base */ protected function notfound($no_layout = false) { - $this->response->html($this->template->layout('app/notfound', array( + $this->response->html($this->helper->layout->app('app/notfound', array( 'title' => t('Page not found'), 'no_layout' => $no_layout, ))); @@ -149,7 +149,7 @@ abstract class Base extends \Kanboard\Core\Base $this->response->text('Access Forbidden', 403); } - $this->response->html($this->template->layout('app/forbidden', array( + $this->response->html($this->helper->layout->app('app/forbidden', array( 'title' => t('Access Forbidden'), 'no_layout' => $no_layout, ))); @@ -180,43 +180,6 @@ abstract class Base extends \Kanboard\Core\Base } /** - * Common layout for task views - * - * @access protected - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - protected function taskLayout($template, array $params) - { - $content = $this->template->render($template, $params); - $params['task_content_for_layout'] = $content; - $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - - return $this->template->layout('task/layout', $params); - } - - /** - * Common layout for project views - * - * @access protected - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - protected function projectLayout($template, array $params, $sidebar_template = 'project/sidebar') - { - $content = $this->template->render($template, $params); - $params['project_content_for_layout'] = $content; - $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['sidebar_template'] = $sidebar_template; - - return $this->template->layout('project/layout', $params); - } - - /** * Common method to get a task for task views * * @access protected @@ -239,6 +202,36 @@ abstract class Base extends \Kanboard\Core\Base } /** + * Get Task or Project file + * + * @access protected + */ + protected function getFile() + { + $task_id = $this->request->getIntegerParam('task_id'); + $file_id = $this->request->getIntegerParam('file_id'); + $model = 'projectFile'; + + if ($task_id > 0) { + $model = 'taskFile'; + $project_id = $this->taskFinder->getProjectId($task_id); + + if ($project_id !== $this->request->getIntegerParam('project_id')) { + $this->forbidden(); + } + } + + $file = $this->$model->getById($file_id); + + if (empty($file)) { + $this->notfound(); + } + + $file['model'] = $model; + return $file; + } + + /** * Common method to get a project * * @access protected @@ -251,8 +244,7 @@ abstract class Base extends \Kanboard\Core\Base $project = $this->project->getByIdWithOwner($project_id); if (empty($project)) { - $this->flash->failure(t('Project not found.')); - $this->response->redirect($this->helper->url->to('project', 'index')); + $this->notfound(); } return $project; @@ -280,6 +272,23 @@ abstract class Base extends \Kanboard\Core\Base } /** + * Get the current subtask + * + * @access protected + * @return array + */ + protected function getSubtask() + { + $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id')); + + if (empty($subtask)) { + $this->notfound(); + } + + return $subtask; + } + + /** * Common method to get project filters * * @access protected @@ -319,9 +328,10 @@ abstract class Base extends \Kanboard\Core\Base * @param array &$project * @return string */ - protected function getProjectDescription(array &$project) { + protected function getProjectDescription(array &$project) + { if ($project['owner_id'] > 0) { - $description = t('Project owner: ').'**'.$this->template->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL; + $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL; if (! empty($project['description'])) { $description .= '***'.PHP_EOL.PHP_EOL; diff --git a/app/Controller/Board.php b/app/Controller/Board.php index f64de69a..199f1703 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -27,7 +27,7 @@ class Board extends Base } // Display the board with a specific layout - $this->response->html($this->template->layout('board/view_public', array( + $this->response->html($this->helper->layout->app('board/view_public', array( 'project' => $project, 'swimlanes' => $this->board->getBoard($project['id']), 'title' => $project['name'], @@ -49,7 +49,7 @@ class Board extends Base { $params = $this->getProjectFilters('board', 'show'); - $this->response->html($this->template->layout('board/view_private', array( + $this->response->html($this->helper->layout->app('board/view_private', array( 'categories_list' => $this->category->getList($params['project']['id'], false), 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()), diff --git a/app/Controller/BoardPopover.php b/app/Controller/BoardPopover.php index a214439a..63dab302 100644 --- a/app/Controller/BoardPopover.php +++ b/app/Controller/BoardPopover.php @@ -93,9 +93,8 @@ class BoardPopover extends Base { $task = $this->getTask(); - $this->response->html($this->template->render('file/screenshot', array( + $this->response->html($this->template->render('task_file/screenshot', array( 'task' => $task, - 'redirect' => 'board', ))); } @@ -113,7 +112,7 @@ class BoardPopover extends Base $this->response->html($this->template->render('board/popover_close_all_tasks_column', array( 'project' => $project, 'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id), - 'column' => $this->board->getColumnTitleById($column_id), + 'column' => $this->column->getColumnTitleById($column_id), 'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']), 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id), ))); @@ -130,7 +129,7 @@ class BoardPopover extends Base $values = $this->request->getValues(); $this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']); - $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->board->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); + $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->column->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); } } diff --git a/app/Controller/BoardTooltip.php b/app/Controller/BoardTooltip.php index ed58a2f2..bc07ce09 100644 --- a/app/Controller/BoardTooltip.php +++ b/app/Controller/BoardTooltip.php @@ -19,7 +19,21 @@ class BoardTooltip extends Base { $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_tasklinks', array( - 'links' => $this->taskLink->getAll($task['id']), + 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), + 'task' => $task, + ))); + } + + /** + * Get links on mouseover + * + * @access public + */ + public function externallinks() + { + $task = $this->getTask(); + $this->response->html($this->template->render('board/tooltip_external_links', array( + 'links' => $this->taskExternalLink->getAll($task['id']), 'task' => $task, ))); } @@ -48,7 +62,7 @@ class BoardTooltip extends Base $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_files', array( - 'files' => $this->file->getAll($task['id']), + 'files' => $this->taskFile->getAll($task['id']), 'task' => $task, ))); } @@ -90,7 +104,7 @@ class BoardTooltip extends Base { $task = $this->getTask(); - $this->response->html($this->template->render('task/recurring_info', array( + $this->response->html($this->template->render('task_recurrence/info', array( 'task' => $task, 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php index 67a402d3..a0a25e41 100644 --- a/app/Controller/Calendar.php +++ b/app/Controller/Calendar.php @@ -20,7 +20,7 @@ class Calendar extends Base */ public function show() { - $this->response->html($this->template->layout('calendar/show', array( + $this->response->html($this->helper->layout->app('calendar/show', array( 'check_interval' => $this->config->get('board_private_refresh_interval'), ) + $this->getProjectFilters('calendar', 'show'))); } diff --git a/app/Controller/Category.php b/app/Controller/Category.php index a0af4139..258a3b78 100644 --- a/app/Controller/Category.php +++ b/app/Controller/Category.php @@ -38,7 +38,7 @@ class Category extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('category/index', array( + $this->response->html($this->helper->layout->project('category/index', array( 'categories' => $this->category->getList($project['id'], false), 'values' => $values + array('project_id' => $project['id']), 'errors' => $errors, @@ -81,7 +81,7 @@ class Category extends Base $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category/edit', array( + $this->response->html($this->helper->layout->project('category/edit', array( 'values' => empty($values) ? $category : $values, 'errors' => $errors, 'project' => $project, @@ -123,7 +123,7 @@ class Category extends Base $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category/remove', array( + $this->response->html($this->helper->layout->project('category/remove', array( 'project' => $project, 'category' => $category, 'title' => t('Remove a category') diff --git a/app/Controller/Column.php b/app/Controller/Column.php index 1ce575d7..bbe12fe1 100644 --- a/app/Controller/Column.php +++ b/app/Controller/Column.php @@ -15,20 +15,12 @@ class Column extends Base * * @access public */ - public function index(array $values = array(), array $errors = array()) + public function index() { $project = $this->getProject(); - $columns = $this->board->getColumns($project['id']); + $columns = $this->column->getAll($project['id']); - foreach ($columns as $column) { - $values['title['.$column['id'].']'] = $column['title']; - $values['description['.$column['id'].']'] = $column['description']; - $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; - } - - $this->response->html($this->projectLayout('column/index', array( - 'errors' => $errors, - 'values' => $values + array('project_id' => $project['id']), + $this->response->html($this->helper->layout->project('column/index', array( 'columns' => $columns, 'project' => $project, 'title' => t('Edit board') @@ -36,46 +28,63 @@ class Column extends Base } /** - * Validate and add a new column + * Show form to create a new column * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $columns = $this->board->getColumnsList($project['id']); - $data = $this->request->getValues(); - $values = array(); - foreach ($columns as $column_id => $column_title) { - $values['title['.$column_id.']'] = $column_title; + if (empty($values)) { + $values = array('project_id' => $project['id']); } - list($valid, $errors) = $this->columnValidator->validateCreation($data); + $this->response->html($this->template->render('column/create', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Add a new column') + ))); + } + + /** + * Validate and add a new column + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->columnValidator->validateCreation($values); if ($valid) { - if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) { - $this->flash->success(t('Board updated successfully.')); - $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + if ($this->column->create($project['id'], $values['title'], $values['task_limit'], $values['description'])) { + $this->flash->success(t('Column created successfully.')); + return $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])), true); } else { - $this->flash->failure(t('Unable to update this board.')); + $errors['title'] = array(t('Another column with the same name exists in the project')); } } - $this->index($values, $errors); + $this->create($values, $errors); } /** * Display a form to edit a column * * @access public + * @param array $values + * @param array $errors */ public function edit(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + $column = $this->column->getById($this->request->getIntegerParam('column_id')); - $this->response->html($this->projectLayout('column/edit', array( + $this->response->html($this->helper->layout->project('column/edit', array( 'errors' => $errors, 'values' => $values ?: $column, 'project' => $project, @@ -97,7 +106,7 @@ class Column extends Base list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { - if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) { + if ($this->column->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) { $this->flash->success(t('Board updated successfully.')); $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); } else { @@ -109,22 +118,21 @@ class Column extends Base } /** - * Move a column up or down + * Move column position * * @access public */ public function move() { - $this->checkCSRFParam(); $project = $this->getProject(); - $column_id = $this->request->getIntegerParam('column_id'); - $direction = $this->request->getStringParam('direction'); + $values = $this->request->getJson(); - if ($direction === 'up' || $direction === 'down') { - $this->board->{'move'.$direction}($project['id'], $column_id); + if (! empty($values) && isset($values['column_id']) && isset($values['position'])) { + $result = $this->column->changePosition($project['id'], $values['column_id'], $values['position']); + return $this->response->json(array('result' => $result)); } - $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + $this->forbidden(); } /** @@ -136,8 +144,8 @@ class Column extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('column/remove', array( - 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + $this->response->html($this->helper->layout->project('column/remove', array( + 'column' => $this->column->getById($this->request->getIntegerParam('column_id')), 'project' => $project, 'title' => t('Remove a column from a board') ))); @@ -152,9 +160,9 @@ class Column extends Base { $project = $this->getProject(); $this->checkCSRFParam(); - $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + $column_id = $this->request->getIntegerParam('column_id'); - if (! empty($column) && $this->board->removeColumn($column['id'])) { + if ($this->column->remove($column_id)) { $this->flash->success(t('Column removed successfully.')); } else { $this->flash->failure(t('Unable to remove this column.')); diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php index a608dd1c..ff7ec305 100644 --- a/app/Controller/Comment.php +++ b/app/Controller/Comment.php @@ -21,13 +21,11 @@ class Comment extends Base $comment = $this->comment->getById($this->request->getIntegerParam('comment_id')); if (empty($comment)) { - $this->notfound(); + return $this->notfound(); } if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) { - $this->response->html($this->template->layout('comment/forbidden', array( - 'title' => t('Access Forbidden') - ))); + return $this->forbidden(); } return $comment; @@ -41,7 +39,6 @@ class Comment extends Base public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); if (empty($values)) { $values = array( @@ -50,20 +47,10 @@ class Comment extends Base ); } - if ($ajax) { - $this->response->html($this->template->render('comment/create', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'ajax' => $ajax, - ))); - } - - $this->response->html($this->taskLayout('comment/create', array( + $this->response->html($this->template->render('comment/create', array( 'values' => $values, 'errors' => $errors, 'task' => $task, - 'title' => t('Add a comment'), ))); } @@ -76,7 +63,6 @@ class Comment extends Base { $task = $this->getTask(); $values = $this->request->getValues(); - $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); list($valid, $errors) = $this->commentValidator->validateCreation($values); @@ -87,11 +73,7 @@ class Comment extends Base $this->flash->failure(t('Unable to create your comment.')); } - if ($ajax) { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments')); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true); } $this->create($values, $errors); @@ -107,7 +89,7 @@ class Comment extends Base $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment/edit', array( + $this->response->html($this->template->render('comment/edit', array( 'values' => empty($values) ? $comment : $values, 'errors' => $errors, 'comment' => $comment, @@ -124,7 +106,7 @@ class Comment extends Base public function update() { $task = $this->getTask(); - $comment = $this->getComment(); + $this->getComment(); $values = $this->request->getValues(); list($valid, $errors) = $this->commentValidator->validateModification($values); @@ -136,7 +118,7 @@ class Comment extends Base $this->flash->failure(t('Unable to update your comment.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comment-'.$comment['id'])); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false); } $this->edit($values, $errors); @@ -152,7 +134,7 @@ class Comment extends Base $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment/remove', array( + $this->response->html($this->template->render('comment/remove', array( 'comment' => $comment, 'task' => $task, 'title' => t('Remove a comment') diff --git a/app/Controller/Config.php b/app/Controller/Config.php index 4aee8553..e811f870 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -11,24 +11,6 @@ namespace Kanboard\Controller; class Config extends Base { /** - * Common layout for config views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['values'] = $this->config->getAll(); - $params['errors'] = array(); - $params['config_content_for_layout'] = $this->template->render($template, $params); - - return $this->template->layout('config/layout', $params); - } - - /** * Common method between pages * * @access private @@ -44,7 +26,12 @@ class Config extends Base $values += array('password_reset' => 0); break; case 'project': - $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0); + $values += array( + 'subtask_restriction' => 0, + 'subtask_time_tracking' => 0, + 'cfd_include_closed_tasks' => 0, + 'disable_private_project' => 0, + ); break; case 'integrations': $values += array('integration_gravatar' => 0); @@ -72,7 +59,7 @@ class Config extends Base */ public function index() { - $this->response->html($this->layout('config/about', array( + $this->response->html($this->helper->layout->config('config/about', array( 'db_size' => $this->config->getDatabaseSize(), 'title' => t('Settings').' > '.t('About'), ))); @@ -85,7 +72,7 @@ class Config extends Base */ public function plugins() { - $this->response->html($this->layout('config/plugins', array( + $this->response->html($this->helper->layout->config('config/plugins', array( 'plugins' => $this->pluginLoader->plugins, 'title' => t('Settings').' > '.t('Plugins'), ))); @@ -100,10 +87,12 @@ class Config extends Base { $this->common('application'); - $this->response->html($this->layout('config/application', array( + $this->response->html($this->helper->layout->config('config/application', array( 'languages' => $this->config->getLanguages(), 'timezones' => $this->config->getTimezones(), - 'date_formats' => $this->dateParser->getAvailableFormats(), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), + 'datetime_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateTimeFormats()), + 'time_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getTimeFormats()), 'title' => t('Settings').' > '.t('Application settings'), ))); } @@ -117,7 +106,7 @@ class Config extends Base { $this->common('project'); - $this->response->html($this->layout('config/project', array( + $this->response->html($this->helper->layout->config('config/project', array( 'colors' => $this->color->getList(), 'default_columns' => implode(', ', $this->board->getDefaultColumns()), 'title' => t('Settings').' > '.t('Project settings'), @@ -133,7 +122,7 @@ class Config extends Base { $this->common('board'); - $this->response->html($this->layout('config/board', array( + $this->response->html($this->helper->layout->config('config/board', array( 'title' => t('Settings').' > '.t('Board settings'), ))); } @@ -147,7 +136,7 @@ class Config extends Base { $this->common('calendar'); - $this->response->html($this->layout('config/calendar', array( + $this->response->html($this->helper->layout->config('config/calendar', array( 'title' => t('Settings').' > '.t('Calendar settings'), ))); } @@ -161,7 +150,7 @@ class Config extends Base { $this->common('integrations'); - $this->response->html($this->layout('config/integrations', array( + $this->response->html($this->helper->layout->config('config/integrations', array( 'title' => t('Settings').' > '.t('Integrations'), ))); } @@ -175,7 +164,7 @@ class Config extends Base { $this->common('webhook'); - $this->response->html($this->layout('config/webhook', array( + $this->response->html($this->helper->layout->config('config/webhook', array( 'title' => t('Settings').' > '.t('Webhook settings'), ))); } @@ -187,7 +176,7 @@ class Config extends Base */ public function api() { - $this->response->html($this->layout('config/api', array( + $this->response->html($this->helper->layout->config('config/api', array( 'title' => t('Settings').' > '.t('API'), ))); } diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php index 42e404f8..ecaa9834 100644 --- a/app/Controller/Currency.php +++ b/app/Controller/Currency.php @@ -11,29 +11,13 @@ namespace Kanboard\Controller; class Currency extends Base { /** - * Common layout for config views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['config_content_for_layout'] = $this->template->render($template, $params); - - return $this->template->layout('config/layout', $params); - } - - /** * Display all currency rates and form * * @access public */ public function index(array $values = array(), array $errors = array()) { - $this->response->html($this->layout('currency/index', array( + $this->response->html($this->helper->layout->config('currency/index', array( 'config_values' => array('application_currency' => $this->config->get('application_currency')), 'values' => $values, 'errors' => $errors, diff --git a/app/Controller/Customfilter.php b/app/Controller/Customfilter.php index 1b43f1d0..41da0b11 100644 --- a/app/Controller/Customfilter.php +++ b/app/Controller/Customfilter.php @@ -21,7 +21,7 @@ class Customfilter extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('custom_filter/index', array( + $this->response->html($this->helper->layout->project('custom_filter/index', array( 'values' => $values + array('project_id' => $project['id']), 'errors' => $errors, 'project' => $project, @@ -57,6 +57,23 @@ class Customfilter extends Base } /** + * Confirmation dialog before removing a custom filter + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id')); + + $this->response->html($this->helper->layout->project('custom_filter/remove', array( + 'project' => $project, + 'filter' => $filter, + 'title' => t('Remove a custom filter') + ))); + } + + /** * Remove a custom filter * * @access public @@ -90,7 +107,7 @@ class Customfilter extends Base $this->checkPermission($project, $filter); - $this->response->html($this->projectLayout('custom_filter/edit', array( + $this->response->html($this->helper->layout->project('custom_filter/edit', array( 'values' => empty($values) ? $filter : $values, 'errors' => $errors, 'project' => $project, diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php index a233b120..6f309d48 100644 --- a/app/Controller/Doc.php +++ b/app/Controller/Doc.php @@ -52,8 +52,6 @@ class Doc extends Base } } - $this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), - ))); + $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename))); } } diff --git a/app/Controller/Export.php b/app/Controller/Export.php index c39f58a1..c2ff652e 100644 --- a/app/Controller/Export.php +++ b/app/Controller/Export.php @@ -27,7 +27,7 @@ class Export extends Base $this->response->csv($data); } - $this->response->html($this->projectLayout('export/'.$action, array( + $this->response->html($this->helper->layout->project('export/'.$action, array( 'values' => array( 'controller' => 'export', 'action' => $action, @@ -37,7 +37,7 @@ class Export extends Base ), 'errors' => array(), 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'project' => $project, 'title' => $page_title, ), 'export/sidebar')); @@ -80,6 +80,6 @@ class Export extends Base */ public function transitions() { - $this->common('transition', 'export', t('Transitions'), 'transitions', t('Task transitions export')); + $this->common('transitionExport', 'export', t('Transitions'), 'transitions', t('Task transitions export')); } } diff --git a/app/Controller/File.php b/app/Controller/File.php deleted file mode 100644 index b46f7d19..00000000 --- a/app/Controller/File.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php - -namespace Kanboard\Controller; - -use Kanboard\Core\ObjectStorage\ObjectStorageException; - -/** - * File controller - * - * @package controller - * @author Frederic Guillot - */ -class File extends Base -{ - /** - * Screenshot - * - * @access public - */ - public function screenshot() - { - $task = $this->getTask(); - - if ($this->request->isPost() && $this->file->uploadScreenshot($task['project_id'], $task['id'], $this->request->getValue('screenshot')) !== false) { - $this->flash->success(t('Screenshot uploaded successfully.')); - - if ($this->request->getStringParam('redirect') === 'board') { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } - - $this->response->html($this->taskLayout('file/screenshot', array( - 'task' => $task, - 'redirect' => 'task', - ))); - } - - /** - * File upload form - * - * @access public - */ - public function create() - { - $task = $this->getTask(); - - $this->response->html($this->taskLayout('file/new', array( - 'task' => $task, - 'max_size' => ini_get('upload_max_filesize'), - ))); - } - - /** - * File upload (save files) - * - * @access public - */ - public function save() - { - $task = $this->getTask(); - - if (! $this->file->uploadFiles($task['project_id'], $task['id'], 'files')) { - $this->flash->failure(t('Unable to upload the file.')); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } - - /** - * File download - * - * @access public - */ - public function download() - { - try { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - if ($file['task_id'] != $task['id']) { - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } - - $this->response->forceDownload($file['name']); - $this->objectStorage->output($file['path']); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - } - } - - /** - * Open a file (show the content in a popover) - * - * @access public - */ - public function open() - { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - if ($file['task_id'] == $task['id']) { - $this->response->html($this->template->render('file/open', array( - 'file' => $file, - 'task' => $task, - ))); - } - } - - /** - * Display image - * - * @access public - */ - public function image() - { - try { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - if ($file['task_id'] == $task['id']) { - $this->response->contentType($this->file->getImageMimeType($file['name'])); - $this->objectStorage->output($file['path']); - } - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - } - } - - /** - * Display image thumbnails - * - * @access public - */ - public function thumbnail() - { - $this->response->contentType('image/jpeg'); - - try { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - if ($file['task_id'] == $task['id']) { - $this->objectStorage->output($this->file->getThumbnailPath($file['path'])); - } - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - - // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19 - $data = $this->objectStorage->get($file['path']); - $this->file->generateThumbnailFromData($file['path'], $data); - $this->objectStorage->output($this->file->getThumbnailPath($file['path'])); - } - } - - /** - * Remove a file - * - * @access public - */ - public function remove() - { - $this->checkCSRFParam(); - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - if ($file['task_id'] == $task['id'] && $this->file->remove($file['id'])) { - $this->flash->success(t('File removed successfully.')); - } else { - $this->flash->failure(t('Unable to remove this file.')); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } - - /** - * Confirmation dialog before removing a file - * - * @access public - */ - public function confirm() - { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); - - $this->response->html($this->taskLayout('file/remove', array( - 'task' => $task, - 'file' => $file, - ))); - } -} diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php new file mode 100644 index 00000000..bc91c3d8 --- /dev/null +++ b/app/Controller/FileViewer.php @@ -0,0 +1,116 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\ObjectStorage\ObjectStorageException; + +/** + * File Viewer Controller + * + * @package controller + * @author Frederic Guillot + */ +class FileViewer extends Base +{ + /** + * Get file content from object storage + * + * @access private + * @param array $file + * @return string + */ + private function getFileContent(array $file) + { + $content = ''; + + try { + + if ($file['is_image'] == 0) { + $content = $this->objectStorage->get($file['path']); + } + + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + + return $content; + } + + /** + * Show file content in a popover + * + * @access public + */ + public function show() + { + $file = $this->getFile(); + $type = $this->helper->file->getPreviewType($file['name']); + $params = array('file_id' => $file['id'], 'project_id' => $this->request->getIntegerParam('project_id')); + + if ($file['model'] === 'taskFile') { + $params['task_id'] = $file['task_id']; + } + + $this->response->html($this->template->render('file_viewer/show', array( + 'file' => $file, + 'params' => $params, + 'type' => $type, + 'content' => $this->getFileContent($file), + ))); + } + + /** + * Display image + * + * @access public + */ + public function image() + { + try { + $file = $this->getFile(); + $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); + $this->objectStorage->output($file['path']); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + } + + /** + * Display image thumbnail + * + * @access public + */ + public function thumbnail() + { + $this->response->contentType('image/jpeg'); + + try { + $file = $this->getFile(); + $model = $file['model']; + $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + + // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19 + $data = $this->objectStorage->get($file['path']); + $this->$model->generateThumbnailFromData($file['path'], $data); + $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + } + } + + /** + * File download + * + * @access public + */ + public function download() + { + try { + $file = $this->getFile(); + $this->response->forceDownload($file['name']); + $this->objectStorage->output($file['path']); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + } +} diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php index ac0e6fad..9ffa277f 100644 --- a/app/Controller/Gantt.php +++ b/app/Controller/Gantt.php @@ -23,10 +23,9 @@ class Gantt extends Base $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); } - $this->response->html($this->template->layout('gantt/projects', array( + $this->response->html($this->helper->layout->app('gantt/projects', array( 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(), 'title' => t('Gantt chart for all projects'), - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), ))); } @@ -65,7 +64,7 @@ class Gantt extends Base $filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position'); } - $this->response->html($this->template->layout('gantt/project', $params + array( + $this->response->html($this->helper->layout->app('gantt/project', $params + array( 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), 'sorting' => $sorting, 'tasks' => $filter->format(), @@ -102,19 +101,23 @@ class Gantt extends Base { $project = $this->getProject(); + $values = $values + array( + 'project_id' => $project['id'], + 'column_id' => $this->column->getFirstColumnId($project['id']), + 'position' => 1 + ); + + $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); + $values = $this->hook->merge('controller:gantt:task:form:default', $values, array('default_values' => $values)); + $this->response->html($this->template->render('gantt/task_creation', array( + 'project' => $project, 'errors' => $errors, - 'values' => $values + array( - 'project_id' => $project['id'], - 'column_id' => $this->board->getFirstColumn($project['id']), - 'position' => 1 - ), + 'values' => $values, 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), 'title' => $project['name'].' > '.t('New task') ))); } diff --git a/app/Controller/Group.php b/app/Controller/Group.php index e952c0e5..fa47f428 100644 --- a/app/Controller/Group.php +++ b/app/Controller/Group.php @@ -24,8 +24,7 @@ class Group extends Base ->setQuery($this->group->getQuery()) ->calculate(); - $this->response->html($this->template->layout('group/index', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('group/index', array( 'title' => t('Groups').' ('.$paginator->getTotal().')', 'paginator' => $paginator, ))); @@ -48,8 +47,7 @@ class Group extends Base ->setQuery($this->groupMember->getQuery($group_id)) ->calculate(); - $this->response->html($this->template->layout('group/users', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('group/users', array( 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')', 'paginator' => $paginator, 'group' => $group, @@ -63,8 +61,7 @@ class Group extends Base */ public function create(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('group/create', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('group/create', array( 'errors' => $errors, 'values' => $values, 'title' => t('New group') @@ -104,8 +101,7 @@ class Group extends Base $values = $this->group->getById($this->request->getIntegerParam('group_id')); } - $this->response->html($this->template->layout('group/edit', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('group/edit', array( 'errors' => $errors, 'values' => $values, 'title' => t('Edit group') @@ -148,8 +144,7 @@ class Group extends Base $values['group_id'] = $group_id; } - $this->response->html($this->template->layout('group/associate', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('group/associate', array( 'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)), 'group' => $group, 'errors' => $errors, @@ -191,7 +186,7 @@ class Group extends Base $group = $this->group->getById($group_id); $user = $this->user->getById($user_id); - $this->response->html($this->template->layout('group/dissociate', array( + $this->response->html($this->helper->layout->app('group/dissociate', array( 'group' => $group, 'user' => $user, 'title' => t('Remove user from group "%s"', $group['name']), @@ -228,7 +223,7 @@ class Group extends Base $group_id = $this->request->getIntegerParam('group_id'); $group = $this->group->getById($group_id); - $this->response->html($this->template->layout('group/remove', array( + $this->response->html($this->helper->layout->app('group/remove', array( 'group' => $group, 'title' => t('Remove group'), ))); diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php index f8e9e25f..f1ea6d8f 100644 --- a/app/Controller/Ical.php +++ b/app/Controller/Ical.php @@ -3,6 +3,7 @@ namespace Kanboard\Controller; use Kanboard\Model\TaskFilter; +use Kanboard\Model\Task as TaskModel; use Eluceo\iCal\Component\Calendar as iCalendar; /** @@ -31,6 +32,7 @@ class Ical extends Base // Common filter $filter = $this->taskFilterICalendarFormatter ->create() + ->filterByStatus(TaskModel::STATUS_OPEN) ->filterByOwner($user['id']); // Calendar properties @@ -60,6 +62,7 @@ class Ical extends Base // Common filter $filter = $this->taskFilterICalendarFormatter ->create() + ->filterByStatus(TaskModel::STATUS_OPEN) ->filterByProject($project['id']); // Calendar properties diff --git a/app/Controller/Link.php b/app/Controller/Link.php index d52d1f91..ec7ab1af 100644 --- a/app/Controller/Link.php +++ b/app/Controller/Link.php @@ -12,22 +12,6 @@ namespace Kanboard\Controller; class Link extends Base { /** - * Common layout for config views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['config_content_for_layout'] = $this->template->render($template, $params); - - return $this->template->layout('config/layout', $params); - } - - /** * Get the current link * * @access private @@ -51,7 +35,7 @@ class Link extends Base */ public function index(array $values = array(), array $errors = array()) { - $this->response->html($this->layout('link/index', array( + $this->response->html($this->helper->layout->config('link/index', array( 'links' => $this->link->getMergedList(), 'values' => $values, 'errors' => $errors, @@ -91,7 +75,7 @@ class Link extends Base $link = $this->getLink(); $link['label'] = t($link['label']); - $this->response->html($this->layout('link/edit', array( + $this->response->html($this->helper->layout->config('link/edit', array( 'values' => $values ?: $link, 'errors' => $errors, 'labels' => $this->link->getList($link['id']), @@ -131,7 +115,7 @@ class Link extends Base { $link = $this->getLink(); - $this->response->html($this->layout('link/remove', array( + $this->response->html($this->helper->layout->config('link/remove', array( 'link' => $link, 'title' => t('Remove a link') ))); diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php index b9c851f5..c784dd50 100644 --- a/app/Controller/Listing.php +++ b/app/Controller/Listing.php @@ -30,8 +30,11 @@ class Listing extends Base ->setQuery($query) ->calculate(); - $this->response->html($this->template->layout('listing/show', $params + array( + $this->response->html($this->helper->layout->app('listing/show', $params + array( 'paginator' => $paginator, + 'categories_list' => $this->category->getList($params['project']['id'], false), + 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), + 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()), ))); } } diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php index ed901def..452faecd 100644 --- a/app/Controller/Oauth.php +++ b/app/Controller/Oauth.php @@ -11,36 +11,6 @@ namespace Kanboard\Controller; class Oauth extends Base { /** - * Link or authenticate a Google account - * - * @access public - */ - public function google() - { - $this->step1('Google'); - } - - /** - * Link or authenticate a Github account - * - * @access public - */ - public function github() - { - $this->step1('Github'); - } - - /** - * Link or authenticate a Gitlab account - * - * @access public - */ - public function gitlab() - { - $this->step1('Gitlab'); - } - - /** * Unlink external account * * @access public @@ -65,7 +35,7 @@ class Oauth extends Base * @access private * @param string $provider */ - private function step1($provider) + protected function step1($provider) { $code = $this->request->getStringParam('code'); @@ -79,11 +49,11 @@ class Oauth extends Base /** * Link or authenticate the user * - * @access private + * @access protected * @param string $provider * @param string $code */ - private function step2($provider, $code) + protected function step2($provider, $code) { $this->authenticationManager->getProvider($provider)->setCode($code); @@ -97,10 +67,10 @@ class Oauth extends Base /** * Link the account * - * @access private + * @access protected * @param string $provider */ - private function link($provider) + protected function link($provider) { $authProvider = $this->authenticationManager->getProvider($provider); @@ -117,15 +87,15 @@ class Oauth extends Base /** * Authenticate the account * - * @access private + * @access protected * @param string $provider */ - private function authenticate($provider) + protected function authenticate($provider) { if ($this->authenticationManager->oauthAuthentication($provider)) { $this->response->redirect($this->helper->url->to('app', 'index')); } else { - $this->response->html($this->template->layout('auth/index', array( + $this->response->html($this->helper->layout->app('auth/index', array( 'errors' => array('login' => t('External authentication failed')), 'values' => array(), 'no_layout' => true, diff --git a/app/Controller/PasswordReset.php b/app/Controller/PasswordReset.php index 23567c9c..f6a0eb8e 100644 --- a/app/Controller/PasswordReset.php +++ b/app/Controller/PasswordReset.php @@ -17,7 +17,7 @@ class PasswordReset extends Base { $this->checkActivation(); - $this->response->html($this->template->layout('password_reset/create', array( + $this->response->html($this->helper->layout->app('password_reset/create', array( 'errors' => $errors, 'values' => $values, 'no_layout' => true, @@ -53,7 +53,7 @@ class PasswordReset extends Base $user_id = $this->passwordReset->getUserIdByToken($token); if ($user_id !== false) { - $this->response->html($this->template->layout('password_reset/change', array( + $this->response->html($this->helper->layout->app('password_reset/change', array( 'token' => $token, 'errors' => $errors, 'values' => $values, diff --git a/app/Controller/Project.php b/app/Controller/Project.php index ffd62b09..cdfbd94a 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -32,8 +32,7 @@ class Project extends Base ->setQuery($this->project->getQueryColumnStats($project_ids)) ->calculate(); - $this->response->html($this->template->layout('project/index', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->response->html($this->helper->layout->app('project/index', array( 'paginator' => $paginator, 'nb_projects' => $nb_projects, 'title' => t('Projects').' ('.$nb_projects.')' @@ -49,7 +48,7 @@ class Project extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('project/show', array( + $this->response->html($this->helper->layout->project('project/show', array( 'project' => $project, 'stats' => $this->project->getTaskStats($project['id']), 'title' => $project['name'], @@ -78,7 +77,7 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'share', array('project_id' => $project['id']))); } - $this->response->html($this->projectLayout('project/share', array( + $this->response->html($this->helper->layout->project('project/share', array( 'project' => $project, 'title' => t('Public access'), ))); @@ -99,7 +98,7 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'integrations', array('project_id' => $project['id']))); } - $this->response->html($this->projectLayout('project/integrations', array( + $this->response->html($this->helper->layout->project('project/integrations', array( 'project' => $project, 'title' => t('Integrations'), 'webhook_token' => $this->config->get('webhook_token'), @@ -124,7 +123,7 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'notifications', array('project_id' => $project['id']))); } - $this->response->html($this->projectLayout('project/notifications', array( + $this->response->html($this->helper->layout->project('project/notifications', array( 'notifications' => $this->projectNotification->readSettings($project['id']), 'types' => $this->projectNotificationType->getTypes(), 'project' => $project, @@ -153,7 +152,7 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'index')); } - $this->response->html($this->projectLayout('project/remove', array( + $this->response->html($this->helper->layout->project('project/remove', array( 'project' => $project, 'title' => t('Remove project') ))); @@ -171,17 +170,18 @@ class Project extends Base $project = $this->getProject(); if ($this->request->getStringParam('duplicate') === 'yes') { - $values = array_keys($this->request->getValues()); - if ($this->projectDuplication->duplicate($project['id'], $values) !== false) { + $project_id = $this->projectDuplication->duplicate($project['id'], array_keys($this->request->getValues()), $this->userSession->getId()); + + if ($project_id !== false) { $this->flash->success(t('Project cloned successfully.')); } else { $this->flash->failure(t('Unable to clone this project.')); } - $this->response->redirect($this->helper->url->to('project', 'index')); + $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id))); } - $this->response->html($this->projectLayout('project/duplicate', array( + $this->response->html($this->helper->layout->project('project/duplicate', array( 'project' => $project, 'title' => t('Clone this project') ))); @@ -208,7 +208,7 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id']))); } - $this->response->html($this->projectLayout('project/disable', array( + $this->response->html($this->helper->layout->project('project/disable', array( 'project' => $project, 'title' => t('Project activation') ))); @@ -235,62 +235,9 @@ class Project extends Base $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id']))); } - $this->response->html($this->projectLayout('project/enable', array( + $this->response->html($this->helper->layout->project('project/enable', array( 'project' => $project, 'title' => t('Project activation') ))); } - - /** - * Display a form to create a new project - * - * @access public - */ - public function create(array $values = array(), array $errors = array()) - { - $is_private = isset($values['is_private']) && $values['is_private'] == 1; - - $this->response->html($this->template->layout('project/new', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), - 'values' => $values, - 'errors' => $errors, - 'is_private' => $is_private, - 'title' => $is_private ? t('New private project') : t('New project'), - ))); - } - - /** - * Display a form to create a private project - * - * @access public - */ - public function createPrivate(array $values = array(), array $errors = array()) - { - $values['is_private'] = 1; - $this->create($values, $errors); - } - - /** - * Validate and save a new project - * - * @access public - */ - public function save() - { - $values = $this->request->getValues(); - list($valid, $errors) = $this->projectValidator->validateCreation($values); - - if ($valid) { - $project_id = $this->project->create($values, $this->userSession->getId(), true); - - if ($project_id > 0) { - $this->flash->success(t('Your project have been created successfully.')); - $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id))); - } - - $this->flash->failure(t('Unable to create your project.')); - } - - $this->create($values, $errors); - } } diff --git a/app/Controller/ProjectCreation.php b/app/Controller/ProjectCreation.php new file mode 100644 index 00000000..88f41fcd --- /dev/null +++ b/app/Controller/ProjectCreation.php @@ -0,0 +1,125 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Project Creation Controller + * + * @package controller + * @author Frederic Guillot + */ +class ProjectCreation extends Base +{ + /** + * Display a form to create a new project + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $is_private = isset($values['is_private']) && $values['is_private'] == 1; + $projects_list = array(0 => t('Do not duplicate anything')) + $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + + $this->response->html($this->helper->layout->app('project_creation/create', array( + 'values' => $values, + 'errors' => $errors, + 'is_private' => $is_private, + 'projects_list' => $projects_list, + 'title' => $is_private ? t('New private project') : t('New project'), + ))); + } + + /** + * Display a form to create a private project + * + * @access public + */ + public function createPrivate(array $values = array(), array $errors = array()) + { + $values['is_private'] = 1; + $this->create($values, $errors); + } + + /** + * Validate and save a new project + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->projectValidator->validateCreation($values); + + if ($valid) { + $project_id = $this->createOrDuplicate($values); + + if ($project_id > 0) { + $this->flash->success(t('Your project have been created successfully.')); + return $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id))); + } + + $this->flash->failure(t('Unable to create your project.')); + } + + $this->create($values, $errors); + } + + /** + * Create or duplicate a project + * + * @access private + * @param array $values + * @return boolean|integer + */ + private function createOrDuplicate(array $values) + { + if (empty($values['src_project_id'])) { + return $this->createNewProject($values); + } + + return $this->duplicateNewProject($values); + } + + /** + * Save a new project + * + * @access private + * @param array $values + * @return boolean|integer + */ + private function createNewProject(array $values) + { + $project = array( + 'name' => $values['name'], + 'is_private' => $values['is_private'], + ); + + return $this->project->create($project, $this->userSession->getId(), true); + } + + /** + * Creatte from another project + * + * @access private + * @param array $values + * @return boolean|integer + */ + private function duplicateNewProject(array $values) + { + $selection = array(); + + foreach ($this->projectDuplication->getOptionalSelection() as $item) { + if (isset($values[$item]) && $values[$item] == 1) { + $selection[] = $item; + } + } + + return $this->projectDuplication->duplicate( + $values['src_project_id'], + $selection, + $this->userSession->getId(), + $values['name'], + $values['is_private'] == 1 + ); + } +} diff --git a/app/Controller/ProjectEdit.php b/app/Controller/ProjectEdit.php index 0dfc7de3..f4a3a7cb 100644 --- a/app/Controller/ProjectEdit.php +++ b/app/Controller/ProjectEdit.php @@ -89,11 +89,11 @@ class ProjectEdit extends Base { if ($redirect === 'edit') { if (isset($values['is_private'])) { - if (! $this->helper->user->hasProjectAccess('project', 'create', $project['id'])) { + if (! $this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) { unset($values['is_private']); } } elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) { - if ($this->helper->user->hasProjectAccess('project', 'create', $project['id'])) { + if ($this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) { $values += array('is_private' => 0); } } @@ -114,7 +114,7 @@ class ProjectEdit extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout($template, array( + $this->response->html($this->helper->layout->project($template, array( 'owners' => $this->projectUserRole->getAssignableUsersList($project['id'], true), 'values' => empty($values) ? $project : $values, 'errors' => $errors, diff --git a/app/Controller/ProjectFile.php b/app/Controller/ProjectFile.php new file mode 100644 index 00000000..96764a92 --- /dev/null +++ b/app/Controller/ProjectFile.php @@ -0,0 +1,79 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Project File Controller + * + * @package controller + * @author Frederic Guillot + */ +class ProjectFile extends Base +{ + /** + * File upload form + * + * @access public + */ + public function create() + { + $project = $this->getProject(); + + $this->response->html($this->template->render('project_file/create', array( + 'project' => $project, + 'max_size' => $this->helper->text->phpToBytes(ini_get('upload_max_filesize')), + ))); + } + + /** + * Save uploaded files + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + if (! $this->projectFile->uploadFiles($project['id'], $this->request->getFileInfo('files'))) { + $this->flash->failure(t('Unable to upload the file.')); + } + + $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id'])), true); + } + + /** + * Remove a file + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $file = $this->projectFile->getById($this->request->getIntegerParam('file_id')); + + if ($this->projectFile->remove($file['id'])) { + $this->flash->success(t('File removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this file.')); + } + + $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id']))); + } + + /** + * Confirmation dialog before removing a file + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $file = $this->projectFile->getById($this->request->getIntegerParam('file_id')); + + $this->response->html($this->template->render('project_file/remove', array( + 'project' => $project, + 'file' => $file, + ))); + } +} diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php new file mode 100644 index 00000000..b0687ed3 --- /dev/null +++ b/app/Controller/ProjectOverview.php @@ -0,0 +1,29 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Project Overview Controller + * + * @package controller + * @author Frederic Guillot + */ +class ProjectOverview extends Base +{ + /** + * Show project overview + */ + public function show() + { + $params = $this->getProjectFilters('ProjectOverview', 'show'); + $params['users'] = $this->projectUserRole->getAllUsersGroupedByRole($params['project']['id']); + $params['roles'] = $this->role->getProjectRoles(); + $params['events'] = $this->projectActivity->getProject($params['project']['id'], 10); + $params['images'] = $this->projectFile->getAllImages($params['project']['id']); + $params['files'] = $this->projectFile->getAllDocuments($params['project']['id']); + + $this->project->getColumnStats($params['project']); + + $this->response->html($this->helper->layout->app('project_overview/show', $params)); + } +} diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php index 4434d017..800da02f 100644 --- a/app/Controller/ProjectPermission.php +++ b/app/Controller/ProjectPermission.php @@ -13,6 +13,24 @@ use Kanboard\Core\Security\Role; class ProjectPermission extends Base { /** + * Permissions are only available for team projects + * + * @access protected + * @param integer $project_id Default project id + * @return array + */ + protected function getProject($project_id = 0) + { + $project = parent::getProject($project_id); + + if ($project['is_private'] == 1) { + $this->forbidden(); + } + + return $project; + } + + /** * Show all permissions * * @access public @@ -25,7 +43,7 @@ class ProjectPermission extends Base $values['role'] = Role::PROJECT_MEMBER; } - $this->response->html($this->projectLayout('project_permission/index', array( + $this->response->html($this->helper->layout->project('project_permission/index', array( 'project' => $project, 'users' => $this->projectUserRole->getUsers($project['id']), 'groups' => $this->projectGroupRole->getGroups($project['id']), @@ -62,6 +80,7 @@ class ProjectPermission extends Base */ public function addUser() { + $project = $this->getProject(); $values = $this->request->getValues(); if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) { @@ -70,7 +89,7 @@ class ProjectPermission extends Base $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id']))); + $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); } /** @@ -81,19 +100,16 @@ class ProjectPermission extends Base public function removeUser() { $this->checkCSRFParam(); + $project = $this->getProject(); + $user_id = $this->request->getIntegerParam('user_id'); - $values = array( - 'project_id' => $this->request->getIntegerParam('project_id'), - 'user_id' => $this->request->getIntegerParam('user_id'), - ); - - if ($this->projectUserRole->removeUser($values['project_id'], $values['user_id'])) { + if ($this->projectUserRole->removeUser($project['id'], $user_id)) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id']))); + $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); } /** @@ -103,10 +119,10 @@ class ProjectPermission extends Base */ public function changeUserRole() { - $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->getProject(); $values = $this->request->getJson(); - if (! empty($project_id) && ! empty($values) && $this->projectUserRole->changeUserRole($project_id, $values['id'], $values['role'])) { + if (! empty($project) && ! empty($values) && $this->projectUserRole->changeUserRole($project['id'], $values['id'], $values['role'])) { $this->response->json(array('status' => 'ok')); } else { $this->response->json(array('status' => 'error')); @@ -120,19 +136,20 @@ class ProjectPermission extends Base */ public function addGroup() { + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['group_id']) && ! empty($values['external_id'])) { $values['group_id'] = $this->group->create($values['name'], $values['external_id']); } - if ($this->projectGroupRole->addGroup($values['project_id'], $values['group_id'], $values['role'])) { + if ($this->projectGroupRole->addGroup($project['id'], $values['group_id'], $values['role'])) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id']))); + $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); } /** @@ -143,19 +160,16 @@ class ProjectPermission extends Base public function removeGroup() { $this->checkCSRFParam(); + $project = $this->getProject(); + $group_id = $this->request->getIntegerParam('group_id'); - $values = array( - 'project_id' => $this->request->getIntegerParam('project_id'), - 'group_id' => $this->request->getIntegerParam('group_id'), - ); - - if ($this->projectGroupRole->removeGroup($values['project_id'], $values['group_id'])) { + if ($this->projectGroupRole->removeGroup($project['id'], $group_id)) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id']))); + $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); } /** @@ -165,10 +179,10 @@ class ProjectPermission extends Base */ public function changeGroupRole() { - $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->getProject(); $values = $this->request->getJson(); - if (! empty($project_id) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project_id, $values['id'], $values['role'])) { + if (! empty($project) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project['id'], $values['id'], $values['role'])) { $this->response->json(array('status' => 'ok')); } else { $this->response->json(array('status' => 'error')); diff --git a/app/Controller/Projectuser.php b/app/Controller/Projectuser.php index 78b93ab6..a6d4fe4e 100644 --- a/app/Controller/Projectuser.php +++ b/app/Controller/Projectuser.php @@ -14,23 +14,6 @@ use Kanboard\Core\Security\Role; */ class Projectuser extends Base { - /** - * Common layout for users overview views - * - * @access private - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - private function layout($template, array $params) - { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - $params['content_for_sublayout'] = $this->template->render($template, $params); - $params['filter'] = array('user_id' => $params['user_id']); - - return $this->template->layout('project_user/layout', $params); - } - private function common() { $user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID); @@ -41,7 +24,7 @@ class Projectuser extends Base $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); } - return array($user_id, $project_ids, $this->user->getList(true)); + return array($user_id, $project_ids, $this->user->getActiveUsersList(true)); } private function role($role, $action, $title, $title_user) @@ -50,7 +33,7 @@ class Projectuser extends Base $query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats')); - if ($user_id !== UserModel::EVERYBODY_ID) { + if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) { $query->eq(UserModel::TABLE.'.id', $user_id); $title = t($title_user, $users[$user_id]); } @@ -62,7 +45,7 @@ class Projectuser extends Base ->setQuery($query) ->calculate(); - $this->response->html($this->layout('project_user/roles', array( + $this->response->html($this->helper->layout->projectUser('project_user/roles', array( 'paginator' => $paginator, 'title' => $title, 'user_id' => $user_id, @@ -76,7 +59,7 @@ class Projectuser extends Base $query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active); - if ($user_id !== UserModel::EVERYBODY_ID) { + if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) { $query->eq(TaskModel::TABLE.'.owner_id', $user_id); $title = t($title_user, $users[$user_id]); } @@ -88,7 +71,7 @@ class Projectuser extends Base ->setQuery($query) ->calculate(); - $this->response->html($this->layout('project_user/tasks', array( + $this->response->html($this->helper->layout->projectUser('project_user/tasks', array( 'paginator' => $paginator, 'title' => $title, 'user_id' => $user_id, diff --git a/app/Controller/Search.php b/app/Controller/Search.php index 390210c0..9b9b9e65 100644 --- a/app/Controller/Search.php +++ b/app/Controller/Search.php @@ -36,8 +36,7 @@ class Search extends Base $nb_tasks = $paginator->getTotal(); } - $this->response->html($this->template->layout('search/index', array( - 'board_selector' => $projects, + $this->response->html($this->helper->layout->app('search/index', array( 'values' => array( 'search' => $search, 'controller' => 'search', diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php index caaaa85e..bc2108f9 100644 --- a/app/Controller/Subtask.php +++ b/app/Controller/Subtask.php @@ -2,8 +2,6 @@ namespace Kanboard\Controller; -use Kanboard\Model\Subtask as SubtaskModel; - /** * Subtask controller * @@ -13,20 +11,20 @@ use Kanboard\Model\Subtask as SubtaskModel; class Subtask extends Base { /** - * Get the current subtask - * - * @access private - * @return array + * Show list of subtasks */ - private function getSubtask() + public function show() { - $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id')); - - if (empty($subtask)) { - $this->notfound(); - } + $task = $this->getTask(); - return $subtask; + $this->response->html($this->helper->layout->task('subtask/show', array( + 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), + 'task' => $task, + 'project' => $this->getProject(), + 'subtasks' => $this->subtask->getAll($task['id']), + 'editable' => true, + 'show_title' => true, + ))); } /** @@ -45,7 +43,7 @@ class Subtask extends Base ); } - $this->response->html($this->taskLayout('subtask/create', array( + $this->response->html($this->template->render('subtask/create', array( 'values' => $values, 'errors' => $errors, 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), @@ -73,10 +71,10 @@ class Subtask extends Base } if (isset($values['another_subtask']) && $values['another_subtask'] == 1) { - $this->response->redirect($this->helper->url->to('subtask', 'create', array('project_id' => $task['project_id'], 'task_id' => $task['id'], 'another_subtask' => 1))); + return $this->create(array('project_id' => $task['project_id'], 'task_id' => $task['id'], 'another_subtask' => 1)); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks')); + return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'), true); } $this->create($values, $errors); @@ -92,7 +90,7 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubTask(); - $this->response->html($this->taskLayout('subtask/edit', array( + $this->response->html($this->template->render('subtask/edit', array( 'values' => empty($values) ? $subtask : $values, 'errors' => $errors, 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), @@ -122,7 +120,7 @@ class Subtask extends Base $this->flash->failure(t('Unable to update your sub-task.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks')); + return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } $this->edit($values, $errors); @@ -138,7 +136,7 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - $this->response->html($this->taskLayout('subtask/remove', array( + $this->response->html($this->template->render('subtask/remove', array( 'subtask' => $subtask, 'task' => $task, ))); @@ -161,97 +159,7 @@ class Subtask extends Base $this->flash->failure(t('Unable to remove this sub-task.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks')); - } - - /** - * Change status to the next status: Toto -> In Progress -> Done - * - * @access public - */ - public function toggleStatus() - { - $task = $this->getTask(); - $subtask = $this->getSubtask(); - $redirect = $this->request->getStringParam('redirect', 'task'); - - $this->subtask->toggleStatus($subtask['id']); - - if ($redirect === 'board') { - $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); - - $this->response->html($this->template->render('board/tooltip_subtasks', array( - 'subtasks' => $this->subtask->getAll($task['id']), - 'task' => $task, - ))); - } - - $this->toggleRedirect($task, $redirect); - } - - /** - * Handle subtask restriction (popover) - * - * @access public - */ - public function subtaskRestriction() - { - $task = $this->getTask(); - $subtask = $this->getSubtask(); - - $this->response->html($this->template->render('subtask/restriction_change_status', array( - 'status_list' => array( - SubtaskModel::STATUS_TODO => t('Todo'), - SubtaskModel::STATUS_DONE => t('Done'), - ), - 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()), - 'subtask' => $subtask, - 'task' => $task, - 'redirect' => $this->request->getStringParam('redirect'), - ))); - } - - /** - * Change status of the in progress subtask and the other subtask - * - * @access public - */ - public function changeRestrictionStatus() - { - $task = $this->getTask(); - $subtask = $this->getSubtask(); - $values = $this->request->getValues(); - - // Change status of the previous in progress subtask - $this->subtask->update(array( - 'id' => $values['id'], - 'status' => $values['status'], - )); - - // Set the current subtask to in pogress - $this->subtask->update(array( - 'id' => $subtask['id'], - 'status' => SubtaskModel::STATUS_INPROGRESS, - )); - - $this->toggleRedirect($task, $values['redirect']); - } - - /** - * Redirect to the right page - * - * @access private - */ - private function toggleRedirect(array $task, $redirect) - { - switch ($redirect) { - case 'board': - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - case 'dashboard': - $this->response->redirect($this->helper->url->to('app', 'index')); - default: - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'subtasks')); - } + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } /** @@ -261,14 +169,15 @@ class Subtask extends Base */ public function movePosition() { - $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); $task_id = $this->request->getIntegerParam('task_id'); - $subtask_id = $this->request->getIntegerParam('subtask_id'); - $direction = $this->request->getStringParam('direction'); - $method = $direction === 'up' ? 'moveUp' : 'moveDown'; + $values = $this->request->getJson(); + + if (! empty($values) && $this->helper->user->hasProjectAccess('Subtask', 'movePosition', $project_id)) { + $result = $this->subtask->changePosition($task_id, $values['subtask_id'], $values['position']); + return $this->response->json(array('result' => $result)); + } - $this->subtask->$method($task_id, $subtask_id); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id), 'subtasks')); + $this->forbidden(); } } diff --git a/app/Controller/SubtaskRestriction.php b/app/Controller/SubtaskRestriction.php new file mode 100644 index 00000000..56024867 --- /dev/null +++ b/app/Controller/SubtaskRestriction.php @@ -0,0 +1,61 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Model\Subtask as SubtaskModel; + +/** + * Subtask Restriction + * + * @package controller + * @author Frederic Guillot + */ +class SubtaskRestriction extends Base +{ + /** + * Show popup + * + * @access public + */ + public function popover() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + $this->response->html($this->template->render('subtask_restriction/popover', array( + 'status_list' => array( + SubtaskModel::STATUS_TODO => t('Todo'), + SubtaskModel::STATUS_DONE => t('Done'), + ), + 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()), + 'subtask' => $subtask, + 'task' => $task, + ))); + } + + /** + * Change status of the in progress subtask and the other subtask + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + $values = $this->request->getValues(); + + // Change status of the previous "in progress" subtask + $this->subtask->update(array( + 'id' => $values['id'], + 'status' => $values['status'], + )); + + // Set the current subtask to "in progress" + $this->subtask->update(array( + 'id' => $subtask['id'], + 'status' => SubtaskModel::STATUS_INPROGRESS, + )); + + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + } +} diff --git a/app/Controller/SubtaskStatus.php b/app/Controller/SubtaskStatus.php new file mode 100644 index 00000000..4fb82fc0 --- /dev/null +++ b/app/Controller/SubtaskStatus.php @@ -0,0 +1,72 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Subtask Status + * + * @package controller + * @author Frederic Guillot + */ +class SubtaskStatus extends Base +{ + /** + * Change status to the next status: Toto -> In Progress -> Done + * + * @access public + */ + public function change() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + $status = $this->subtask->toggleStatus($subtask['id']); + + if ($this->request->getIntegerParam('refresh-table') === 0) { + $subtask['status'] = $status; + $html = $this->helper->subtask->toggleStatus($subtask, $task['project_id']); + } else { + $html = $this->renderTable($task); + } + + $this->response->html($html); + } + + /** + * Start/stop timer for subtasks + * + * @access public + */ + public function timer() + { + $task = $this->getTask(); + $subtask_id = $this->request->getIntegerParam('subtask_id'); + $timer = $this->request->getStringParam('timer'); + + if ($timer === 'start') { + $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId()); + } elseif ($timer === 'stop') { + $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId()); + $this->subtaskTimeTracking->updateTaskTimeTracking($task['id']); + } + + $this->response->html($this->renderTable($task)); + } + + /** + * Render table + * + * @access private + * @param array $task + * @return string + */ + private function renderTable(array $task) + { + return $this->template->render('subtask/table', array( + 'task' => $task, + 'subtasks' => $this->subtask->getAll($task['id']), + 'editable' => true, + 'redirect' => 'task', + )); + } +} diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php index 66410888..8270a16f 100644 --- a/app/Controller/Swimlane.php +++ b/app/Controller/Swimlane.php @@ -36,18 +36,32 @@ class Swimlane extends Base * * @access public */ - public function index(array $values = array(), array $errors = array()) + public function index() { $project = $this->getProject(); - $this->response->html($this->projectLayout('swimlane/index', array( + $this->response->html($this->helper->layout->project('swimlane/index', array( 'default_swimlane' => $this->swimlane->getDefault($project['id']), 'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), 'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Create a new swimlane + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->template->render('swimlane/create', array( 'values' => $values + array('project_id' => $project['id']), 'errors' => $errors, 'project' => $project, - 'title' => t('Swimlanes') ))); } @@ -67,11 +81,28 @@ class Swimlane extends Base $this->flash->success(t('Your swimlane have been created successfully.')); $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); } else { - $this->flash->failure(t('Unable to create your swimlane.')); + $errors = array('name' => array(t('Another swimlane with the same name exists in the project'))); } } - $this->index($values, $errors); + $this->create($values, $errors); + } + + /** + * Edit default swimlane (display the form) + * + * @access public + */ + public function editDefault(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $swimlane = $this->swimlane->getDefault($project['id']); + + $this->response->html($this->helper->layout->project('swimlane/edit_default', array( + 'values' => empty($values) ? $swimlane : $values, + 'errors' => $errors, + 'project' => $project, + ))); } /** @@ -79,23 +110,23 @@ class Swimlane extends Base * * @access public */ - public function change() + public function updateDefault() { $project = $this->getProject(); $values = $this->request->getValues() + array('show_default_swimlane' => 0); - list($valid, ) = $this->swimlaneValidator->validateDefaultModification($values); + list($valid, $errors) = $this->swimlaneValidator->validateDefaultModification($values); if ($valid) { if ($this->swimlane->updateDefault($values)) { $this->flash->success(t('The default swimlane have been updated successfully.')); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])), true); } else { $this->flash->failure(t('Unable to update this swimlane.')); } } - $this->index(); + $this->editDefault($values, $errors); } /** @@ -108,11 +139,10 @@ class Swimlane extends Base $project = $this->getProject(); $swimlane = $this->getSwimlane($project['id']); - $this->response->html($this->projectLayout('swimlane/edit', array( + $this->response->html($this->helper->layout->project('swimlane/edit', array( 'values' => empty($values) ? $swimlane : $values, 'errors' => $errors, 'project' => $project, - 'title' => t('Swimlanes') ))); } @@ -133,7 +163,7 @@ class Swimlane extends Base $this->flash->success(t('Swimlane updated successfully.')); $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); } else { - $this->flash->failure(t('Unable to update this swimlane.')); + $errors = array('name' => array(t('Another swimlane with the same name exists in the project'))); } } @@ -150,10 +180,9 @@ class Swimlane extends Base $project = $this->getProject(); $swimlane = $this->getSwimlane($project['id']); - $this->response->html($this->projectLayout('swimlane/remove', array( + $this->response->html($this->helper->layout->project('swimlane/remove', array( 'project' => $project, 'swimlane' => $swimlane, - 'title' => t('Remove a swimlane') ))); } @@ -198,6 +227,25 @@ class Swimlane extends Base } /** + * Disable default swimlane + * + * @access public + */ + public function disableDefault() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + + if ($this->swimlane->disableDefault($project['id'])) { + $this->flash->success(t('Swimlane updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this swimlane.')); + } + + $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + } + + /** * Enable a swimlane * * @access public @@ -218,32 +266,39 @@ class Swimlane extends Base } /** - * Move up a swimlane + * Enable default swimlane * * @access public */ - public function moveup() + public function enableDefault() { $this->checkCSRFParam(); $project = $this->getProject(); - $swimlane_id = $this->request->getIntegerParam('swimlane_id'); - $this->swimlane->moveUp($project['id'], $swimlane_id); + if ($this->swimlane->enableDefault($project['id'])) { + $this->flash->success(t('Swimlane updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this swimlane.')); + } + $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); } /** - * Move down a swimlane + * Move swimlane position * * @access public */ - public function movedown() + public function move() { - $this->checkCSRFParam(); $project = $this->getProject(); - $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + $values = $this->request->getJson(); - $this->swimlane->moveDown($project['id'], $swimlane_id); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + if (! empty($values) && isset($values['swimlane_id']) && isset($values['position'])) { + $result = $this->swimlane->changePosition($project['id'], $values['swimlane_id'], $values['position']); + return $this->response->json(array('result' => $result)); + } + + $this->forbidden(); } } diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 1811dcb7..aeaf5792 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Core\DateParser; + /** * Task controller * @@ -21,22 +23,26 @@ class Task extends Base // Token verification if (empty($project)) { - $this->forbidden(true); + return $this->forbidden(true); } $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); if (empty($task)) { - $this->notfound(true); + return $this->notfound(true); + } + + if ($task['project_id'] != $project['id']) { + return $this->forbidden(true); } - $this->response->html($this->template->layout('task/public', array( + $this->response->html($this->helper->layout->app('task/public', array( 'project' => $project, 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $this->subtask->getAll($task['id']), 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'task' => $task, - 'columns_list' => $this->board->getColumnsList($task['project_id']), + 'columns_list' => $this->column->getList($task['project_id']), 'colors_list' => $this->color->getList(), 'title' => $task['title'], 'no_layout' => true, @@ -62,27 +68,19 @@ class Task extends Base 'time_spent' => $task['time_spent'] ?: '', ); - $this->dateParser->format($values, array('date_started'), 'Y-m-d H:i'); + $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT)); - $this->response->html($this->taskLayout('task/show', array( + $this->response->html($this->helper->layout->task('task/show', array( 'project' => $this->project->getById($task['project_id']), - 'files' => $this->file->getAllDocuments($task['id']), - 'images' => $this->file->getAllImages($task['id']), + 'files' => $this->taskFile->getAllDocuments($task['id']), + 'images' => $this->taskFile->getAllImages($task['id']), 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()), 'subtasks' => $subtasks, 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'task' => $task, 'values' => $values, 'link_label_list' => $this->link->getList(0, false), - 'columns_list' => $this->board->getColumnsList($task['project_id']), - 'colors_list' => $this->color->getList(), 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'title' => $task['project_name'].' > '.$task['title'], - 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), ))); } @@ -95,8 +93,7 @@ class Task extends Base { $task = $this->getTask(); - $this->response->html($this->taskLayout('task/analytics', array( - 'title' => $task['title'], + $this->response->html($this->helper->layout->task('task/analytics', array( 'task' => $task, 'lead_time' => $this->taskAnalytic->getLeadTime($task), 'cycle_time' => $this->taskAnalytic->getCycleTime($task), @@ -114,14 +111,14 @@ class Task extends Base $task = $this->getTask(); $subtask_paginator = $this->paginator - ->setUrl('task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) + ->setUrl('task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) ->setMax(15) ->setOrder('start') ->setDirection('DESC') ->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id'])) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); - $this->response->html($this->taskLayout('task/time_tracking_details', array( + $this->response->html($this->helper->layout->task('task/time_tracking_details', array( 'task' => $task, 'subtask_paginator' => $subtask_paginator, ))); @@ -136,7 +133,7 @@ class Task extends Base { $task = $this->getTask(); - $this->response->html($this->taskLayout('task/transitions', array( + $this->response->html($this->helper->layout->task('task/transitions', array( 'task' => $task, 'transitions' => $this->transition->getAllByTask($task['id']), ))); @@ -167,7 +164,7 @@ class Task extends Base $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); } - $this->response->html($this->taskLayout('task/remove', array( + $this->response->html($this->template->render('task/remove', array( 'task' => $task, ))); } diff --git a/app/Controller/TaskExternalLink.php b/app/Controller/TaskExternalLink.php new file mode 100644 index 00000000..72015686 --- /dev/null +++ b/app/Controller/TaskExternalLink.php @@ -0,0 +1,185 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound; + +/** + * Task External Link Controller + * + * @package controller + * @author Frederic Guillot + */ +class TaskExternalLink extends Base +{ + /** + * Creation form + * + * @access public + */ + public function show() + { + $task = $this->getTask(); + + $this->response->html($this->helper->layout->task('task_external_link/show', array( + 'links' => $this->taskExternalLink->getAll($task['id']), + 'task' => $task, + 'title' => t('List of external links'), + ))); + } + + /** + * First creation form + * + * @access public + */ + public function find(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + + $this->response->html($this->template->render('task_external_link/find', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'types' => $this->externalLinkManager->getTypes(), + ))); + } + + /** + * Second creation form + * + * @access public + */ + public function create() + { + try { + + $task = $this->getTask(); + $values = $this->request->getValues(); + + $provider = $this->externalLinkManager->setUserInput($values)->find(); + $link = $provider->getLink(); + + $this->response->html($this->template->render('task_external_link/create', array( + 'values' => array( + 'title' => $link->getTitle(), + 'url' => $link->getUrl(), + 'link_type' => $provider->getType(), + ), + 'dependencies' => $provider->getDependencies(), + 'errors' => array(), + 'task' => $task, + ))); + + } catch (ExternalLinkProviderNotFound $e) { + $errors = array('text' => array(t('Unable to fetch link information.'))); + $this->find($values, $errors); + } + } + + /** + * Save link + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + list($valid, $errors) = $this->externalLinkValidator->validateCreation($values); + + if ($valid && $this->taskExternalLink->create($values)) { + $this->flash->success(t('Link added successfully.')); + return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + } + + $this->edit($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $link_id = $this->request->getIntegerParam('link_id'); + + if ($link_id > 0) { + $values = $this->taskExternalLink->getById($link_id); + } + + if (empty($values)) { + return $this->notfound(); + } + + $provider = $this->externalLinkManager->getProvider($values['link_type']); + + $this->response->html($this->template->render('task_external_link/edit', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'dependencies' => $provider->getDependencies(), + ))); + } + + /** + * Update link + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + list($valid, $errors) = $this->externalLinkValidator->validateModification($values); + + if ($valid && $this->taskExternalLink->update($values)) { + $this->flash->success(t('Link updated successfully.')); + return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $link_id = $this->request->getIntegerParam('link_id'); + $link = $this->taskExternalLink->getById($link_id); + + if (empty($link)) { + return $this->notfound(); + } + + $this->response->html($this->template->render('task_external_link/remove', array( + 'link' => $link, + 'task' => $task, + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + + if ($this->taskExternalLink->remove($this->request->getIntegerParam('link_id'))) { + $this->flash->success(t('Link removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } +} diff --git a/app/Controller/TaskFile.php b/app/Controller/TaskFile.php new file mode 100644 index 00000000..2b0152a7 --- /dev/null +++ b/app/Controller/TaskFile.php @@ -0,0 +1,98 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Task File Controller + * + * @package controller + * @author Frederic Guillot + */ +class TaskFile extends Base +{ + /** + * Screenshot + * + * @access public + */ + public function screenshot() + { + $task = $this->getTask(); + + if ($this->request->isPost() && $this->taskFile->uploadScreenshot($task['id'], $this->request->getValue('screenshot')) !== false) { + $this->flash->success(t('Screenshot uploaded successfully.')); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + } + + $this->response->html($this->template->render('task_file/screenshot', array( + 'task' => $task, + ))); + } + + /** + * File upload form + * + * @access public + */ + public function create() + { + $task = $this->getTask(); + + $this->response->html($this->template->render('task_file/create', array( + 'task' => $task, + 'max_size' => $this->helper->text->phpToBytes(ini_get('upload_max_filesize')), + ))); + } + + /** + * File upload (save files) + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + + if (! $this->taskFile->uploadFiles($task['id'], $this->request->getFileInfo('files'))) { + $this->flash->failure(t('Unable to upload the file.')); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + } + + /** + * Remove a file + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + $file = $this->taskFile->getById($this->request->getIntegerParam('file_id')); + + if ($file['task_id'] == $task['id'] && $this->taskFile->remove($file['id'])) { + $this->flash->success(t('File removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this file.')); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } + + /** + * Confirmation dialog before removing a file + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $file = $this->taskFile->getById($this->request->getIntegerParam('file_id')); + + $this->response->html($this->template->render('task_file/remove', array( + 'task' => $task, + 'file' => $file, + ))); + } +} diff --git a/app/Controller/TaskImport.php b/app/Controller/TaskImport.php index f09c14ce..460c608c 100644 --- a/app/Controller/TaskImport.php +++ b/app/Controller/TaskImport.php @@ -20,7 +20,7 @@ class TaskImport extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('task_import/step1', array( + $this->response->html($this->helper->layout->project('task_import/step1', array( 'project' => $project, 'values' => $values, 'errors' => $errors, diff --git a/app/Controller/TaskRecurrence.php b/app/Controller/TaskRecurrence.php new file mode 100644 index 00000000..569ef8d9 --- /dev/null +++ b/app/Controller/TaskRecurrence.php @@ -0,0 +1,61 @@ +<?php + +namespace Kanboard\Controller; + +/** + * Task Recurrence controller + * + * @package controller + * @author Frederic Guillot + */ +class TaskRecurrence extends Base +{ + /** + * Edit recurrence form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + + if (empty($values)) { + $values = $task; + } + + $this->response->html($this->template->render('task_recurrence/edit', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); + } + + /** + * Update recurrence form + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values); + + if ($valid) { + if ($this->taskModification->update($values)) { + $this->flash->success(t('Task updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your task.')); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + } + + $this->edit($values, $errors); + } +} diff --git a/app/Controller/Taskcreation.php b/app/Controller/Taskcreation.php index 49ccea7f..1d8a0e29 100644 --- a/app/Controller/Taskcreation.php +++ b/app/Controller/Taskcreation.php @@ -18,31 +18,29 @@ class Taskcreation extends Base public function create(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $method = $this->request->isAjax() ? 'render' : 'layout'; $swimlanes_list = $this->swimlane->getList($project['id'], false, true); if (empty($values)) { $values = array( 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)), 'column_id' => $this->request->getIntegerParam('column_id'), - 'color_id' => $this->request->getStringParam('color_id', $this->color->getDefaultColor()), - 'owner_id' => $this->request->getIntegerParam('owner_id'), - 'another_task' => $this->request->getIntegerParam('another_task'), + 'color_id' => $this->color->getDefaultColor(), + 'owner_id' => $this->userSession->getId(), ); + + $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); + $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values)); } - $this->response->html($this->template->$method('task_creation/form', array( + $this->response->html($this->template->render('task_creation/form', array( 'project' => $project, - 'ajax' => $this->request->isAjax(), 'errors' => $errors, 'values' => $values + array('project_id' => $project['id']), - 'columns_list' => $this->board->getColumnsList($project['id']), + 'columns_list' => $this->column->getList($project['id']), 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'swimlanes_list' => $swimlanes_list, - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), 'title' => $project['name'].' > '.t('New task') ))); } @@ -61,25 +59,26 @@ class Taskcreation extends Base if ($valid && $this->taskCreation->create($values)) { $this->flash->success(t('Task created successfully.')); - $this->afterSave($project, $values); - } else { - $this->flash->failure(t('Unable to create your task.')); + return $this->afterSave($project, $values); } + $this->flash->failure(t('Unable to create your task.')); $this->create($values, $errors); } private function afterSave(array $project, array &$values) { if (isset($values['another_task']) && $values['another_task'] == 1) { - unset($values['title']); - unset($values['description']); - - if (! $this->request->isAjax()) { - $this->response->redirect($this->helper->url->to('taskcreation', 'create', $values)); - } - } else { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); + return $this->create(array( + 'owner_id' => $values['owner_id'], + 'color_id' => $values['color_id'], + 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0, + 'column_id' => $values['column_id'], + 'swimlane_id' => isset($values['swimlane_id']) ? $values['swimlane_id'] : 0, + 'another_task' => 1, + )); } + + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); } } diff --git a/app/Controller/Taskduplication.php b/app/Controller/Taskduplication.php index ae8bfcbc..8fca930d 100644 --- a/app/Controller/Taskduplication.php +++ b/app/Controller/Taskduplication.php @@ -2,8 +2,6 @@ namespace Kanboard\Controller; -use Kanboard\Model\Project as ProjectModel; - /** * Task Duplication controller * @@ -30,11 +28,11 @@ class Taskduplication extends Base $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task_id))); } else { $this->flash->failure(t('Unable to create this task.')); - $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } } - $this->response->html($this->taskLayout('task_duplication/duplicate', array( + $this->response->html($this->template->render('task_duplication/duplicate', array( 'task' => $task, ))); } @@ -109,7 +107,7 @@ class Taskduplication extends Base private function chooseDestination(array $task, $template) { $values = array(); - $projects_list = $this->projectUserRole->getProjectsByUser($this->userSession->getId(), array(ProjectModel::ACTIVE)); + $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -117,7 +115,7 @@ class Taskduplication extends Base $dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list)); $swimlanes_list = $this->swimlane->getList($dst_project_id, false, true); - $columns_list = $this->board->getColumnsList($dst_project_id); + $columns_list = $this->column->getList($dst_project_id); $categories_list = $this->category->getList($dst_project_id); $users_list = $this->projectUserRole->getAssignableUsersList($dst_project_id); @@ -130,7 +128,7 @@ class Taskduplication extends Base $users_list = array(); } - $this->response->html($this->taskLayout($template, array( + $this->response->html($this->template->render($template, array( 'values' => $values, 'task' => $task, 'projects_list' => $projects_list, diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php index a81d3ee5..b051caad 100644 --- a/app/Controller/Tasklink.php +++ b/app/Controller/Tasklink.php @@ -22,13 +22,34 @@ class Tasklink extends Base $link = $this->taskLink->getById($this->request->getIntegerParam('link_id')); if (empty($link)) { - $this->notfound(); + return $this->notfound(); } return $link; } /** + * Show links + * + * @access public + */ + public function show() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); + + $this->response->html($this->helper->layout->task('tasklink/show', array( + 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), + 'task' => $task, + 'project' => $project, + 'link_label_list' => $this->link->getList(0, false), + 'editable' => true, + 'is_public' => false, + 'show_title' => true, + ))); + } + + /** * Creation form * * @access public @@ -36,20 +57,8 @@ class Tasklink extends Base public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - - if ($ajax && empty($errors)) { - $this->response->html($this->template->render('tasklink/create', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'labels' => $this->link->getList(0, false), - 'title' => t('Add a new link'), - 'ajax' => $ajax, - ))); - } - $this->response->html($this->taskLayout('tasklink/create', array( + $this->response->html($this->template->render('tasklink/create', array( 'values' => $values, 'errors' => $errors, 'task' => $task, @@ -67,19 +76,13 @@ class Tasklink extends Base { $task = $this->getTask(); $values = $this->request->getValues(); - $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); list($valid, $errors) = $this->taskLinkValidator->validateCreation($values); if ($valid) { if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { $this->flash->success(t('Link added successfully.')); - - if ($ajax) { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links', true); } $errors = array('title' => array(t('The exact same link already exists'))); @@ -105,7 +108,7 @@ class Tasklink extends Base $values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title']; } - $this->response->html($this->taskLayout('tasklink/edit', array( + $this->response->html($this->template->render('tasklink/edit', array( 'values' => $values, 'errors' => $errors, 'task_link' => $task_link, @@ -130,7 +133,7 @@ class Tasklink extends Base if ($valid) { if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { $this->flash->success(t('Link updated successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); } $this->flash->failure(t('Unable to update your link.')); @@ -149,7 +152,7 @@ class Tasklink extends Base $task = $this->getTask(); $link = $this->getTaskLink(); - $this->response->html($this->taskLayout('tasklink/remove', array( + $this->response->html($this->template->render('tasklink/remove', array( 'link' => $link, 'task' => $task, ))); diff --git a/app/Controller/Taskmodification.php b/app/Controller/Taskmodification.php index 2c97970b..6b945f37 100644 --- a/app/Controller/Taskmodification.php +++ b/app/Controller/Taskmodification.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Core\DateParser; + /** * Task Modification controller * @@ -23,71 +25,48 @@ class Taskmodification extends Base } /** - * Update time tracking information + * Edit description form * * @access public */ - public function time() + public function description(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $values = $this->request->getValues(); - - list($valid, ) = $this->taskValidator->validateTimeModification($values); - if ($valid && $this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); + if (empty($values)) { + $values = array('id' => $task['id'], 'description' => $task['description']); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + $this->response->html($this->template->render('task_modification/edit_description', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + ))); } /** - * Edit description form + * Update description * * @access public */ - public function description() + public function updateDescription() { $task = $this->getTask(); - $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - - list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values); + $values = $this->request->getValues(); - if ($valid) { - if ($this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); - } + list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values); - if ($ajax) { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } else { - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } + if ($valid) { + if ($this->taskModification->update($values)) { + $this->flash->success(t('Task updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your task.')); } - } else { - $values = $task; - $errors = array(); - } - $params = array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'ajax' => $ajax, - ); - - if ($ajax) { - $this->response->html($this->template->render('task_modification/edit_description', $params)); - } else { - $this->response->html($this->taskLayout('task_modification/edit_description', $params)); + return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } + + $this->description($values, $errors); } /** @@ -99,15 +78,17 @@ class Taskmodification extends Base { $task = $this->getTask(); $project = $this->project->getById($task['project_id']); - $ajax = $this->request->isAjax(); if (empty($values)) { $values = $task; + $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); + $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values)); } - $this->dateParser->format($values, array('date_due')); + $values = $this->dateParser->format($values, array('date_due'), $this->config->get('application_date_format', DateParser::DATE_FORMAT)); + $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT)); - $params = array( + $this->response->html($this->template->render('task_modification/edit_task', array( 'project' => $project, 'values' => $values, 'errors' => $errors, @@ -115,18 +96,7 @@ class Taskmodification extends Base 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($task['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'ajax' => $ajax, - ); - - if ($ajax) { - $html = $this->template->render('task_modification/edit_task', $params); - } else { - $html = $this->taskLayout('task_modification/edit_task', $params); - } - - $this->response->html($html); + ))); } /** @@ -143,56 +113,10 @@ class Taskmodification extends Base if ($valid && $this->taskModification->update($values)) { $this->flash->success(t('Task updated successfully.')); - - if ($this->request->isAjax()) { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } else { - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } + return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } else { $this->flash->failure(t('Unable to update your task.')); $this->edit($values, $errors); } } - - /** - * Edit recurrence form - * - * @access public - */ - public function recurrence() - { - $task = $this->getTask(); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - - list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values); - - if ($valid) { - if ($this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } - } else { - $values = $task; - $errors = array(); - } - - $params = array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), - 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), - ); - - $this->response->html($this->taskLayout('task_modification/edit_recurrence', $params)); - } } diff --git a/app/Controller/Taskstatus.php b/app/Controller/Taskstatus.php index b03baebf..a67459c9 100644 --- a/app/Controller/Taskstatus.php +++ b/app/Controller/Taskstatus.php @@ -17,9 +17,7 @@ class Taskstatus extends Base */ public function close() { - $task = $this->getTask(); - $this->changeStatus($task, 'close', t('Task closed successfully.'), t('Unable to close this task.')); - $this->renderTemplate($task, 'task_status/close'); + $this->changeStatus('close', 'task_status/close', t('Task closed successfully.'), t('Unable to close this task.')); } /** @@ -29,13 +27,22 @@ class Taskstatus extends Base */ public function open() { - $task = $this->getTask(); - $this->changeStatus($task, 'open', t('Task opened successfully.'), t('Unable to open this task.')); - $this->renderTemplate($task, 'task_status/open'); + $this->changeStatus('open', 'task_status/open', t('Task opened successfully.'), t('Unable to open this task.')); } - private function changeStatus(array $task, $method, $success_message, $failure_message) + /** + * Common method to change status + * + * @access private + * @param string $method + * @param string $template + * @param string $success_message + * @param string $failure_message + */ + private function changeStatus($method, $template, $success_message, $failure_message) { + $task = $this->getTask(); + if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); @@ -45,28 +52,11 @@ class Taskstatus extends Base $this->flash->failure($failure_message); } - if ($this->request->getStringParam('redirect') === 'board') { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } - } - - private function renderTemplate(array $task, $template) - { - $redirect = $this->request->getStringParam('redirect'); - - if ($this->request->isAjax()) { - $this->response->html($this->template->render($template, array( - 'task' => $task, - 'redirect' => $redirect, - ))); + return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } - $this->response->html($this->taskLayout($template, array( + $this->response->html($this->template->render($template, array( 'task' => $task, - 'redirect' => $redirect, ))); } } diff --git a/app/Controller/Timer.php b/app/Controller/Timer.php deleted file mode 100644 index 0267fcdd..00000000 --- a/app/Controller/Timer.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace Kanboard\Controller; - -/** - * Time Tracking controller - * - * @package controller - * @author Frederic Guillot - */ -class Timer extends Base -{ - /** - * Start/stop timer for subtasks - * - * @access public - */ - public function subtask() - { - $project_id = $this->request->getIntegerParam('project_id'); - $task_id = $this->request->getIntegerParam('task_id'); - $subtask_id = $this->request->getIntegerParam('subtask_id'); - $timer = $this->request->getStringParam('timer'); - - if ($timer === 'start') { - $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId()); - } elseif ($timer === 'stop') { - $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId()); - $this->subtaskTimeTracking->updateTaskTimeTracking($task_id); - } - - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks'); - } -} diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php index 8dbfcf66..10292261 100644 --- a/app/Controller/Twofactor.php +++ b/app/Controller/Twofactor.php @@ -33,7 +33,7 @@ class Twofactor extends User $this->checkCurrentUser($user); unset($this->sessionStorage->twoFactorSecret); - $this->response->html($this->layout('twofactor/index', array( + $this->response->html($this->helper->layout->user('twofactor/index', array( 'user' => $user, 'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(), ))); @@ -60,7 +60,7 @@ class Twofactor extends User $provider->setSecret($this->sessionStorage->twoFactorSecret); } - $this->response->html($this->layout('twofactor/show', array( + $this->response->html($this->helper->layout->user('twofactor/show', array( 'user' => $user, 'secret' => $this->sessionStorage->twoFactorSecret, 'qrcode_url' => $provider->getQrCodeUrl($label), @@ -165,7 +165,7 @@ class Twofactor extends User $this->sessionStorage->twoFactorBeforeCodeCalled = true; } - $this->response->html($this->template->layout('twofactor/check', array( + $this->response->html($this->helper->layout->app('twofactor/check', array( 'title' => t('Check two factor authentication code'), ))); } @@ -191,7 +191,7 @@ class Twofactor extends User $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); } - $this->response->html($this->layout('twofactor/disable', array( + $this->response->html($this->helper->layout->user('twofactor/disable', array( 'user' => $user, ))); } diff --git a/app/Controller/User.php b/app/Controller/User.php index 97e01553..f7d7d2e0 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -15,27 +15,6 @@ use Kanboard\Core\Security\Role; class User extends Base { /** - * Common layout for user views - * - * @access protected - * @param string $template Template name - * @param array $params Template parameters - * @return string - */ - protected function layout($template, array $params) - { - $content = $this->template->render($template, $params); - $params['user_content_for_layout'] = $content; - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - - if (isset($params['user'])) { - $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; - } - - return $this->template->layout('user/layout', $params); - } - - /** * List all users * * @access public @@ -50,11 +29,11 @@ class User extends Base ->calculate(); $this->response->html( - $this->template->layout('user/index', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->helper->layout->app('user/index', array( 'title' => t('Users').' ('.$paginator->getTotal().')', 'paginator' => $paginator, - ))); + ) + )); } /** @@ -71,8 +50,7 @@ class User extends Base } $this->response->html( - $this->template->layout('user/profile', array( - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + $this->helper->layout->app('user/profile', array( 'title' => $user['name'] ?: $user['username'], 'user' => $user, ) @@ -88,11 +66,10 @@ class User extends Base { $is_remote = $this->request->getIntegerParam('remote') == 1 || (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1); - $this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array( + $this->response->html($this->helper->layout->app($is_remote ? 'user/create_remote' : 'user/create_local', array( 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), 'roles' => $this->role->getApplicationRoles(), - 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), 'projects' => $this->project->getList(), 'errors' => $errors, 'values' => $values + array('role' => Role::APP_USER), @@ -142,7 +119,7 @@ class User extends Base public function show() { $user = $this->getUser(); - $this->response->html($this->layout('user/show', array( + $this->response->html($this->helper->layout->user('user/show', array( 'user' => $user, 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), @@ -166,7 +143,7 @@ class User extends Base ->setQuery($this->subtaskTimeTracking->getUserQuery($user['id'])) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); - $this->response->html($this->layout('user/timesheet', array( + $this->response->html($this->helper->layout->user('user/timesheet', array( 'subtask_paginator' => $subtask_paginator, 'user' => $user, ))); @@ -180,7 +157,7 @@ class User extends Base public function passwordReset() { $user = $this->getUser(); - $this->response->html($this->layout('user/password_reset', array( + $this->response->html($this->helper->layout->user('user/password_reset', array( 'tokens' => $this->passwordReset->getAll($user['id']), 'user' => $user, ))); @@ -194,7 +171,7 @@ class User extends Base public function last() { $user = $this->getUser(); - $this->response->html($this->layout('user/last', array( + $this->response->html($this->helper->layout->user('user/last', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); @@ -208,7 +185,7 @@ class User extends Base public function sessions() { $user = $this->getUser(); - $this->response->html($this->layout('user/sessions', array( + $this->response->html($this->helper->layout->user('user/sessions', array( 'sessions' => $this->rememberMeSession->getAll($user['id']), 'user' => $user, ))); @@ -243,7 +220,7 @@ class User extends Base $this->response->redirect($this->helper->url->to('user', 'notifications', array('user_id' => $user['id']))); } - $this->response->html($this->layout('user/notifications', array( + $this->response->html($this->helper->layout->user('user/notifications', array( 'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)), 'notifications' => $this->userNotification->readSettings($user['id']), 'types' => $this->userNotificationType->getTypes(), @@ -268,7 +245,7 @@ class User extends Base $this->response->redirect($this->helper->url->to('user', 'integrations', array('user_id' => $user['id']))); } - $this->response->html($this->layout('user/integrations', array( + $this->response->html($this->helper->layout->user('user/integrations', array( 'user' => $user, 'values' => $this->userMetadata->getall($user['id']), ))); @@ -282,7 +259,7 @@ class User extends Base public function external() { $user = $this->getUser(); - $this->response->html($this->layout('user/external', array( + $this->response->html($this->helper->layout->user('user/external', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); @@ -310,7 +287,7 @@ class User extends Base $this->response->redirect($this->helper->url->to('user', 'share', array('user_id' => $user['id']))); } - $this->response->html($this->layout('user/share', array( + $this->response->html($this->helper->layout->user('user/share', array( 'user' => $user, 'title' => t('Public access'), ))); @@ -342,7 +319,7 @@ class User extends Base } } - $this->response->html($this->layout('user/password', array( + $this->response->html($this->helper->layout->user('user/password', array( 'values' => $values, 'errors' => $errors, 'user' => $user, @@ -384,7 +361,7 @@ class User extends Base } } - $this->response->html($this->layout('user/edit', array( + $this->response->html($this->helper->layout->user('user/edit', array( 'values' => $values, 'errors' => $errors, 'user' => $user, @@ -422,36 +399,10 @@ class User extends Base } } - $this->response->html($this->layout('user/authentication', array( + $this->response->html($this->helper->layout->user('user/authentication', array( 'values' => $values, 'errors' => $errors, 'user' => $user, ))); } - - /** - * Remove a user - * - * @access public - */ - public function remove() - { - $user = $this->getUser(); - - if ($this->request->getStringParam('confirmation') === 'yes') { - $this->checkCSRFParam(); - - if ($this->user->remove($user['id'])) { - $this->flash->success(t('User removed successfully.')); - } else { - $this->flash->failure(t('Unable to remove this user.')); - } - - $this->response->redirect($this->helper->url->to('user', 'index')); - } - - $this->response->html($this->layout('user/remove', array( - 'user' => $user, - ))); - } } diff --git a/app/Controller/UserImport.php b/app/Controller/UserImport.php index cbc5aa14..debd69e5 100644 --- a/app/Controller/UserImport.php +++ b/app/Controller/UserImport.php @@ -18,7 +18,7 @@ class UserImport extends Base */ public function step1(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('user_import/step1', array( + $this->response->html($this->helper->layout->app('user_import/step1', array( 'values' => $values, 'errors' => $errors, 'max_size' => ini_get('upload_max_filesize'), diff --git a/app/Controller/UserStatus.php b/app/Controller/UserStatus.php new file mode 100644 index 00000000..b8ee5c91 --- /dev/null +++ b/app/Controller/UserStatus.php @@ -0,0 +1,111 @@ +<?php + +namespace Kanboard\Controller; + +/** + * User Status Controller + * + * @package controller + * @author Frederic Guillot + */ +class UserStatus extends Base +{ + /** + * Confirm remove a user + * + * @access public + */ + public function confirmRemove() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('user_status/remove', array( + 'user' => $user, + ))); + } + + /** + * Remove a user + * + * @access public + */ + public function remove() + { + $user = $this->getUser(); + $this->checkCSRFParam(); + + if ($this->user->remove($user['id'])) { + $this->flash->success(t('User removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this user.')); + } + + $this->response->redirect($this->helper->url->to('user', 'index')); + } + + /** + * Confirm enable a user + * + * @access public + */ + public function confirmEnable() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('user_status/enable', array( + 'user' => $user, + ))); + } + + /** + * Enable a user + * + * @access public + */ + public function enable() + { + $user = $this->getUser(); + $this->checkCSRFParam(); + + if ($this->user->enable($user['id'])) { + $this->flash->success(t('User activated successfully.')); + } else { + $this->flash->failure(t('Unable to enable this user.')); + } + + $this->response->redirect($this->helper->url->to('user', 'index')); + } + + /** + * Confirm disable a user + * + * @access public + */ + public function confirmDisable() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('user_status/disable', array( + 'user' => $user, + ))); + } + + /** + * Disable a user + * + * @access public + */ + public function disable() + { + $user = $this->getUser(); + $this->checkCSRFParam(); + + if ($this->user->disable($user['id'])) { + $this->flash->success(t('User disabled successfully.')); + } else { + $this->flash->failure(t('Unable to disable this user.')); + } + + $this->response->redirect($this->helper->url->to('user', 'index')); + } +} diff --git a/app/Core/Base.php b/app/Core/Base.php index 2821e5ae..e53f299a 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -16,6 +16,7 @@ use Pimple\Container; * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic * @property \Kanboard\Core\Action\ActionManager $actionManager + * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager * @property \Kanboard\Core\Cache\MemoryCache $memoryCache * @property \Kanboard\Core\Event\EventManager $eventManager * @property \Kanboard\Core\Group\GroupManager $groupManager @@ -30,7 +31,6 @@ use Pimple\Container; * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage * @property \Kanboard\Core\Plugin\Hook $hook * @property \Kanboard\Core\Plugin\Loader $pluginLoader - * @property \Kanboard\Core\Security\AccessMap $projectAccessMap * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap * @property \Kanboard\Core\Security\AccessMap $projectAccessMap @@ -62,21 +62,21 @@ use Pimple\Container; * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color + * @property \Kanboard\Model\Column $column * @property \Kanboard\Model\Comment $comment * @property \Kanboard\Model\Config $config * @property \Kanboard\Model\Currency $currency * @property \Kanboard\Model\CustomFilter $customFilter - * @property \Kanboard\Model\File $file + * @property \Kanboard\Model\TaskFile $taskFile + * @property \Kanboard\Model\ProjectFile $projectFile * @property \Kanboard\Model\Group $group * @property \Kanboard\Model\GroupMember $groupMember * @property \Kanboard\Model\LastLogin $lastLogin * @property \Kanboard\Model\Link $link * @property \Kanboard\Model\Notification $notification - * @property \Kanboard\Model\OverdueNotification $overdueNotification * @property \Kanboard\Model\PasswordReset $passwordReset * @property \Kanboard\Model\Project $project * @property \Kanboard\Model\ProjectActivity $projectActivity - * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic * @property \Kanboard\Model\ProjectDuplication $projectDuplication * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats @@ -89,15 +89,13 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType * @property \Kanboard\Model\RememberMeSession $rememberMeSession * @property \Kanboard\Model\Subtask $subtask - * @property \Kanboard\Model\SubtaskExport $subtaskExport * @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking * @property \Kanboard\Model\Swimlane $swimlane * @property \Kanboard\Model\Task $task * @property \Kanboard\Model\TaskAnalytic $taskAnalytic * @property \Kanboard\Model\TaskCreation $taskCreation * @property \Kanboard\Model\TaskDuplication $taskDuplication - * @property \Kanboard\Model\TaskExport $taskExport - * @property \Kanboard\Model\TaskImport $taskImport + * @property \Kanboard\Model\TaskExternalLink $taskExternalLink * @property \Kanboard\Model\TaskFinder $taskFinder * @property \Kanboard\Model\TaskFilter $taskFilter * @property \Kanboard\Model\TaskLink $taskLink @@ -108,7 +106,6 @@ use Pimple\Container; * @property \Kanboard\Model\TaskMetadata $taskMetadata * @property \Kanboard\Model\Transition $transition * @property \Kanboard\Model\User $user - * @property \Kanboard\Model\UserImport $userImport * @property \Kanboard\Model\UserLocking $userLocking * @property \Kanboard\Model\UserMention $userMention * @property \Kanboard\Model\UserNotification $userNotification @@ -116,12 +113,10 @@ use Pimple\Container; * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification * @property \Kanboard\Model\UserMetadata $userMetadata - * @property \Kanboard\Model\Webhook $webhook * @property \Kanboard\Validator\ActionValidator $actionValidator * @property \Kanboard\Validator\AuthValidator $authValidator * @property \Kanboard\Validator\ColumnValidator $columnValidator * @property \Kanboard\Validator\CategoryValidator $categoryValidator - * @property \Kanboard\Validator\ColumnValidator $columnValidator * @property \Kanboard\Validator\CommentValidator $commentValidator * @property \Kanboard\Validator\CurrencyValidator $currencyValidator * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator @@ -132,8 +127,14 @@ use Pimple\Container; * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator + * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator * @property \Kanboard\Validator\TaskValidator $taskValidator * @property \Kanboard\Validator\UserValidator $userValidator + * @property \Kanboard\Import\TaskImport $taskImport + * @property \Kanboard\Import\UserImport $userImport + * @property \Kanboard\Export\SubtaskExport $subtaskExport + * @property \Kanboard\Export\TaskExport $taskExport + * @property \Kanboard\Export\TransitionExport $transitionExport * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/Base.php index d62b8507..2879f1f1 100644 --- a/app/Core/Cache/Base.php +++ b/app/Core/Cache/Base.php @@ -11,6 +11,26 @@ namespace Kanboard\Core\Cache; abstract class Base { /** + * Fetch value from cache + * + * @abstract + * @access public + * @param string $key + * @return mixed Null when not found, cached value otherwise + */ + abstract public function get($key); + + /** + * Save a new value in the cache + * + * @abstract + * @access public + * @param string $key + * @param mixed $value + */ + abstract public function set($key, $value); + + /** * Proxy cache * * Note: Arguments must be scalar types diff --git a/app/Core/Csv.php b/app/Core/Csv.php index e45af24c..88010166 100644 --- a/app/Core/Csv.php +++ b/app/Core/Csv.php @@ -87,7 +87,8 @@ class Csv * * @static * @access public - * @return integer + * @param mixed $value + * @return int */ public static function getBooleanValue($value) { diff --git a/app/Core/DateParser.php b/app/Core/DateParser.php index 6577af0f..835eb3e3 100644 --- a/app/Core/DateParser.php +++ b/app/Core/DateParser.php @@ -12,68 +12,95 @@ use DateTime; */ class DateParser extends Base { + const DATE_FORMAT = 'm/d/Y'; + const DATE_TIME_FORMAT = 'm/d/Y H:i'; + /** - * Return true if the date is within the date range + * List of time formats * * @access public - * @param DateTime $date - * @param DateTime $start - * @param DateTime $end - * @return boolean + * @return string[] */ - public function withinDateRange(DateTime $date, DateTime $start, DateTime $end) + public function getTimeFormats() { - return $date >= $start && $date <= $end; + return array( + 'H:i', + 'g:i a', + ); } /** - * Get the total number of hours between 2 datetime objects - * Minutes are rounded to the nearest quarter + * List of date formats * * @access public - * @param DateTime $d1 - * @param DateTime $d2 - * @return float + * @param boolean $iso + * @return string[] */ - public function getHours(DateTime $d1, DateTime $d2) + public function getDateFormats($iso = false) { - $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp())); - return round($seconds / 3600, 2); + $iso_formats = array( + 'Y-m-d', + 'Y_m_d', + ); + + $user_formats = array( + 'm/d/Y', + 'd/m/Y', + 'Y/m/d', + 'd.m.Y', + ); + + return $iso ? array_merge($iso_formats, $user_formats) : $user_formats; } /** - * Round the timestamp to the nearest quarter + * List of datetime formats * * @access public - * @param integer $seconds Timestamp - * @return integer + * @param boolean $iso + * @return string[] */ - public function getRoundedSeconds($seconds) + public function getDateTimeFormats($iso = false) { - return (int) round($seconds / (15 * 60)) * (15 * 60); + $formats = array(); + + foreach ($this->getDateFormats($iso) as $date) { + foreach ($this->getTimeFormats() as $time) { + $formats[] = $date.' '.$time; + } + } + + return $formats; } /** - * Return a timestamp if the given date format is correct otherwise return 0 + * List of all date formats * * @access public - * @param string $value Date to parse - * @param string $format Date format - * @return integer + * @param boolean $iso + * @return string[] */ - public function getValidDate($value, $format) + public function getAllDateFormats($iso = false) { - $date = DateTime::createFromFormat($format, $value); + return array_merge($this->getDateFormats($iso), $this->getDateTimeFormats($iso)); + } - if ($date !== false) { - $errors = DateTime::getLastErrors(); - if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { - $timestamp = $date->getTimestamp(); - return $timestamp > 0 ? $timestamp : 0; - } + /** + * Get available formats (visible in settings) + * + * @access public + * @param array $formats + * @return array + */ + public function getAvailableFormats(array $formats) + { + $values = array(); + + foreach ($formats as $format) { + $values[$format] = date($format); } - return 0; + return $values; } /** @@ -85,7 +112,11 @@ class DateParser extends Base */ public function getTimestamp($value) { - foreach ($this->getAllFormats() as $format) { + if (ctype_digit($value)) { + return (int) $value; + } + + foreach ($this->getAllDateFormats(true) as $format) { $timestamp = $this->getValidDate($value, $format); if ($timestamp !== 0) { @@ -97,104 +128,103 @@ class DateParser extends Base } /** - * Get ISO8601 date from user input + * Return a timestamp if the given date format is correct otherwise return 0 * - * @access public - * @param string $value Date to parse - * @return string + * @access private + * @param string $value Date to parse + * @param string $format Date format + * @return integer */ - public function getIsoDate($value) + private function getValidDate($value, $format) { - return date('Y-m-d', ctype_digit($value) ? $value : $this->getTimestamp($value)); + $date = DateTime::createFromFormat($format, $value); + + if ($date !== false) { + $errors = DateTime::getLastErrors(); + if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { + $timestamp = $date->getTimestamp(); + return $timestamp > 0 ? $timestamp : 0; + } + } + + return 0; } /** - * Get all combinations of date/time formats + * Return true if the date is within the date range * * @access public - * @return string[] + * @param DateTime $date + * @param DateTime $start + * @param DateTime $end + * @return boolean */ - public function getAllFormats() + public function withinDateRange(DateTime $date, DateTime $start, DateTime $end) { - $formats = array(); - - foreach ($this->getDateFormats() as $date) { - foreach ($this->getTimeFormats() as $time) { - $formats[] = $date.' '.$time; - } - } - - return array_merge($formats, $this->getDateFormats()); + return $date >= $start && $date <= $end; } /** - * Return the list of supported date formats (for the parser) + * Get the total number of hours between 2 datetime objects + * Minutes are rounded to the nearest quarter * * @access public - * @return string[] + * @param DateTime $d1 + * @param DateTime $d2 + * @return float */ - public function getDateFormats() + public function getHours(DateTime $d1, DateTime $d2) { - return array( - $this->config->get('application_date_format', 'm/d/Y'), - 'Y-m-d', - 'Y_m_d', - ); + $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp())); + return round($seconds / 3600, 2); } /** - * Return the list of supported time formats (for the parser) + * Round the timestamp to the nearest quarter * * @access public - * @return string[] + * @param integer $seconds Timestamp + * @return integer */ - public function getTimeFormats() + public function getRoundedSeconds($seconds) { - return array( - 'H:i', - 'g:i A', - 'g:iA', - ); + return (int) round($seconds / (15 * 60)) * (15 * 60); } /** - * Return the list of available date formats (for the config page) + * Get ISO-8601 date from user input * * @access public - * @return array + * @param string $value Date to parse + * @return string */ - public function getAvailableFormats() + public function getIsoDate($value) { - return array( - 'm/d/Y' => date('m/d/Y'), - 'd/m/Y' => date('d/m/Y'), - 'Y/m/d' => date('Y/m/d'), - 'd.m.Y' => date('d.m.Y'), - ); + return date('Y-m-d', $this->getTimestamp($value)); } /** - * Remove the time from a timestamp + * Get a timestamp from an ISO date format * * @access public - * @param integer $timestamp Timestamp + * @param string $value * @return integer */ - public function removeTimeFromTimestamp($timestamp) + public function getTimestampFromIsoFormat($value) { - return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); + return $this->removeTimeFromTimestamp(ctype_digit($value) ? $value : strtotime($value)); } /** - * Get a timetstamp from an ISO date format + * Remove the time from a timestamp * * @access public - * @param string $date + * @param integer $timestamp * @return integer */ - public function getTimestampFromIsoFormat($date) + public function removeTimeFromTimestamp($timestamp) { - return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date)); + return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); } /** @@ -204,13 +234,10 @@ class DateParser extends Base * @param array $values Database values * @param string[] $fields Date fields * @param string $format Date format + * @return array */ - public function format(array &$values, array $fields, $format = '') + public function format(array $values, array $fields, $format) { - if ($format === '') { - $format = $this->config->get('application_date_format'); - } - foreach ($fields as $field) { if (! empty($values[$field])) { $values[$field] = date($format, $values[$field]); @@ -218,23 +245,28 @@ class DateParser extends Base $values[$field] = ''; } } + + return $values; } /** - * Convert date (form input data) + * Convert date to timestamp * * @access public * @param array $values Database values * @param string[] $fields Date fields * @param boolean $keep_time Keep time or not + * @return array */ - public function convert(array &$values, array $fields, $keep_time = false) + public function convert(array $values, array $fields, $keep_time = false) { foreach ($fields as $field) { - if (! empty($values[$field]) && ! is_numeric($values[$field])) { + if (! empty($values[$field])) { $timestamp = $this->getTimestamp($values[$field]); $values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp); } } + + return $values; } } diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php index 8d76bfcb..162d23e8 100644 --- a/app/Core/Event/EventManager.php +++ b/app/Core/Event/EventManager.php @@ -52,6 +52,7 @@ class EventManager Task::EVENT_CLOSE => t('Closing a task'), Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), + Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'), ); $events = array_merge($events, $this->events); diff --git a/app/Core/ExternalLink/ExternalLinkInterface.php b/app/Core/ExternalLink/ExternalLinkInterface.php new file mode 100644 index 00000000..2dbc0a19 --- /dev/null +++ b/app/Core/ExternalLink/ExternalLinkInterface.php @@ -0,0 +1,36 @@ +<?php + +namespace Kanboard\Core\ExternalLink; + +/** + * External Link Interface + * + * @package externalLink + * @author Frederic Guillot + */ +interface ExternalLinkInterface +{ + /** + * Get link title + * + * @access public + * @return string + */ + public function getTitle(); + + /** + * Get link URL + * + * @access public + * @return string + */ + public function getUrl(); + + /** + * Set link URL + * + * @access public + * @param string $url + */ + public function setUrl($url); +} diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php new file mode 100644 index 00000000..1fa423c2 --- /dev/null +++ b/app/Core/ExternalLink/ExternalLinkManager.php @@ -0,0 +1,173 @@ +<?php + +namespace Kanboard\Core\ExternalLink; + +use Kanboard\Core\Base; + +/** + * External Link Manager + * + * @package externalLink + * @author Frederic Guillot + */ +class ExternalLinkManager extends Base +{ + /** + * Automatic type value + * + * @var string + */ + const TYPE_AUTO = 'auto'; + + /** + * Registered providers + * + * @access private + * @var array + */ + private $providers = array(); + + /** + * Type chosen by the user + * + * @access private + * @var string + */ + private $userInputType = ''; + + /** + * Text entered by the user + * + * @access private + * @var string + */ + private $userInputText = ''; + + /** + * Register a new provider + * + * Providers are registered in a LIFO queue + * + * @access public + * @param ExternalLinkProviderInterface $provider + * @return ExternalLinkManager + */ + public function register(ExternalLinkProviderInterface $provider) + { + array_unshift($this->providers, $provider); + return $this; + } + + /** + * Get provider + * + * @access public + * @param string $type + * @throws ExternalLinkProviderNotFound + * @return ExternalLinkProviderInterface + */ + public function getProvider($type) + { + foreach ($this->providers as $provider) { + if ($provider->getType() === $type) { + return $provider; + } + } + + throw new ExternalLinkProviderNotFound('Unable to find link provider: '.$type); + } + + /** + * Get link types + * + * @access public + * @return array + */ + public function getTypes() + { + $types = array(); + + foreach ($this->providers as $provider) { + $types[$provider->getType()] = $provider->getName(); + } + + asort($types); + + return array(self::TYPE_AUTO => t('Auto')) + $types; + } + + /** + * Get dependency label from a provider + * + * @access public + * @param string $type + * @param string $dependency + * @return string + */ + public function getDependencyLabel($type, $dependency) + { + $provider = $this->getProvider($type); + $dependencies = $provider->getDependencies(); + return isset($dependencies[$dependency]) ? $dependencies[$dependency] : $dependency; + } + + /** + * Find a provider that match + * + * @access public + * @throws ExternalLinkProviderNotFound + * @return ExternalLinkProviderInterface + */ + public function find() + { + if ($this->userInputType === self::TYPE_AUTO) { + $provider = $this->findProvider(); + } else { + $provider = $this->getProvider($this->userInputType); + $provider->setUserTextInput($this->userInputText); + + if (! $provider->match()) { + throw new ExternalLinkProviderNotFound('Unable to parse URL with selected provider'); + } + } + + if ($provider === null) { + throw new ExternalLinkProviderNotFound('Unable to find link information from provided information'); + } + + return $provider; + } + + /** + * Set form values + * + * @access public + * @param array $values + * @return ExternalLinkManager + */ + public function setUserInput(array $values) + { + $this->userInputType = empty($values['type']) ? self::TYPE_AUTO : $values['type']; + $this->userInputText = empty($values['text']) ? '' : trim($values['text']); + return $this; + } + + /** + * Find a provider that user input + * + * @access private + * @return ExternalLinkProviderInterface + */ + private function findProvider() + { + foreach ($this->providers as $provider) { + $provider->setUserTextInput($this->userInputText); + + if ($provider->match()) { + return $provider; + } + } + + return null; + } +} diff --git a/app/Core/ExternalLink/ExternalLinkProviderInterface.php b/app/Core/ExternalLink/ExternalLinkProviderInterface.php new file mode 100644 index 00000000..c908e1eb --- /dev/null +++ b/app/Core/ExternalLink/ExternalLinkProviderInterface.php @@ -0,0 +1,71 @@ +<?php + +namespace Kanboard\Core\ExternalLink; + +/** + * External Link Provider Interface + * + * @package externalLink + * @author Frederic Guillot + */ +interface ExternalLinkProviderInterface +{ + /** + * Get provider name (label) + * + * @access public + * @return string + */ + public function getName(); + + /** + * Get link type (will be saved in the database) + * + * @access public + * @return string + */ + public function getType(); + + /** + * Get a dictionary of supported dependency types by the provider + * + * Example: + * + * [ + * 'related' => t('Related'), + * 'child' => t('Child'), + * 'parent' => t('Parent'), + * 'self' => t('Self'), + * ] + * + * The dictionary key is saved in the database. + * + * @access public + * @return array + */ + public function getDependencies(); + + /** + * Set text entered by the user + * + * @access public + * @param string $input + */ + public function setUserTextInput($input); + + /** + * Return true if the provider can parse correctly the user input + * + * @access public + * @return boolean + */ + public function match(); + + /** + * Get the link found with the properties + * + * @access public + * @return ExternalLinkInterface + */ + public function getLink(); +} diff --git a/app/Core/ExternalLink/ExternalLinkProviderNotFound.php b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php new file mode 100644 index 00000000..4fd05202 --- /dev/null +++ b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php @@ -0,0 +1,15 @@ +<?php + +namespace Kanboard\Core\ExternalLink; + +use Exception; + +/** + * External Link Provider Not Found Exception + * + * @package externalLink + * @author Frederic Guillot + */ +class ExternalLinkProviderNotFound extends Exception +{ +} diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 5edaa3f0..3764a67c 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -10,16 +10,18 @@ use Pimple\Container; * @package core * @author Frederic Guillot * - * @property \Helper\App $app - * @property \Helper\Asset $asset - * @property \Helper\Dt $dt - * @property \Helper\File $file - * @property \Helper\Form $form - * @property \Helper\Subtask $subtask - * @property \Helper\Task $task - * @property \Helper\Text $text - * @property \Helper\Url $url - * @property \Helper\User $user + * @property \Kanboard\Helper\AppHelper $app + * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\DateHelper $dt + * @property \Kanboard\Helper\FileHelper $file + * @property \Kanboard\Helper\FormHelper $form + * @property \Kanboard\Helper\ModelHelper $model + * @property \Kanboard\Helper\SubtaskHelper $subtask + * @property \Kanboard\Helper\TaskHelper $task + * @property \Kanboard\Helper\TextHelper $text + * @property \Kanboard\Helper\UrlHelper $url + * @property \Kanboard\Helper\UserHelper $user + * @property \Kanboard\Helper\LayoutHelper $layout */ class Helper { @@ -27,17 +29,17 @@ class Helper * Helper instances * * @access private - * @var array + * @var \Pimple\Container */ - private $helpers = array(); + private $helpers; /** * Container instance * - * @access protected + * @access private * @var \Pimple\Container */ - protected $container; + private $container; /** * Constructor @@ -48,33 +50,49 @@ class Helper public function __construct(Container $container) { $this->container = $container; + $this->helpers = new Container; } /** - * Load automatically helpers + * Expose helpers with magic getter * * @access public - * @param string $name Helper name + * @param string $helper * @return mixed */ - public function __get($name) + public function __get($helper) { - if (! isset($this->helpers[$name])) { - $class = '\Kanboard\Helper\\'.ucfirst($name); - $this->helpers[$name] = new $class($this->container); - } + return $this->getHelper($helper); + } - return $this->helpers[$name]; + /** + * Expose helpers with method + * + * @access public + * @param string $helper + * @return mixed + */ + public function getHelper($helper) + { + return $this->helpers[$helper]; } /** - * HTML escaping + * Register a new Helper * - * @param string $value Value to escape - * @return string + * @access public + * @param string $property + * @param string $className + * @return Helper */ - public function e($value) + public function register($property, $className) { - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + $container = $this->container; + + $this->helpers[$property] = function() use($className, $container) { + return new $className($container); + }; + + return $this; } } diff --git a/app/Core/Http/Client.php b/app/Core/Http/Client.php index c6bf36a6..12b0a1cb 100644 --- a/app/Core/Http/Client.php +++ b/app/Core/Http/Client.php @@ -34,6 +34,19 @@ class Client extends Base const HTTP_USER_AGENT = 'Kanboard'; /** + * Send a GET HTTP request + * + * @access public + * @param string $url + * @param string[] $headers + * @return string + */ + public function get($url, array $headers = array()) + { + return $this->doRequest('GET', $url, '', $headers); + } + + /** * Send a GET HTTP request and parse JSON response * * @access public diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php index 1b3036d5..e0df2d3c 100644 --- a/app/Core/Http/Request.php +++ b/app/Core/Http/Request.php @@ -29,7 +29,12 @@ class Request extends Base * Constructor * * @access public - * @param \Pimple\Container $container + * @param \Pimple\Container $container + * @param array $server + * @param array $get + * @param array $post + * @param array $files + * @param array $cookies */ public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array()) { @@ -211,7 +216,11 @@ class Request extends Base */ public function isHTTPS() { - return isset($this->server['HTTPS']) && $this->server['HTTPS'] !== '' && $this->server['HTTPS'] !== 'off'; + if ($this->getServerVariable('HTTP_X_FORWARDED_PROTO') === 'https') { + return true; + } + + return $this->getServerVariable('HTTPS') !== '' && $this->server['HTTPS'] !== 'off'; } /** diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index 7fefddeb..d098f519 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -68,11 +68,12 @@ class Response extends Base * * @access public * @param string $url Redirection URL + * @param boolean $self If Ajax request and true: refresh the current page */ - public function redirect($url) + public function redirect($url, $self = false) { - if ($this->request->getServerVariable('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') { - header('X-Ajax-Redirect: '.$url); + if ($this->request->isAjax()) { + header('X-Ajax-Redirect: '.($self ? 'self' : $url)); } else { header('Location: '.$url); } diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php index 63149ae3..05658190 100644 --- a/app/Core/Ldap/Client.php +++ b/app/Core/Ldap/Client.php @@ -31,7 +31,7 @@ class Client */ public static function connect($username = null, $password = null) { - $client = new self; + $client = new static; $client->open($client->getLdapServer()); $username = $username ?: $client->getLdapUsername(); $password = $password ?: $client->getLdapPassword(); @@ -60,6 +60,7 @@ class Client * Establish server connection * * @access public + * @throws ClientException * @param string $server LDAP server hostname or IP * @param integer $port LDAP port * @param boolean $tls Start TLS @@ -98,6 +99,7 @@ class Client * Anonymous authentication * * @access public + * @throws ClientException * @return boolean */ public function useAnonymousAuthentication() @@ -113,6 +115,7 @@ class Client * Authentication with username/password * * @access public + * @throws ClientException * @param string $bind_rdn * @param string $bind_password * @return boolean diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php index e03495ec..1779fa61 100644 --- a/app/Core/Ldap/Query.php +++ b/app/Core/Ldap/Query.php @@ -78,7 +78,7 @@ class Query * Get LDAP Entries * * @access public - * @return Entities + * @return Entries */ public function getEntries() { diff --git a/app/Core/Mail/Client.php b/app/Core/Mail/Client.php index e1f31696..641b6abe 100644 --- a/app/Core/Mail/Client.php +++ b/app/Core/Mail/Client.php @@ -41,7 +41,7 @@ class Client extends Base * @param string $name * @param string $subject * @param string $html - * @return EmailClient + * @return Client */ public function send($email, $name, $subject, $html) { @@ -70,7 +70,7 @@ class Client extends Base * * @access public * @param string $transport - * @return EmailClientInterface + * @return ClientInterface */ public function getTransport($transport) { @@ -83,7 +83,7 @@ class Client extends Base * @access public * @param string $transport * @param string $class - * @return EmailClient + * @return Client */ public function setTransport($transport, $class) { diff --git a/app/Core/ObjectStorage/FileStorage.php b/app/Core/ObjectStorage/FileStorage.php index dd049ca2..18453890 100644 --- a/app/Core/ObjectStorage/FileStorage.php +++ b/app/Core/ObjectStorage/FileStorage.php @@ -33,6 +33,7 @@ class FileStorage implements ObjectStorageInterface * Fetch object contents * * @access public + * @throws ObjectStorageException * @param string $key * @return string */ @@ -51,6 +52,7 @@ class FileStorage implements ObjectStorageInterface * Save object * * @access public + * @throws ObjectStorageException * @param string $key * @param string $blob */ @@ -67,6 +69,7 @@ class FileStorage implements ObjectStorageInterface * Output directly object content * * @access public + * @throws ObjectStorageException * @param string $key */ public function output($key) @@ -84,6 +87,7 @@ class FileStorage implements ObjectStorageInterface * Move local file to object storage * * @access public + * @throws ObjectStorageException * @param string $src_filename * @param string $key * @return boolean @@ -136,6 +140,7 @@ class FileStorage implements ObjectStorageInterface * Create object folder * * @access private + * @throws ObjectStorageException * @param string $key */ private function createFolder($key) diff --git a/app/Core/Plugin/Loader.php b/app/Core/Plugin/Loader.php index 530d9b40..ff4f2c14 100644 --- a/app/Core/Plugin/Loader.php +++ b/app/Core/Plugin/Loader.php @@ -55,6 +55,7 @@ class Loader extends \Kanboard\Core\Base * Load plugin * * @access public + * @throws LogicException * @param string $plugin */ public function load($plugin) diff --git a/app/Core/Security/AccessMap.php b/app/Core/Security/AccessMap.php index f34c4b00..2431a921 100644 --- a/app/Core/Security/AccessMap.php +++ b/app/Core/Security/AccessMap.php @@ -39,7 +39,7 @@ class AccessMap * * @access public * @param string $role - * @return Acl + * @return AccessMap */ public function setDefaultRole($role) { @@ -53,7 +53,7 @@ class AccessMap * @access public * @param string $role * @param array $subroles - * @return Acl + * @return AccessMap */ public function setRoleHierarchy($role, array $subroles) { @@ -113,7 +113,7 @@ class AccessMap * @param string $controller Controller class name * @param mixed $methods List of method name or just one method * @param string $role Lowest role required - * @return Acl + * @return AccessMap */ public function add($controller, $methods, $role) { @@ -135,7 +135,7 @@ class AccessMap * @param string $controller * @param string $method * @param string $role - * @return Acl + * @return AccessMap */ private function addRule($controller, $method, $role) { @@ -157,7 +157,7 @@ class AccessMap * @access public * @param string $controller * @param string $method - * @return boolean + * @return array */ public function getRoles($controller, $method) { diff --git a/app/Core/Security/OAuthAuthenticationProviderInterface.php b/app/Core/Security/OAuthAuthenticationProviderInterface.php index c32339e0..3092672d 100644 --- a/app/Core/Security/OAuthAuthenticationProviderInterface.php +++ b/app/Core/Security/OAuthAuthenticationProviderInterface.php @@ -14,7 +14,7 @@ interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInt * Get user object * * @access public - * @return UserProviderInterface + * @return \Kanboard\Core\User\UserProviderInterface */ public function getUser(); @@ -31,7 +31,7 @@ interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInt * Get configured OAuth2 service * * @access public - * @return Kanboard\Core\Http\OAuth2 + * @return \Kanboard\Core\Http\OAuth2 */ public function getService(); diff --git a/app/Core/Security/PasswordAuthenticationProviderInterface.php b/app/Core/Security/PasswordAuthenticationProviderInterface.php index 918a4aec..c2304546 100644 --- a/app/Core/Security/PasswordAuthenticationProviderInterface.php +++ b/app/Core/Security/PasswordAuthenticationProviderInterface.php @@ -14,7 +14,7 @@ interface PasswordAuthenticationProviderInterface extends AuthenticationProvider * Get user object * * @access public - * @return UserProviderInterface + * @return \Kanboard\Core\User\UserProviderInterface */ public function getUser(); diff --git a/app/Core/Security/PreAuthenticationProviderInterface.php b/app/Core/Security/PreAuthenticationProviderInterface.php index 391e8d0f..c13b06c5 100644 --- a/app/Core/Security/PreAuthenticationProviderInterface.php +++ b/app/Core/Security/PreAuthenticationProviderInterface.php @@ -14,7 +14,7 @@ interface PreAuthenticationProviderInterface extends AuthenticationProviderInter * Get user object * * @access public - * @return UserProviderInterface + * @return \Kanboard\Core\User\UserProviderInterface */ public function getUser(); } diff --git a/app/Core/Template.php b/app/Core/Template.php index 8ded6f7c..f85c7f28 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -3,63 +3,50 @@ namespace Kanboard\Core; /** - * Template class + * Template * * @package core * @author Frederic Guillot */ -class Template extends Helper +class Template { /** - * List of template overrides + * Helper object * * @access private - * @var array + * @var Helper */ - private $overrides = array(); + private $helper; /** - * Rendering start time - * - * @access private - * @var float - */ - private $startTime = 0; - - /** - * Total rendering time + * List of template overrides * * @access private - * @var float + * @var array */ - private $renderingTime = 0; + private $overrides = array(); /** - * Method executed before the rendering + * Template constructor * - * @access protected - * @param string $template + * @access public + * @param Helper $helper */ - protected function beforeRender($template) + public function __construct(Helper $helper) { - if (DEBUG) { - $this->startTime = microtime(true); - } + $this->helper = $helper; } /** - * Method executed after the rendering + * Expose helpers with magic getter * - * @access protected - * @param string $template + * @access public + * @param string $helper + * @return mixed */ - protected function afterRender($template) + public function __get($helper) { - if (DEBUG) { - $duration = microtime(true) - $this->startTime; - $this->renderingTime += $duration; - $this->container['logger']->debug('Rendering '.$template.' in '.$duration.'s, total='.$this->renderingTime); - } + return $this->helper->getHelper($helper); } /** @@ -76,33 +63,10 @@ class Template extends Helper */ public function render($__template_name, array $__template_args = array()) { - $this->beforeRender($__template_name); - extract($__template_args); ob_start(); include $this->getTemplateFile($__template_name); - $html = ob_get_clean(); - - $this->afterRender($__template_name); - - return $html; - } - - /** - * Render a page layout - * - * @access public - * @param string $template_name Template name - * @param array $template_args Key/value map - * @param string $layout_name Layout name - * @return string - */ - public function layout($template_name, array $template_args = array(), $layout_name = 'layout') - { - return $this->render( - $layout_name, - $template_args + array('content_for_layout' => $this->render($template_name, $template_args)) - ); + return ob_get_clean(); } /** diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 96a481f6..113c0dc6 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -147,32 +147,6 @@ class Translator } /** - * Get a formatted datetime - * - * $translator->datetime('%Y-%m-%d', time()); - * - * @access public - * @param string $format Format defined by the strftime function - * @param integer $timestamp Unix timestamp - * @return string - */ - public function datetime($format, $timestamp) - { - if (! $timestamp) { - return ''; - } - - $format = $this->get($format, $format); - - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - $format = str_replace('%e', '%d', $format); - $format = str_replace('%k', '%H', $format); - } - - return strftime($format, (int) $timestamp); - } - - /** * Get an identifier from the translations or return the default * * @access public @@ -199,8 +173,6 @@ class Translator */ public static function load($language, $path = self::PATH) { - setlocale(LC_TIME, $language.'.UTF-8', $language); - $filename = $path.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.'translations.php'; if (file_exists($filename)) { diff --git a/app/Core/User/UserProfile.php b/app/Core/User/UserProfile.php index ccbc7f06..ef325801 100644 --- a/app/Core/User/UserProfile.php +++ b/app/Core/User/UserProfile.php @@ -52,7 +52,7 @@ class UserProfile extends Base $this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds()); } - if (! empty($profile)) { + if (! empty($profile) && $profile['is_active'] == 1) { $this->userSession->initialize($profile); return true; } diff --git a/app/Event/GenericEvent.php b/app/Event/GenericEvent.php index 1129fd16..94a51479 100644 --- a/app/Event/GenericEvent.php +++ b/app/Event/GenericEvent.php @@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\Event as BaseEvent; class GenericEvent extends BaseEvent implements ArrayAccess { - private $container = array(); + protected $container = array(); public function __construct(array $values = array()) { diff --git a/app/Event/TaskListEvent.php b/app/Event/TaskListEvent.php new file mode 100644 index 00000000..9be1a7d9 --- /dev/null +++ b/app/Event/TaskListEvent.php @@ -0,0 +1,11 @@ +<?php + +namespace Kanboard\Event; + +class TaskListEvent extends GenericEvent +{ + public function setTasks(array &$tasks) + { + $this->container['tasks'] =& $tasks; + } +} diff --git a/app/Model/SubtaskExport.php b/app/Export/SubtaskExport.php index 7c4e941d..386c566b 100644 --- a/app/Model/SubtaskExport.php +++ b/app/Export/SubtaskExport.php @@ -1,11 +1,16 @@ <?php -namespace Kanboard\Model; +namespace Kanboard\Export; + +use Kanboard\Core\Base; +use Kanboard\Model\Task; +use Kanboard\Model\Subtask; +use Kanboard\Model\User; /** * Subtask Export * - * @package model + * @package export * @author Frederic Guillot */ class SubtaskExport extends Base diff --git a/app/Model/TaskExport.php b/app/Export/TaskExport.php index 278c0897..b98582aa 100644 --- a/app/Model/TaskExport.php +++ b/app/Export/TaskExport.php @@ -1,13 +1,16 @@ <?php -namespace Kanboard\Model; +namespace Kanboard\Export; +use Kanboard\Core\Base; +use Kanboard\Core\DateParser; +use Kanboard\Model\Task; use PDO; /** - * Task Export model + * Task Export * - * @package model + * @package export * @author Frederic Guillot */ class TaskExport extends Base @@ -106,7 +109,7 @@ class TaskExport extends Base $task['score'] = $task['score'] ?: 0; $task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?'; - $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d'); + $task = $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), DateParser::DATE_FORMAT); return $task; } diff --git a/app/Export/TransitionExport.php b/app/Export/TransitionExport.php new file mode 100644 index 00000000..97dc28a7 --- /dev/null +++ b/app/Export/TransitionExport.php @@ -0,0 +1,77 @@ +<?php + +namespace Kanboard\Export; + +use Kanboard\Core\Base; +use Kanboard\Core\DateParser; + +/** + * Transition Export + * + * @package export + * @author Frederic Guillot + */ +class TransitionExport extends Base +{ + /** + * Get project export + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $results = array($this->getColumns()); + $transitions = $this->transition->getAllByProjectAndDate($project_id, $from, $to); + + foreach ($transitions as $transition) { + $results[] = $this->format($transition); + } + + return $results; + } + + /** + * Get column titles + * + * @access protected + * @return string[] + */ + protected function getColumns() + { + return array( + e('Id'), + e('Task Title'), + e('Source column'), + e('Destination column'), + e('Executer'), + e('Date'), + e('Time spent'), + ); + } + + /** + * Format the output of a transition array + * + * @access protected + * @param array $transition + * @return array + */ + protected function format(array $transition) + { + $values = array( + (int) $transition['id'], + $transition['title'], + $transition['src_column'], + $transition['dst_column'], + $transition['name'] ?: $transition['username'], + date($this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT), $transition['date']), + round($transition['time_spent'] / 3600, 2) + ); + + return $values; + } +} diff --git a/app/ExternalLink/AttachmentLink.php b/app/ExternalLink/AttachmentLink.php new file mode 100644 index 00000000..5a0d1344 --- /dev/null +++ b/app/ExternalLink/AttachmentLink.php @@ -0,0 +1,26 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\ExternalLink\ExternalLinkInterface; + +/** + * Attachment Link + * + * @package externalLink + * @author Frederic Guillot + */ +class AttachmentLink extends BaseLink implements ExternalLinkInterface +{ + /** + * Get link title + * + * @access public + * @return string + */ + public function getTitle() + { + $path = parse_url($this->url, PHP_URL_PATH); + return basename($path); + } +} diff --git a/app/ExternalLink/AttachmentLinkProvider.php b/app/ExternalLink/AttachmentLinkProvider.php new file mode 100644 index 00000000..706ac1dd --- /dev/null +++ b/app/ExternalLink/AttachmentLinkProvider.php @@ -0,0 +1,117 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface; + +/** + * Attachment Link Provider + * + * @package externalLink + * @author Frederic Guillot + */ +class AttachmentLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface +{ + /** + * File extensions that are not attachments + * + * @access protected + * @var array + */ + protected $extensions = array( + 'html', + 'htm', + 'xhtml', + 'php', + 'jsp', + 'do', + 'action', + 'asp', + 'aspx', + 'cgi', + ); + + /** + * Get provider name + * + * @access public + * @return string + */ + public function getName() + { + return t('Attachment'); + } + + /** + * Get link type + * + * @access public + * @return string + */ + public function getType() + { + return 'attachment'; + } + + /** + * Get a dictionary of supported dependency types by the provider + * + * @access public + * @return array + */ + public function getDependencies() + { + return array( + 'related' => t('Related'), + ); + } + + /** + * Return true if the provider can parse correctly the user input + * + * @access public + * @return boolean + */ + public function match() + { + if (preg_match('/^https?:\/\/.*\.([^\/]+)$/', $this->userInput, $matches)) { + return $this->isValidExtension($matches[1]); + } + + return false; + } + + /** + * Get the link found with the properties + * + * @access public + * @return \Kanboard\Core\ExternalLink\ExternalLinkInterface + */ + public function getLink() + { + $link = new AttachmentLink($this->container); + $link->setUrl($this->userInput); + + return $link; + } + + /** + * Check file extension + * + * @access protected + * @param string $extension + * @return boolean + */ + protected function isValidExtension($extension) + { + $extension = strtolower($extension); + + foreach ($this->extensions as $ext) { + if ($extension === $ext) { + return false; + } + } + + return true; + } +} diff --git a/app/ExternalLink/BaseLink.php b/app/ExternalLink/BaseLink.php new file mode 100644 index 00000000..08693ae7 --- /dev/null +++ b/app/ExternalLink/BaseLink.php @@ -0,0 +1,44 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\Base; + +/** + * Base Link + * + * @package externalLink + * @author Frederic Guillot + */ +abstract class BaseLink extends Base +{ + /** + * URL + * + * @access protected + * @var string + */ + protected $url = ''; + + /** + * Get link URL + * + * @access public + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set link URL + * + * @access public + * @param string $url + */ + public function setUrl($url) + { + $this->url = $url; + } +} diff --git a/app/ExternalLink/BaseLinkProvider.php b/app/ExternalLink/BaseLinkProvider.php new file mode 100644 index 00000000..749cda94 --- /dev/null +++ b/app/ExternalLink/BaseLinkProvider.php @@ -0,0 +1,33 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\Base; + +/** + * Base Link Provider + * + * @package externalLink + * @author Frederic Guillot + */ +abstract class BaseLinkProvider extends Base +{ + /** + * User input + * + * @access protected + * @var string + */ + protected $userInput = ''; + + /** + * Set text entered by the user + * + * @access public + * @param string $input + */ + public function setUserTextInput($input) + { + $this->userInput = trim($input); + } +} diff --git a/app/ExternalLink/WebLink.php b/app/ExternalLink/WebLink.php new file mode 100644 index 00000000..9338ca42 --- /dev/null +++ b/app/ExternalLink/WebLink.php @@ -0,0 +1,37 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\ExternalLink\ExternalLinkInterface; + +/** + * Web Link + * + * @package externalLink + * @author Frederic Guillot + */ +class WebLink extends BaseLink implements ExternalLinkInterface +{ + /** + * Get link title + * + * @access public + * @return string + */ + public function getTitle() + { + $html = $this->httpClient->get($this->url); + + if (preg_match('/<title>(.*)<\/title>/siU', $html, $matches)) { + return trim($matches[1]); + } + + $components = parse_url($this->url); + + if (! empty($components['host']) && ! empty($components['path'])) { + return $components['host'].$components['path']; + } + + return t('Title not found'); + } +} diff --git a/app/ExternalLink/WebLinkProvider.php b/app/ExternalLink/WebLinkProvider.php new file mode 100644 index 00000000..5ec1bbe4 --- /dev/null +++ b/app/ExternalLink/WebLinkProvider.php @@ -0,0 +1,77 @@ +<?php + +namespace Kanboard\ExternalLink; + +use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface; + +/** + * Web Link Provider + * + * @package externalLink + * @author Frederic Guillot + */ +class WebLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface +{ + /** + * Get provider name + * + * @access public + * @return string + */ + public function getName() + { + return t('Web Link'); + } + + /** + * Get link type + * + * @access public + * @return string + */ + public function getType() + { + return 'weblink'; + } + + /** + * Get a dictionary of supported dependency types by the provider + * + * @access public + * @return array + */ + public function getDependencies() + { + return array( + 'related' => t('Related'), + ); + } + + /** + * Return true if the provider can parse correctly the user input + * + * @access public + * @return boolean + */ + public function match() + { + $startWithHttp = strpos($this->userInput, 'http://') === 0 || strpos($this->userInput, 'https://') === 0; + $validUrl = filter_var($this->userInput, FILTER_VALIDATE_URL); + + return $startWithHttp && $validUrl; + } + + /** + * Get the link found with the properties + * + * @access public + * @return \Kanboard\Core\ExternalLink\ExternalLinkInterface + */ + public function getLink() + { + $link = new WebLink($this->container); + $link->setUrl($this->userInput); + + return $link; + } +} diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskFilterGanttFormatter.php index 08059d4c..a4eef1ee 100644 --- a/app/Formatter/TaskFilterGanttFormatter.php +++ b/app/Formatter/TaskFilterGanttFormatter.php @@ -47,7 +47,7 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface private function formatTask(array $task) { if (! isset($this->columns[$task['project_id']])) { - $this->columns[$task['project_id']] = $this->board->getColumnsList($task['project_id']); + $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); } $start = $task['date_started'] ?: time(); diff --git a/app/Helper/App.php b/app/Helper/AppHelper.php index 0593795f..e6f6412d 100644 --- a/app/Helper/App.php +++ b/app/Helper/AppHelper.php @@ -5,23 +5,24 @@ namespace Kanboard\Helper; use Kanboard\Core\Base; /** - * Application helpers + * Application Helper * * @package helper * @author Frederic Guillot */ -class App extends Base +class AppHelper extends Base { /** * Get config variable * * @access public * @param string $param + * @param mixed $default_value * @return mixed */ - public function config($param) + public function config($param, $default_value = '') { - return $this->config->get($param); + return $this->config->get($param, $default_value); } /** @@ -115,11 +116,11 @@ class App extends Base $failure_message = $this->flash->getMessage('failure'); if (! empty($success_message)) { - return '<div class="alert alert-success alert-fade-out">'.$this->helper->e($success_message).'</div>'; + return '<div class="alert alert-success alert-fade-out">'.$this->helper->text->e($success_message).'</div>'; } if (! empty($failure_message)) { - return '<div class="alert alert-error">'.$this->helper->e($failure_message).'</div>'; + return '<div class="alert alert-error">'.$this->helper->text->e($failure_message).'</div>'; } return ''; diff --git a/app/Helper/Asset.php b/app/Helper/AssetHelper.php index c4178e8c..b3dc711f 100644 --- a/app/Helper/Asset.php +++ b/app/Helper/AssetHelper.php @@ -2,18 +2,21 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** - * Assets helpers + * Asset Helper * * @package helper * @author Frederic Guillot */ -class Asset extends \Kanboard\Core\Base +class AssetHelper extends Base { /** * Add a Javascript asset * - * @param string $filename Filename + * @param string $filename Filename + * @param bool $async * @return string */ public function js($filename, $async = false) diff --git a/app/Helper/Board.php b/app/Helper/BoardHelper.php index 430d1858..a86a6c18 100644 --- a/app/Helper/Board.php +++ b/app/Helper/BoardHelper.php @@ -2,13 +2,15 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** * Board Helper * * @package helper * @author Frederic Guillot */ -class Board extends \Kanboard\Core\Base +class BoardHelper extends Base { /** * Return true if tasks are collapsed diff --git a/app/Helper/Dt.php b/app/Helper/DateHelper.php index 78002b1b..3844ce64 100644 --- a/app/Helper/Dt.php +++ b/app/Helper/DateHelper.php @@ -3,6 +3,7 @@ namespace Kanboard\Helper; use DateTime; +use Kanboard\Core\Base; /** * DateTime helpers @@ -10,9 +11,53 @@ use DateTime; * @package helper * @author Frederic Guillot */ -class Dt extends \Kanboard\Core\Base +class DateHelper extends Base { /** + * Get formatted time + * + * @access public + * @param integer $value + * @return string + */ + public function time($value) + { + return date($this->config->get('application_time_format', 'H:i'), $value); + } + + /** + * Get formatted date + * + * @access public + * @param integer $value + * @return string + */ + public function date($value) + { + if (empty($value)) { + return ''; + } + + if (! ctype_digit($value)) { + $value = strtotime($value); + } + + return date($this->config->get('application_date_format', 'm/d/Y'), $value); + } + + /** + * Get formatted datetime + * + * @access public + * @param integer $value + * @return string + */ + public function datetime($value) + { + return date($this->config->get('application_datetime_format', 'm/d/Y H:i'), $value); + } + + /** * Get duration in seconds into human format * * @access public @@ -107,6 +152,6 @@ class Dt extends \Kanboard\Core\Base */ public function getWeekDay($day) { - return dt('%A', strtotime('next Monday +'.($day - 1).' days')); + return date('l', strtotime('next Monday +'.($day - 1).' days')); } } diff --git a/app/Helper/File.php b/app/Helper/File.php deleted file mode 100644 index d2cdfc6a..00000000 --- a/app/Helper/File.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php - -namespace Kanboard\Helper; - -/** - * File helpers - * - * @package helper - * @author Frederic Guillot - */ -class File extends \Kanboard\Core\Base -{ - /** - * Get file icon - * - * @access public - * @param string $filename Filename - * @return string Font-Awesome-Icon-Name - */ - public function icon($filename) - { - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - switch ($extension) { - case 'jpeg': - case 'jpg': - case 'png': - case 'gif': - return 'fa-file-image-o'; - case 'xls': - case 'xlsx': - return 'fa-file-excel-o'; - case 'doc': - case 'docx': - return 'fa-file-word-o'; - case 'ppt': - case 'pptx': - return 'fa-file-powerpoint-o'; - case 'zip': - case 'rar': - return 'fa-file-archive-o'; - case 'mp3': - return 'fa-audio-o'; - case 'avi': - return 'fa-video-o'; - case 'php': - case 'html': - case 'css': - return 'fa-code-o'; - case 'pdf': - return 'fa-file-pdf-o'; - } - - return 'fa-file-o'; - } -} diff --git a/app/Helper/FileHelper.php b/app/Helper/FileHelper.php new file mode 100644 index 00000000..cabf371c --- /dev/null +++ b/app/Helper/FileHelper.php @@ -0,0 +1,109 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * File helpers + * + * @package helper + * @author Frederic Guillot + */ +class FileHelper extends Base +{ + /** + * Get file icon + * + * @access public + * @param string $filename Filename + * @return string Font-Awesome-Icon-Name + */ + public function icon($filename) + { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + switch ($extension) { + case 'jpeg': + case 'jpg': + case 'png': + case 'gif': + return 'fa-file-image-o'; + case 'xls': + case 'xlsx': + return 'fa-file-excel-o'; + case 'doc': + case 'docx': + return 'fa-file-word-o'; + case 'ppt': + case 'pptx': + return 'fa-file-powerpoint-o'; + case 'zip': + case 'rar': + case 'tar': + case 'bz2': + case 'xz': + case 'gz': + return 'fa-file-archive-o'; + case 'mp3': + return 'fa-file-audio-o'; + case 'avi': + case 'mov': + return 'fa-file-video-o'; + case 'php': + case 'html': + case 'css': + return 'fa-file-code-o'; + case 'pdf': + return 'fa-file-pdf-o'; + } + + return 'fa-file-o'; + } + + /** + * Return the image mimetype based on the file extension + * + * @access public + * @param $filename + * @return string + */ + public function getImageMimeType($filename) + { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + switch ($extension) { + case 'jpeg': + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + default: + return 'image/jpeg'; + } + } + + /** + * Get the preview type + * + * @access public + * @param string $filename + * @return string + */ + public function getPreviewType($filename) + { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + switch ($extension) { + case 'md': + case 'markdown': + return 'markdown'; + case 'txt': + return 'text'; + } + + return null; + } +} diff --git a/app/Helper/Form.php b/app/Helper/FormHelper.php index bfd75ee3..c2ea1d72 100644 --- a/app/Helper/Form.php +++ b/app/Helper/FormHelper.php @@ -10,7 +10,7 @@ use Kanboard\Core\Base; * @package helper * @author Frederic Guillot */ -class Form extends Base +class FormHelper extends Base { /** * Hidden CSRF token field @@ -40,11 +40,12 @@ class Form extends Base * Display a select field * * @access public - * @param string $name Field name - * @param array $options Options - * @param array $values Form values - * @param array $errors Form errors - * @param string $class CSS class + * @param string $name Field name + * @param array $options Options + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes + * @param string $class CSS class * @return string */ public function select($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '') @@ -52,7 +53,7 @@ class Form extends Base $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>'; foreach ($options as $id => $value) { - $html .= '<option value="'.$this->helper->e($id).'"'; + $html .= '<option value="'.$this->helper->text->e($id).'"'; if (isset($values->$name) && $id == $values->$name) { $html .= ' selected="selected"'; @@ -61,7 +62,7 @@ class Form extends Base $html .= ' selected="selected"'; } - $html .= '>'.$this->helper->e($value).'</option>'; + $html .= '>'.$this->helper->text->e($value).'</option>'; } $html .= '</select>'; @@ -103,7 +104,7 @@ class Form extends Base */ public function radio($name, $label, $value, $selected = false, $class = '') { - return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>'; + return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->text->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->text->e($label).'</label>'; } /** @@ -139,7 +140,7 @@ class Form extends Base */ public function checkbox($name, $label, $value, $checked = false, $class = '') { - return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($checked ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>'; + return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->text->e($value).'" '.($checked ? 'checked="checked"' : '').'> '.$this->helper->text->e($label).'</label>'; } /** @@ -153,7 +154,7 @@ class Form extends Base */ public function label($label, $name, array $attributes = array()) { - return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->e($label).'</label>'; + return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->text->e($label).'</label>'; } /** @@ -173,7 +174,7 @@ class Form extends Base $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '; $html .= implode(' ', $attributes).'>'; - $html .= isset($values->$name) ? $this->helper->e($values->$name) : isset($values[$name]) ? $values[$name] : ''; + $html .= isset($values->$name) ? $this->helper->text->e($values->$name) : isset($values[$name]) ? $values[$name] : ''; $html .= '</textarea>'; $html .= $this->errorList($errors, $name); @@ -334,7 +335,7 @@ class Form extends Base $html .= '<ul class="form-errors">'; foreach ($errors[$name] as $error) { - $html .= '<li>'.$this->helper->e($error).'</li>'; + $html .= '<li>'.$this->helper->text->e($error).'</li>'; } $html .= '</ul>'; @@ -354,9 +355,9 @@ class Form extends Base private function formValue($values, $name) { if (isset($values->$name)) { - return 'value="'.$this->helper->e($values->$name).'"'; + return 'value="'.$this->helper->text->e($values->$name).'"'; } - return isset($values[$name]) ? 'value="'.$this->helper->e($values[$name]).'"' : ''; + return isset($values[$name]) ? 'value="'.$this->helper->text->e($values[$name]).'"' : ''; } } diff --git a/app/Helper/Hook.php b/app/Helper/HookHelper.php index 7b691949..2d13ebcc 100644 --- a/app/Helper/Hook.php +++ b/app/Helper/HookHelper.php @@ -2,13 +2,15 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** * Template Hook helpers * * @package helper * @author Frederic Guillot */ -class Hook extends \Kanboard\Core\Base +class HookHelper extends Base { /** * Add assets JS or CSS @@ -54,7 +56,7 @@ class Hook extends \Kanboard\Core\Base * @access public * @param string $hook * @param string $template - * @return \Helper\Hook + * @return \Kanboard\Helper\Hook */ public function attach($hook, $template) { diff --git a/app/Helper/LayoutHelper.php b/app/Helper/LayoutHelper.php new file mode 100644 index 00000000..9384da1b --- /dev/null +++ b/app/Helper/LayoutHelper.php @@ -0,0 +1,188 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * Layout Helper + * + * @package helper + * @author Frederic Guillot + */ +class LayoutHelper extends Base +{ + /** + * Render a template without the layout if Ajax request + * + * @access public + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + public function app($template, array $params = array()) + { + if ($this->request->isAjax()) { + return $this->template->render($template, $params); + } + + if (! isset($params['no_layout']) && ! isset($params['board_selector'])) { + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + } + + return $this->pageLayout($template, $params); + } + + /** + * Common layout for user views + * + * @access public + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + public function user($template, array $params) + { + if (isset($params['user'])) { + $params['title'] = '#'.$params['user']['id'].' '.($params['user']['name'] ?: $params['user']['username']); + } + + return $this->subLayout('user/layout', 'user/sidebar', $template, $params); + } + + /** + * Common layout for task views + * + * @access public + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + public function task($template, array $params) + { + $params['title'] = $params['task']['project_name']; + return $this->subLayout('task/layout', 'task/sidebar', $template, $params); + } + + /** + * Common layout for project views + * + * @access public + * @param string $template + * @param array $params + * @param string $sidebar + * @return string + */ + public function project($template, array $params, $sidebar = 'project/sidebar') + { + if (empty($params['title'])) { + $params['title'] = $params['project']['name']; + } elseif ($params['project']['name'] !== $params['title']) { + $params['title'] = $params['project']['name'].' > '.$params['title']; + } + + return $this->subLayout('project/layout', $sidebar, $template, $params); + } + + /** + * Common layout for project user views + * + * @access public + * @param string $template + * @param array $params + * @return string + */ + public function projectUser($template, array $params) + { + $params['filter'] = array('user_id' => $params['user_id']); + return $this->subLayout('project_user/layout', 'project_user/sidebar', $template, $params); + } + + /** + * Common layout for config views + * + * @access public + * @param string $template + * @param array $params + * @return string + */ + public function config($template, array $params) + { + if (! isset($params['values'])) { + $params['values'] = $this->config->getAll(); + } + + if (! isset($params['errors'])) { + $params['errors'] = array(); + } + + return $this->subLayout('config/layout', 'config/sidebar', $template, $params); + } + + /** + * Common layout for dashboard views + * + * @access public + * @param string $template + * @param array $params + * @return string + */ + public function dashboard($template, array $params) + { + return $this->subLayout('app/layout', 'app/sidebar', $template, $params); + } + + /** + * Common layout for analytic views + * + * @access public + * @param string $template + * @param array $params + * @return string + */ + public function analytic($template, array $params) + { + return $this->subLayout('analytic/layout', 'analytic/sidebar', $template, $params); + } + + /** + * Render page layout + * + * @access public + * @param string $template Template name + * @param array $params Key/value dictionary + * @param string $layout Layout name + * @return string + */ + public function pageLayout($template, array $params = array(), $layout = 'layout') + { + return $this->template->render( + $layout, + $params + array('content_for_layout' => $this->template->render($template, $params)) + ); + } + + /** + * Common method to generate a sub-layout + * + * @access public + * @param string $sublayout + * @param string $sidebar + * @param string $template + * @param array $params + * @return string + */ + public function subLayout($sublayout, $sidebar, $template, array $params = array()) + { + $content = $this->template->render($template, $params); + + if ($this->request->isAjax()) { + return $content; + } + + $params['content_for_sublayout'] = $content; + $params['sidebar_template'] = $sidebar; + + return $this->app($sublayout, $params); + } +} diff --git a/app/Helper/ModelHelper.php b/app/Helper/ModelHelper.php new file mode 100644 index 00000000..d49637c8 --- /dev/null +++ b/app/Helper/ModelHelper.php @@ -0,0 +1,94 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * Model Helper + * + * @package helper + * @author Frederic Guillot + */ +class ModelHelper extends Base +{ + /** + * Remove keys from an array + * + * @access public + * @param array $values Input array + * @param string[] $keys List of keys to remove + */ + public function removeFields(array &$values, array $keys) + { + foreach ($keys as $key) { + if (array_key_exists($key, $values)) { + unset($values[$key]); + } + } + } + + /** + * Remove keys from an array if empty + * + * @access public + * @param array $values Input array + * @param string[] $keys List of keys to remove + */ + public function removeEmptyFields(array &$values, array $keys) + { + foreach ($keys as $key) { + if (array_key_exists($key, $values) && empty($values[$key])) { + unset($values[$key]); + } + } + } + + /** + * Force fields to be at 0 if empty + * + * @access public + * @param array $values Input array + * @param string[] $keys List of keys + */ + public function resetFields(array &$values, array $keys) + { + foreach ($keys as $key) { + if (isset($values[$key]) && empty($values[$key])) { + $values[$key] = 0; + } + } + } + + /** + * Force some fields to be integer + * + * @access public + * @param array $values Input array + * @param string[] $keys List of keys + */ + public function convertIntegerFields(array &$values, array $keys) + { + foreach ($keys as $key) { + if (isset($values[$key])) { + $values[$key] = (int) $values[$key]; + } + } + } + + /** + * Force some fields to be null if empty + * + * @access public + * @param array $values Input array + * @param string[] $keys List of keys + */ + public function convertNullFields(array &$values, array $keys) + { + foreach ($keys as $key) { + if (array_key_exists($key, $values) && empty($values[$key])) { + $values[$key] = null; + } + } + } +} diff --git a/app/Helper/Subtask.php b/app/Helper/Subtask.php deleted file mode 100644 index 90bd733e..00000000 --- a/app/Helper/Subtask.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -namespace Kanboard\Helper; - -/** - * Subtask helpers - * - * @package helper - * @author Frederic Guillot - */ -class Subtask extends \Kanboard\Core\Base -{ - /** - * Get the link to toggle subtask status - * - * @access public - * @param array $subtask - * @param string $redirect - * @param integer $project_id - * @return string - */ - public function toggleStatus(array $subtask, $redirect, $project_id = 0) - { - if ($project_id > 0 && ! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) { - return trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']); - } - - if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) { - return $this->helper->url->link( - trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']), - 'subtask', - 'subtaskRestriction', - array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect), - false, - 'popover task-board-popover' - ); - } - - return $this->helper->url->link( - trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']), - 'subtask', - 'toggleStatus', - array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect) - ); - } -} diff --git a/app/Helper/SubtaskHelper.php b/app/Helper/SubtaskHelper.php new file mode 100644 index 00000000..afa3c14e --- /dev/null +++ b/app/Helper/SubtaskHelper.php @@ -0,0 +1,95 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * Subtask helpers + * + * @package helper + * @author Frederic Guillot + */ +class SubtaskHelper extends Base +{ + public function getTitle(array $subtask) + { + if ($subtask['status'] == 0) { + $html = '<i class="fa fa-square-o fa-fw"></i>'; + } elseif ($subtask['status'] == 1) { + $html = '<i class="fa fa-gears fa-fw"></i>'; + } else { + $html = '<i class="fa fa-check-square-o fa-fw"></i>'; + } + + return $html.$this->helper->text->e($subtask['title']); + } + + /** + * Get the link to toggle subtask status + * + * @access public + * @param array $subtask + * @param integer $project_id + * @param boolean $refresh_table + * @return string + */ + public function toggleStatus(array $subtask, $project_id, $refresh_table = false) + { + if (! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) { + return $this->getTitle($subtask); + } + + $params = array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'refresh-table' => (int) $refresh_table); + + if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) { + return $this->helper->url->link($this->getTitle($subtask), 'SubtaskRestriction', 'popover', $params, false, 'popover'); + } + + $class = 'subtask-toggle-status '.($refresh_table ? 'subtask-refresh-table' : ''); + return $this->helper->url->link($this->getTitle($subtask), 'SubtaskStatus', 'change', $params, false, $class); + } + + public function selectTitle(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="1"', 'required', 'maxlength="255"'), $attributes); + + $html = $this->helper->form->label(t('Title'), 'title'); + $html .= $this->helper->form->text('title', $values, $errors, $attributes); + + return $html; + } + + public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="2"'), $attributes); + + $html = $this->helper->form->label(t('Assignee'), 'user_id'); + $html .= $this->helper->form->select('user_id', $users, $values, $errors, $attributes); + $html .= ' <a href="#" class="assign-me" data-target-id="form-user_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>'; + + return $html; + } + + public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="3"'), $attributes); + + $html = $this->helper->form->label(t('Original estimate'), 'time_estimated'); + $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes); + $html .= ' '.t('hours'); + + return $html; + } + + public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="4"'), $attributes); + + $html = $this->helper->form->label(t('Time spent'), 'time_spent'); + $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes); + $html .= ' '.t('hours'); + + return $html; + } +} diff --git a/app/Helper/Task.php b/app/Helper/Task.php deleted file mode 100644 index 500b8a89..00000000 --- a/app/Helper/Task.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php - -namespace Kanboard\Helper; - -use Kanboard\Core\Base; - -/** - * Task helpers - * - * @package helper - * @author Frederic Guillot - */ -class Task extends Base -{ - public function getColors() - { - return $this->color->getList(); - } - - public function recurrenceTriggers() - { - return $this->task->getRecurrenceTriggerList(); - } - - public function recurrenceTimeframes() - { - return $this->task->getRecurrenceTimeframeList(); - } - - public function recurrenceBasedates() - { - return $this->task->getRecurrenceBasedateList(); - } - - public function canRemove(array $task) - { - return $this->taskPermission->canRemoveTask($task); - } - - public function selectPriority(array $project, array $values) - { - $html = ''; - - if ($project['priority_end'] > $project['priority_start']) { - $range = range($project['priority_start'], $project['priority_end']); - $options = array_combine($range, $range); - $values += array('priority' => $project['priority_default']); - - $html .= $this->helper->form->label(t('Priority'), 'priority'); - $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="7"')); - } - - return $html; - } - - public function formatPriority(array $project, array $task) - { - $html = ''; - - if ($project['priority_end'] > $project['priority_start']) { - $html .= '<span class="task-board-priority" title="'.t('Task priority').'">'; - $html .= $task['priority'] >= 0 ? 'P'.$task['priority'] : '-P'.abs($task['priority']); - $html .= '</span>'; - } - - return $html; - } -} diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php new file mode 100644 index 00000000..4857d0ee --- /dev/null +++ b/app/Helper/TaskHelper.php @@ -0,0 +1,186 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * Task helpers + * + * @package helper + * @author Frederic Guillot + */ +class TaskHelper extends Base +{ + /** + * Local cache for project columns + * + * @access private + * @var array + */ + private $columns = array(); + + public function getColors() + { + return $this->color->getList(); + } + + public function recurrenceTriggers() + { + return $this->task->getRecurrenceTriggerList(); + } + + public function recurrenceTimeframes() + { + return $this->task->getRecurrenceTimeframeList(); + } + + public function recurrenceBasedates() + { + return $this->task->getRecurrenceBasedateList(); + } + + public function canRemove(array $task) + { + return $this->taskPermission->canRemoveTask($task); + } + + public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="3"'), $attributes); + + $html = $this->helper->form->label(t('Assignee'), 'owner_id'); + $html .= $this->helper->form->select('owner_id', $users, $values, $errors, $attributes); + $html .= ' <a href="#" class="assign-me" data-target-id="form-owner_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>'; + + return $html; + } + + public function selectCategory(array $categories, array $values, array $errors = array(), array $attributes = array(), $allow_one_item = false) + { + $attributes = array_merge(array('tabindex="4"'), $attributes); + $html = ''; + + if (! (! $allow_one_item && count($categories) === 1 && key($categories) == 0)) { + $html .= $this->helper->form->label(t('Category'), 'category_id'); + $html .= $this->helper->form->select('category_id', $categories, $values, $errors, $attributes); + } + + return $html; + } + + public function selectSwimlane(array $swimlanes, array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="5"'), $attributes); + $html = ''; + + if (! (count($swimlanes) === 1 && key($swimlanes) == 0)) { + $html .= $this->helper->form->label(t('Swimlane'), 'swimlane_id'); + $html .= $this->helper->form->select('swimlane_id', $swimlanes, $values, $errors, $attributes); + } + + return $html; + } + + public function selectColumn(array $columns, array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="6"'), $attributes); + + $html = $this->helper->form->label(t('Column'), 'column_id'); + $html .= $this->helper->form->select('column_id', $columns, $values, $errors, $attributes); + + return $html; + } + + public function selectPriority(array $project, array $values) + { + $html = ''; + + if ($project['priority_end'] > $project['priority_start']) { + $range = range($project['priority_start'], $project['priority_end']); + $options = array_combine($range, $range); + $values += array('priority' => $project['priority_default']); + + $html .= $this->helper->form->label(t('Priority'), 'priority'); + $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="7"')); + } + + return $html; + } + + public function selectScore(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="8"'), $attributes); + + $html = $this->helper->form->label(t('Complexity'), 'score'); + $html .= $this->helper->form->number('score', $values, $errors, $attributes); + + return $html; + } + + public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="9"'), $attributes); + + $html = $this->helper->form->label(t('Original estimate'), 'time_estimated'); + $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes); + $html .= ' '.t('hours'); + + return $html; + } + + public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="10"'), $attributes); + + $html = $this->helper->form->label(t('Time spent'), 'time_spent'); + $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes); + $html .= ' '.t('hours'); + + return $html; + } + + public function selectStartDate(array $values, array $errors = array(), array $attributes = array()) + { + $placeholder = date($this->config->get('application_date_format', 'm/d/Y H:i')); + $attributes = array_merge(array('tabindex="11"', 'placeholder="'.$placeholder.'"'), $attributes); + + $html = $this->helper->form->label(t('Start Date'), 'date_started'); + $html .= $this->helper->form->text('date_started', $values, $errors, $attributes, 'form-datetime'); + + return $html; + } + + public function selectDueDate(array $values, array $errors = array(), array $attributes = array()) + { + $placeholder = date($this->config->get('application_date_format', 'm/d/Y')); + $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes); + + $html = $this->helper->form->label(t('Due Date'), 'date_due'); + $html .= $this->helper->form->text('date_due', $values, $errors, $attributes, 'form-date'); + + return $html; + } + + public function formatPriority(array $project, array $task) + { + $html = ''; + + if ($project['priority_end'] > $project['priority_start']) { + $html .= '<span class="task-board-priority" title="'.t('Task priority').'">'; + $html .= $task['priority'] >= 0 ? 'P'.$task['priority'] : '-P'.abs($task['priority']); + $html .= '</span>'; + } + + return $html; + } + + public function getProgress($task) + { + if (! isset($this->columns[$task['project_id']])) { + $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); + } + + return $this->task->getProgress($task, $this->columns[$task['project_id']]); + } +} diff --git a/app/Helper/Text.php b/app/Helper/TextHelper.php index 59bfd997..e5aefdcf 100644 --- a/app/Helper/Text.php +++ b/app/Helper/TextHelper.php @@ -11,9 +11,20 @@ use Kanboard\Core\Base; * @package helper * @author Frederic Guillot */ -class Text extends Base +class TextHelper extends Base { /** + * HTML escaping + * + * @param string $value Value to escape + * @return string + */ + public function e($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + } + + /** * Markdown transformation * * @param string $text Markdown content @@ -43,6 +54,29 @@ class Text extends Base } /** + * Get the number of bytes from PHP size + * + * @param integer $val PHP size (example: 2M) + * @return integer + */ + public function phpToBytes($val) + { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } + + /** * Return true if needle is contained in the haystack * * @param string $haystack Haystack @@ -65,7 +99,7 @@ class Text extends Base public function in($id, array $listing, $default_value = '?') { if (isset($listing[$id])) { - return $this->helper->e($listing[$id]); + return $this->helper->text->e($listing[$id]); } return $default_value; diff --git a/app/Helper/Url.php b/app/Helper/UrlHelper.php index 720297cf..cad9fdef 100644 --- a/app/Helper/Url.php +++ b/app/Helper/UrlHelper.php @@ -5,12 +5,12 @@ namespace Kanboard\Helper; use Kanboard\Core\Base; /** - * Url helpers + * Url Helper * * @package helper * @author Frederic Guillot */ -class Url extends Base +class UrlHelper extends Base { private $base = ''; private $directory = ''; @@ -32,19 +32,20 @@ class Url extends Base * HTML Link tag * * @access public - * @param string $label Link label - * @param string $controller Controller name - * @param string $action Action name - * @param array $params Url parameters - * @param boolean $csrf Add a CSRF token - * @param string $class CSS class attribute - * @param boolean $new_tab Open the link in a new tab - * @param string $anchor Link Anchor + * @param string $label Link label + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param boolean $csrf Add a CSRF token + * @param string $class CSS class attribute + * @param string $title + * @param boolean $new_tab Open the link in a new tab + * @param string $anchor Link Anchor * @return string */ public function link($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false, $anchor = '') { - return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title="'.$title.'" '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>'; + return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title=\''.$title.'\' '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>'; } /** diff --git a/app/Helper/User.php b/app/Helper/UserHelper.php index 29844dfb..cbdb4af8 100644 --- a/app/Helper/User.php +++ b/app/Helper/UserHelper.php @@ -2,13 +2,15 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** * User helpers * * @package helper * @author Frederic Guillot */ -class User extends \Kanboard\Core\Base +class UserHelper extends Base { /** * Return true if the logged user as unread notifications @@ -168,7 +170,7 @@ class User extends \Kanboard\Core\Base public function avatar($email, $alt = '') { if (! empty($email) && $this->config->get('integration_gravatar') == 1) { - return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->e($alt).'" title="'.$this->helper->e($alt).'">'; + return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->text->e($alt).'" title="'.$this->helper->text->e($alt).'">'; } return ''; diff --git a/app/Model/TaskImport.php b/app/Import/TaskImport.php index e8dd1946..2abafe14 100644 --- a/app/Model/TaskImport.php +++ b/app/Import/TaskImport.php @@ -1,7 +1,8 @@ <?php -namespace Kanboard\Model; +namespace Kanboard\Import; +use Kanboard\Core\Base; use Kanboard\Core\Csv; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -9,7 +10,7 @@ use SimpleValidator\Validators; /** * Task Import * - * @package model + * @package import * @author Frederic Guillot */ class TaskImport extends Base @@ -111,7 +112,7 @@ class TaskImport extends Base } if (! empty($row['column'])) { - $values['column_id'] = $this->board->getColumnIdByTitle($this->projectId, $row['column']); + $values['column_id'] = $this->column->getColumnIdByTitle($this->projectId, $row['column']); } if (! empty($row['category'])) { @@ -126,7 +127,7 @@ class TaskImport extends Base $values['date_due'] = $this->dateParser->getTimestampFromIsoFormat($row['date_due']); } - $this->removeEmptyFields( + $this->helper->model->removeEmptyFields( $values, array('owner_id', 'creator_id', 'color_id', 'column_id', 'category_id', 'swimlane_id', 'date_due') ); diff --git a/app/Model/UserImport.php b/app/Import/UserImport.php index 0ec4e802..64300d77 100644 --- a/app/Model/UserImport.php +++ b/app/Import/UserImport.php @@ -1,16 +1,18 @@ <?php -namespace Kanboard\Model; +namespace Kanboard\Import; +use Kanboard\Model\User; use SimpleValidator\Validator; use SimpleValidator\Validators; use Kanboard\Core\Security\Role; +use Kanboard\Core\Base; use Kanboard\Core\Csv; /** * User Import * - * @package model + * @package import * @author Frederic Guillot */ class UserImport extends Base @@ -91,7 +93,7 @@ class UserImport extends Base unset($row['is_admin']); unset($row['is_manager']); - $this->removeEmptyFields($row, array('password', 'email', 'name')); + $this->helper->model->removeEmptyFields($row, array('password', 'email', 'name')); return $row; } diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 90ab1296..60e89b56 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Uredi', 'remove' => 'ukloni', 'Remove' => 'Ukloni', - 'Update' => 'Ažuriraj', 'Yes' => 'Da', 'No' => 'Ne', 'cancel' => 'odustani', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Akcije', 'Inactive' => 'Neaktivan', 'Active' => 'Aktivan', - 'Add this column' => 'Dodaj kolonu', '%d tasks on the board' => '%d zadataka na tabli', '%d tasks in total' => '%d zadataka ukupno', 'Unable to update this board.' => 'Nemogu da ažuriram ovu tablu.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Ukloni projekat', 'Edit the board for "%s"' => 'Uredi tablu za "%s"', 'All projects' => 'Svi projekti', - 'Change columns' => 'Zamijeni kolonu', 'Add a new column' => 'Dodaj novu kolonu', 'Title' => 'Naslov', 'Nobody assigned' => 'Niko nije dodijeljen', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Otvori zadatak', 'Do you really want to open this task: "%s"?' => 'Da li zaista želiš da otvoriš zadatak: "%s"?', 'Back to the board' => 'Nazad na tablu', - 'Created on %B %e, %Y at %k:%M %p' => 'Kreiran %e %B %Y o %k:%M', 'There is nobody assigned' => 'Niko nije dodijeljen!', 'Column on the board:' => 'Kolona na tabli:', - 'Status is open' => 'Status otvoren', - 'Status is closed' => 'Status zatvoren', 'Close this task' => 'Zatvori ovaj zadatak', 'Open this task' => 'Otvori ovaj zadatak', 'There is no description.' => 'Bez opisa.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'U izradi', 'Done' => 'Gotovo', 'Application version:' => 'Verzija aplikacije:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Završeno u %e %B %Y o %k:%M', - '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', - 'Date created' => 'Kreiran dana', - 'Date completed' => 'Završen dana', 'Id' => 'Id', '%d closed tasks' => '%d zatvorenih zadataka', 'No task for this project' => 'Nema dodijeljenih zadataka ovom projektu', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Uredi ovaj zadatak', 'Due Date' => 'Treba biti gotovo do dana', 'Invalid date' => 'Pogrešan datum', - 'Must be done before %B %e, %Y' => 'Mora biti gotovo prije %e %B %Y', - '%B %e, %Y' => '%e %B %Y', - '%b %e, %Y' => '%b %e, %Y', 'Automatic actions' => 'Automatske akcije', 'Your automatic action have been created successfully.' => 'Uspješno kreirana automatska akcija', 'Unable to create your automatic action.' => 'Nemoguće kreiranje automatske akcije', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Nije moguće obrisati akciju', 'Action removed successfully.' => 'Akcija obrisana', 'Automatic actions for the project "%s"' => 'Akcije za automatizaciju projekta "%s"', - 'Defined actions' => 'Definisane akcje', 'Add an action' => 'dodaj akcju', 'Event name' => 'Naziv događaja', 'Action name' => 'Naziv akcije', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Na izabrani događaj izvrši odgovarajuću akciju', 'Next step' => 'Slijedeći korak', 'Define action parameters' => 'Definiši parametre akcije', - 'Save this action' => 'Snimi akciju', 'Do you really want to remove this action: "%s"?' => 'Da li da obrišem akciju "%s"?', 'Remove an automatic action' => 'Obriši automatsku akciju', 'Assign the task to a specific user' => 'Dodijeli zadatak određenom korisniku', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Dodeli boju korisniku', 'Column title' => 'Naslov kolone', 'Position' => 'Pozicija', - 'Move Up' => 'Podigni', - 'Move Down' => 'Spusti', 'Duplicate to another project' => 'Dupliciraj u drugi projekat', 'Duplicate' => 'Dupliciraj', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Komentar je uspješno obrisan.', 'Unable to remove this comment.' => 'Neuspješno brisanje komentara.', 'Do you really want to remove this comment?' => 'Da li zaista želiš obrisati ovaj komentar?', - 'Only administrators or the creator of the comment can access to this page.' => 'Samo administrator i kreator komentara mogu pristupiti ovoj stranici.', 'Current password for the user "%s"' => 'Trenutna šifra korisnika "%s"', 'The current password is required' => 'Trenutna šifra je obavezna', 'Wrong password' => 'Pogrešna šifra', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Vanjska autentikacija nije uspostavljena', 'Your external account is linked to your profile successfully.' => 'Uspješno uspostavljena vanjska autentikacija', 'Email' => 'E-mail', - 'Link my Google Account' => 'Poveži sa Google nalogom', - 'Unlink my Google Account' => 'Ukini vezu sa Google nalogom', - 'Login with my Google Account' => 'Prijavi se preko Google naloga', - 'Project not found.' => 'Projekat nije pronađen.', 'Task removed successfully.' => 'Zadatak uspješno uklonjen.', 'Unable to remove this task.' => 'Nemoguće uklanjanje zadatka.', 'Remove a task' => 'Ukloni zadatak', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimalna veličina: ', 'Unable to upload the file.' => 'Nije moguće snimiti fajl.', 'Display another project' => 'Prikaži drugi projekat', - 'Login with my Github Account' => 'Prijavi me s mojim Github korisničkim računom', - 'Link my Github Account' => 'Poveži s mojim Github korisničkim računom', - 'Unlink my Github Account' => 'Odbavi vez s mojim Github korisničkim računom', 'Created by %s' => 'Kreirao %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Posljednja izmjena %e %B %Y o %k:%M', 'Tasks Export' => 'Izvoz zadataka', 'Tasks exportation for "%s"' => 'Izvoz zadataka za "%s"', 'Start Date' => 'Početni datum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Želim obavještenja samo za ove projekte:', 'view the task on Kanboard' => 'Pregledaj zadatke', 'Public access' => 'Javni pristup', - 'User management' => 'Upravljanje korisnicima', 'Active tasks' => 'Aktivni zadaci', 'Disable public access' => 'Zabrani javni pristup', 'Enable public access' => 'Dozvoli javni pristup', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Promijeni šifru', 'Password modification' => 'Izmjena šifre', 'External authentications' => 'Vanjske autentikacije', - 'Google Account' => 'Google korisnički račun', - 'Github Account' => 'Github korisnički račun', 'Never connected.' => 'Bez konekcija.', - 'No account linked.' => 'Bez povezanih korisničkih računa.', - 'Account linked.' => 'Korisnički račun povezan.', 'No external authentication enabled.' => 'Bez omogućenih vanjskih autentikacija.', 'Password modified successfully.' => 'Uspješna izmjena šifre.', 'Unable to change the password.' => 'Nije moguće izmijeniti šifru.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Izmijene izvršioca bazirano na vanjskom korisničkom imenu', 'Change the category based on an external label' => 'Izmijene kategorije bazirano na vanjskoj etiketi', 'Reference' => 'Referenca', - 'Reference: %s' => 'Referenca: %s', 'Label' => 'Etiketa', 'Database' => 'Baza', 'About' => 'O', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frekvencija u sekundama (60 sekundi je uobičajeno)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvencija u sekundama (0 je onemogućeno u budućnosti, 10 sekundi je uobičajeno)', 'Application URL' => 'URL aplikacje', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Primjer: http://example.kanboard.net/ (koristi se u obavještenjima putem email-a)', 'Token regenerated.' => 'Token regenerisan.', 'Date format' => 'Format datuma', 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primjer: "%s", "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Ovaj projekat je privatan', 'Type here to create a new sub-task' => 'Piši ovdje za kreiranje novog pod-zadatka', 'Add' => 'Dodaj', - 'Estimated time: %s hours' => 'Procijenjeno vrijeme: %s sati', - 'Time spent: %s hours' => 'Utrošeno vrijeme: %s sati', - 'Started on %B %e, %Y' => 'Započeto dana %e %B %Y', 'Start date' => 'Datum početka', 'Time estimated' => 'Procijenjeno vrijeme', 'There is nothing assigned to you.' => 'Ništa vam nije dodijeljeno', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Da li zaista želiš ukloniti ovu swimline traku: "%s"?', 'Inactive swimlanes' => 'Neaktivne swimline trake', 'Remove a swimlane' => 'Ukloni swimline traku', - 'Rename' => 'Preimenuj', 'Show default swimlane' => 'Prikaži podrazumijevanu swimline traku', 'Swimlane modification for the project "%s"' => 'Izmjene swimline trake za projekat "%s"', 'Swimlane not found.' => 'Swimline traka nije pronađena.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimline trake', 'Swimlane updated successfully.' => 'Swimline traka uspjeno ažurirana.', 'The default swimlane have been updated successfully.' => 'Podrazumijevana swimline traka uspješno ažurirana.', - 'Unable to create your swimlane.' => 'Nemoguće kreirati swimline traku.', 'Unable to remove this swimlane.' => 'Nemoguće ukloniti swimline traku.', 'Unable to update this swimlane.' => 'Nemoguće ažurirati swimline traku.', 'Your swimlane have been created successfully.' => 'Swimline traka je uspješno kreirana.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Ovaj zadatak', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%b %e', 'Expand tasks' => 'Proširi zadatke', 'Collapse tasks' => 'Skupi zadatke', 'Expand/collapse tasks' => 'Proširi/skupi zadatke', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Prečice tastature', 'Open board switcher' => 'Otvori prekidače ploče', 'Application' => 'Aplikacija', - 'since %B %e, %Y at %k:%M %p' => 'od %B %e, %Y do %k:%M %p', 'Compact view' => 'Kompaktan pregled', 'Horizontal scrolling' => 'Horizontalno listanje', 'Compact/wide view' => 'Skupi/raširi pregled', 'No results match:' => 'Nema rezultata:', 'Currency' => 'Valuta', - 'Files' => 'Fajlovi', - 'Images' => 'Slike', 'Private project' => 'Privatni projekat', 'AUD - Australian Dollar' => 'AUD - Australijski dolar', 'CAD - Canadian Dollar' => 'CAD - Kanadski dolar', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Testiraj svoj uređaj', 'Assign a color when the task is moved to a specific column' => 'Dodijeli boju kada je zadatak pomjeren u odabranu kolonu', '%s via Kanboard' => '%s uz pomoć Kanboard-a', - 'uploaded by: %s' => 'dodano od strane: %s', - 'uploaded on: %s' => 'dodano na: %s', - 'size: %s' => 'veličina: %s', 'Burndown chart for "%s"' => 'Grafikon izgaranja za "%s"', 'Burndown chart' => 'Grafikon izgaranja', 'This chart show the task complexity over the time (Work Remaining).' => 'Ovaj grafikon pokazuje kompleksnost zadatka u vremenu (Preostalo vremena)', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Zadatak ne može biti povezan sa samim sobom', 'The exact same link already exists' => 'Ista veza već postoji', 'Recurrent task is scheduled to be generated' => 'Ponavljajući zadatak je pripremljen da bude kreiran', - 'Recurring information' => 'Informacije o ponavljanju', 'Score' => 'Uspjeh', 'The identifier must be unique' => 'Identifikator mora biti jedinstven', 'This linked task id doesn\'t exists' => 'Povezani ID zadatka ne postoji', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Utrošeno vrijeme je promijenjeno: %sh', 'Time estimated changed: %sh' => 'Očekivano vrijeme je promijenjeno: %sh', 'The field "%s" have been updated' => 'Polje "%s" je ažurirano', - 'The description have been modified' => 'Promijenjen opis', + 'The description has been modified' => 'Promijenjen opis', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Da li zaista želiš zatvoriti zadatak "%s" kao i sve pod-zadatke?', - 'Swimlane: %s' => 'Swimline traka: %s', 'I want to receive notifications for:' => 'Želim dobijati obavještenja za:', 'All tasks' => 'Sve zadatke', 'Only for tasks assigned to me' => 'Samo za zadatke na kojima sam izvršilac', 'Only for tasks created by me' => 'Samo za zadatke koje sam ja napravio', 'Only for tasks created by me and assigned to me' => 'Samo za zadatke koje sam ja napravio i na kojima sam izvršilac', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', - 'New due date: %B %e, %Y' => 'Novi datum završetka: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Početni datum promijenjen: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%Y-%%m-%%d' => '%%Y-%%m-%%d', 'Total for all columns' => 'Ukupno za sve kolone', 'You need at least 2 days of data to show the chart.' => 'Da bi se prikazao ovaj grafik potrebni su podaci iz najmanje posljednja dva dana.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Bez izvršioca', 'View advanced search syntax' => 'Vidi naprednu sintaksu pretrage', 'Overview' => 'Opšti pregled', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Pregle Table/Kalendara/Liste', 'Switch to the board view' => 'Promijeni da vidim tablu', 'Switch to the calendar view' => 'Promijeni da vidim kalendar', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Ovaj grafik pokazuje prosjek vremena vođenja i vremenskog ciklusa za posljednjih %d zadataka tokom vremena.', 'Average time into each column' => 'Prosječno vrijeme u svakoj koloni', 'Lead and cycle time' => 'Vrijeme vođenja i vremenski ciklus', - 'Google Authentication' => 'Google autentifikacija', - 'Help on Google authentication' => 'Pomoć na Google autentifikacija', - 'Github Authentication' => 'Github autentifikacija', - 'Help on Github authentication' => 'Pomoć na Github autentifikacija', 'Lead time: ' => 'Vrijeme vođenja: ', 'Cycle time: ' => 'Vremenski ciklus: ', 'Time spent into each column' => 'Utrošeno vrijeme u svakoj koloni', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Ako zadatak nije zatvoren trenutno vrijeme je iskorišteno umjesto datuma završetka.', 'Set automatically the start date' => 'Automatski postavi početno vrijeme', 'Edit Authentication' => 'Uredi autentifikaciju', - 'Google Id' => 'Google Id', - 'Github Id' => 'Github Id', 'Remote user' => 'Vanjski korisnik', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Vanjski korisnik ne čuva šifru u Kanboard bazi, npr: LDAP, Google i Github korisnički računi.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Ako ste označili kvadratić "Zabrani prijavnu formu", unos pristupnih podataka u prijavnoj formi će biti ignorisan.', 'New remote user' => 'Novi vanjski korisnik', 'New local user' => 'Novi lokalni korisnik', 'Default task color' => 'Podrazumijevana boja zadatka', - 'Hide sidebar' => 'Sakri bočnu traku', - 'Expand sidebar' => 'Proširi bočnu traku', 'This feature does not work with all browsers.' => 'Ovaj funkcionalnost ne radi na svim internet pretraživačima.', 'There is no destination project available.' => 'Nema definisanog odredišta za projekat.', 'Trigger automatically subtask time tracking' => 'Okidač za automatsko vremensko praćenje za pod-zadatke', @@ -901,7 +840,6 @@ return array( 'open file' => 'otvori fajl', 'End date' => 'Datum završetka', 'Users overview' => 'Opšti pregled korisnika', - 'Managers' => 'Menadžeri', 'Members' => 'Članovi', 'Shared project' => 'Dijeljeni projekti', 'Project managers' => 'Menadžeri projekta', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Datum završetka:', 'There is no start date or end date for this project.' => 'Nema početnog ili krajnjeg datuma za ovaj projekat.', 'Projects Gantt chart' => 'Gantogram projekata', - 'Start date: %s' => 'Početni datum: %s', - 'End date: %s' => 'Datum završetka: %s', 'Link type' => 'Tip veze', 'Change task color when using a specific task link' => 'Promijeni boju zadatka kada se koristi određena veza na zadatku', 'Task link creation or modification' => 'Veza na zadatku je napravljena ili izmijenjena', - 'Login with my Gitlab Account' => 'Prijava s mojim Gitlab korisničkim računom', 'Milestone' => 'Prekretnica', - 'Gitlab Authentication' => 'Gitlab autentifikacija', - 'Help on Gitlab authentication' => 'Pomoć na Gitlab autentifikacija', - 'Gitlab Id' => 'Gitlab Id', - 'Gitlab Account' => 'Gitlab korisnički račun', - 'Link my Gitlab Account' => 'Veza s mojim Gitlab korisničkim računom', - 'Unlink my Gitlab Account' => 'Prekini vezu s mojim Gitlab korisničkim računom', 'Documentation: %s' => 'Dokumentacija: %s', 'Switch to the Gantt chart view' => 'Promijeni u gantogram pregled', 'Reset the search/filter box' => 'Vrati na početno pretragu/filtere', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index 83d88f35..8c808967 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Editovat', 'remove' => 'odstranit', 'Remove' => 'Odstranit', - 'Update' => 'Akualizovat', 'Yes' => 'Ano', 'No' => 'Ne', 'cancel' => 'Zrušit', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Akce', 'Inactive' => 'Neaktivní', 'Active' => 'Aktivní', - 'Add this column' => 'Přidat sloupec', '%d tasks on the board' => '%d úkolů na nástěnce', '%d tasks in total' => '%d úkolů celkem', 'Unable to update this board.' => 'Nástěnku není možné aktualizovat', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Vyjmout projekt', 'Edit the board for "%s"' => 'Editace nástěnky pro "%s" ', 'All projects' => 'Všechny projekty', - 'Change columns' => 'Změna sloupců', 'Add a new column' => 'Přidat nový sloupec', 'Title' => 'Název', 'Nobody assigned' => 'Nepřiřazena žádná osoba', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Otevřít úkol', 'Do you really want to open this task: "%s"?' => 'Opravdu chcete znovuotevřít tento úkol: "%s"?', 'Back to the board' => 'Zpět na nástěnku', - 'Created on %B %e, %Y at %k:%M %p' => 'Vytvořeno dne %d.%m.%Y v čase %H:%M', 'There is nobody assigned' => 'Není přiřazeno žádnému uživateli', 'Column on the board:' => 'Sloupec:', - 'Status is open' => 'Status je otevřený', - 'Status is closed' => 'Status je uzavřený', 'Close this task' => 'Uzavřít úkol', 'Open this task' => 'Aufgabe wieder öffnen', 'There is no description.' => 'Bez popisu', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'V řešení', 'Done' => 'Dokončeno', 'Application version:' => 'Verze:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Dokončeno %d.%m.%Y v %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y v %H:%M', - 'Date created' => 'Datum vytvoření', - 'Date completed' => 'Datum dokončení', 'Id' => 'ID', '%d closed tasks' => '%d dokončených úkolů', 'No task for this project' => 'Tento projekt nemá žádné úkoly', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Editace úkolu', 'Due Date' => 'Datum splnění', 'Invalid date' => 'Neplatné datum', - 'Must be done before %B %e, %Y' => 'Musí být dokončeno do %d.%m.%Y ', - '%B %e, %Y' => '%d.%m.%Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'Automaticky vykonávané akce', 'Your automatic action have been created successfully.' => 'Vaše akce byla úspěšně vytvořena.', 'Unable to create your automatic action.' => 'Vaší akci nebylo možné vytvořit.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Tuto akci nelze odstranit.', 'Action removed successfully.' => 'Akce byla úspěšně odstraněna.', 'Automatic actions for the project "%s"' => 'Automaticky vykonávané akce pro projekt "%s"', - 'Defined actions' => 'Definované akce', 'Add an action' => 'Přidat akci', 'Event name' => 'Název události', 'Action name' => 'Název akce', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Kdykoliv se vybraná událost objeví, vykonat odpovídající akci.', 'Next step' => 'Další krok', 'Define action parameters' => 'Definovat parametry akce', - 'Save this action' => 'Uložit akci', 'Do you really want to remove this action: "%s"?' => 'Skutečně chcete odebrat tuto akci: "%s"?', 'Remove an automatic action' => 'Odebrat automaticky prováděnou akci', 'Assign the task to a specific user' => 'Přiřadit tento úkol konkrétnímu uživateli', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Přiřadit barvu konkrétnímu uživateli', 'Column title' => 'Název sloupce', 'Position' => 'Pozice', - 'Move Up' => 'Posunout nahoru', - 'Move Down' => 'Posunout dolu', 'Duplicate to another project' => 'Vytvořit kopii v jiném projektu', 'Duplicate' => 'Vytvořit kopii', 'link' => 'Link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Komentář byl smazán.', 'Unable to remove this comment.' => 'Komentář nelze odebrat.', 'Do you really want to remove this comment?' => 'Skutečně chcete odebrat tento komentář?', - 'Only administrators or the creator of the comment can access to this page.' => 'Přístup k této stránce mají pouze administrátoři nebo vlastníci komentáře.', 'Current password for the user "%s"' => 'Aktuální heslo pro uživatele "%s"', 'The current password is required' => 'Heslo je vyžadováno', 'Wrong password' => 'Neplatné heslo', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'E-Mail', - 'Link my Google Account' => 'Propojit s Google účtem', - 'Unlink my Google Account' => 'Odpojit Google účet', - 'Login with my Google Account' => 'Přihlášení pomocí Google účtu', - 'Project not found.' => 'Projekt nebyl nalezen.', 'Task removed successfully.' => 'Úkol byl úspěšně odebrán.', 'Unable to remove this task.' => 'Tento úkol nelze odebrat.', 'Remove a task' => 'Odebrat úkol', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maximální velikost: ', 'Unable to upload the file.' => 'Soubor nelze nahrát.', 'Display another project' => 'Zobrazit jiný projekt', - // 'Login with my Github Account' => '', - // 'Link my Github Account' => '', - // 'Unlink my Github Account' => '', 'Created by %s' => 'Vytvořeno uživatelem %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslední úprava dne %d.%m.%Y v čase %H:%M', 'Tasks Export' => 'Export úkolů', 'Tasks exportation for "%s"' => 'Export úkolů pro "%s"', 'Start Date' => 'Počáteční datum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Přeji si dostávat upozornění pouze pro následující projekty:', 'view the task on Kanboard' => 'Zobrazit úkol na Kanboard', 'Public access' => 'Veřejný přístup', - 'User management' => 'Správa uživatelů', 'Active tasks' => 'Aktivní úkoly', 'Disable public access' => 'Zakázat veřejný přístup', 'Enable public access' => 'Povolit veřejný přístup', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Změnit heslo', 'Password modification' => 'Změna hesla', 'External authentications' => 'Vzdálená autorizace', - 'Google Account' => 'Google účet', - 'Github Account' => 'github účet', 'Never connected.' => 'Zatím nikdy nespojen.', - 'No account linked.' => 'Žádné propojení účtu.', - 'Account linked.' => 'Propojení účtu', 'No external authentication enabled.' => 'Není povolena žádná vzdálená autorizace.', 'Password modified successfully.' => 'Heslo bylo úspěšně změněno.', 'Unable to change the password.' => 'Nelze změnit heslo.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Změna přiřazení uživatele závislá na externím uživateli', 'Change the category based on an external label' => 'Změna kategorie závislá na externím popisku', 'Reference' => 'Reference', - 'Reference: %s' => 'Reference: %s', // 'Label' => '', 'Database' => 'Datenbank', 'About' => 'O projektu', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frekvence v sekundách (60 sekund ve výchozím nastavení)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvence v sekundách (0 pro zákaz této vlastnosti, 10 sekund ve výchozím nastavení)', 'Application URL' => 'URL aplikace', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ukázka: http://example.kanboard.net/ (použit v emailovém upozornění)', 'Token regenerated.' => 'Token byl opětovně generován.', 'Date format' => 'Formát datumu', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Tento projekt je soukromuý', 'Type here to create a new sub-task' => 'Uveďte zde pro vytvoření nového dílčího úkolu', 'Add' => 'Přidat', - 'Estimated time: %s hours' => 'Předpokládaný čas: %s hodin', - 'Time spent: %s hours' => 'Doba trvání: %s hodin', - 'Started on %B %e, %Y' => 'Zahájeno %B %e %Y', 'Start date' => 'Počáteční datum', 'Time estimated' => 'Odhadovaný čas', 'There is nothing assigned to you.' => 'Nemáte přiřazenou žádnou položku.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', 'Inactive swimlanes' => 'Inaktive Swimlane', 'Remove a swimlane' => 'Odstranit swimlane', - 'Rename' => 'Přejmenovat', 'Show default swimlane' => 'Standard Swimlane anzeigen', 'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "%s"', 'Swimlane not found.' => 'Swimlane nicht gefunden', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.', 'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.', - 'Unable to create your swimlane.' => 'Es ist nicht möglich die Swimlane zu erstellen.', 'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.', 'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.', 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Tento úkol', '<1h' => '<1h', '%dh' => '%dh', - // '%b %e' => '', 'Expand tasks' => 'Rozpbalit úkoly', 'Collapse tasks' => 'Sbalit úkoly', 'Expand/collapse tasks' => 'Rozbalit / sbalit úkoly', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Klávesnicové zkratky', 'Open board switcher' => 'Otevřít přepínač nástěnek', 'Application' => 'Aplikace', - 'since %B %e, %Y at %k:%M %p' => 'dne %d.%m.%Y v čase %H:%M', 'Compact view' => 'Kompaktní zobrazení', 'Horizontal scrolling' => 'Horizontální rolování', 'Compact/wide view' => 'Kompaktní/plné zobrazení', 'No results match:' => 'Žádná shoda:', 'Currency' => 'Měna', - 'Files' => 'Soubory', - 'Images' => 'Obrázky', 'Private project' => 'Soukromý projekt', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Test Vašeho zařízení', 'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'Nahráno uživatelem: %s', - 'uploaded on: %s' => 'Nahráno dne: %s', - 'size: %s' => 'Velikost: %s', 'Burndown chart for "%s"' => 'Burndown-Chart für "%s"', 'Burndown chart' => 'Burndown-Chart', 'This chart show the task complexity over the time (Work Remaining).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh', 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh', 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert', - 'The description have been modified' => 'Die Beschreibung wurde geändert', + 'The description has been modified' => 'Die Beschreibung wurde geändert', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)', - // 'Swimlane: %s' => '', 'I want to receive notifications for:' => 'Chci dostávat upozornění na:', 'All tasks' => 'Všechny úkoly', 'Only for tasks assigned to me' => 'pouze pro moje úkoly', 'Only for tasks created by me' => 'pouze pro mnou vytvořené úkoly', 'Only for tasks created by me and assigned to me' => 'pouze pro mnou vytvořené a mě přiřazené úkoly', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - 'New due date: %B %e, %Y' => 'Neues Ablaufdatum: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Neues Beginndatum: %B %e, %Y', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', 'Total for all columns' => 'S', 'You need at least 2 days of data to show the chart.' => 'Potřebujete nejméně data ze dvou dnů pro zobrazení grafu', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Nepřiřazené', 'View advanced search syntax' => 'Zobrazit syntaxi rozšířeného vyhledávání', 'Overview' => 'Přehled', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Nástěnka/Kalendář/Zobrazení seznamu', 'Switch to the board view' => 'Přepnout na nástěnku', 'Switch to the calendar view' => 'Přepnout na kalendář', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Graf ukazuje průměrnou dodací lhůtu a dobu cyklu pro posledních %d úkolů v průběhu času', 'Average time into each column' => 'Průměrná doba v každé fázi', 'Lead and cycle time' => 'Dodací lhůta a doba cyklu', - 'Google Authentication' => 'Ověřování pomocí služby Google', - 'Help on Google authentication' => 'Nápověda k ověřování pomocí služby Google', - 'Github Authentication' => 'Ověřování pomocí služby Github', - 'Help on Github authentication' => 'Nápověda k ověřování pomocí služby Github', 'Lead time: ' => 'Dodací lhůta: ', 'Cycle time: ' => 'Doba cyklu: ', 'Time spent into each column' => 'Čas strávený v každé fázi', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Jestliže není úkol uzavřen, místo termínu dokončení je použit aktuální čas.', 'Set automatically the start date' => 'Nastavit automaticky počáteční datum', 'Edit Authentication' => 'Upravit ověřování', - 'Google Id' => 'Google ID', - 'Github Id' => 'Github ID', 'Remote user' => 'Vzdálený uživatel', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Hesla vzdáleným uživatelům se neukládají do databáze Kanboard. Naříklad: LDAP, Google a Github účty.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Pokud zaškrtnete políčko "Zakázat přihlašovací formulář", budou pověření zadané do přihlašovacího formuláře ignorovány.', 'New remote user' => 'Nový vzdálený uživatel', 'New local user' => 'Nový lokální uživatel', 'Default task color' => 'Výchozí barva úkolu', - 'Hide sidebar' => 'Schovat postranní panel', - 'Expand sidebar' => 'Rozbalit postranní panel', 'This feature does not work with all browsers.' => 'Tato funkcionalita nefunguje ve všech prohlížečích.', 'There is no destination project available.' => 'Není dostupný žádný cílový projekt.', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 7a82bc1e..5a5606af 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Rediger', 'remove' => 'fjern', 'Remove' => 'Fjern', - 'Update' => 'Opdater', 'Yes' => 'Ja', 'No' => 'Nej', 'cancel' => 'annuller', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Handlinger', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', - 'Add this column' => 'Tilføj denne kolonne', '%d tasks on the board' => '%d Opgaver på boardet', '%d tasks in total' => '%d Opgaver i alt', 'Unable to update this board.' => 'Ikke muligt at opdatere dette board', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Fjern projekt', 'Edit the board for "%s"' => 'Rediger boardet for "%s"', 'All projects' => 'Alle Projekter', - 'Change columns' => 'Ændre kolonner', 'Add a new column' => 'Tilføj en ny kolonne', 'Title' => 'Titel', 'Nobody assigned' => 'Ingen ansvarlig', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Åben en opgave', 'Do you really want to open this task: "%s"?' => 'Vil du virkelig åbne denne opgave: "%s"?', 'Back to the board' => 'Tilbage til boardet', - 'Created on %B %e, %Y at %k:%M %p' => 'Oprettet %d.%m.%Y - %H:%M', 'There is nobody assigned' => 'Der er ingen tilføjet', 'Column on the board:' => 'Kolonne:', - 'Status is open' => 'Status er åbnet', - 'Status is closed' => 'Status er lukket', 'Close this task' => 'Luk denne opgave', 'Open this task' => 'Åben denne opgave', 'There is no description.' => 'Der er ingen beskrivning.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Igangværende', 'Done' => 'Færdig', 'Application version:' => 'Version:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Fuldført %d.%m.%Y - %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y - %H:%M', - 'Date created' => 'Dato for oprettelse', - 'Date completed' => 'Dato for fuldført', 'Id' => 'ID', '%d closed tasks' => '%d lukket opgavet', 'No task for this project' => 'Ingen opgaver i dette projekt', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Rediger denne opgave', 'Due Date' => 'Forfaldsdato', 'Invalid date' => 'Ugyldig dato', - 'Must be done before %B %e, %Y' => 'Skal være fuldført inden %d.%m.%Y', - '%B %e, %Y' => '%d.%m.%Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'Automatiske handlinger', 'Your automatic action have been created successfully.' => 'Din automatiske handling er oprettet.', 'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke oprettes.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Handlingen kunne ikke fjernes.', 'Action removed successfully.' => 'Handlingen er fjernet.', 'Automatic actions for the project "%s"' => 'Automatiske handlinger for projektet "%s"', - 'Defined actions' => 'Defineret handlinger', 'Add an action' => 'Tilføj en handling', 'Event name' => 'Begivenhed', 'Action name' => 'Handling', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Når den valgte begivenhed opstår, udfør den tilsvarende handling.', 'Next step' => 'Næste', 'Define action parameters' => 'Definer Handlingsparametre', - 'Save this action' => 'Gem denne handling', 'Do you really want to remove this action: "%s"?' => 'Vil du virkelig slette denne handling: "%s"?', 'Remove an automatic action' => 'Fjern en automatisk handling', 'Assign the task to a specific user' => 'Tildel opgaven til en bestem bruger', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Tildel en farve til en bestemt bruger', 'Column title' => 'Kolonne titel', 'Position' => 'Position', - 'Move Up' => 'Ryk op', - 'Move Down' => 'Ryk ned', 'Duplicate to another project' => 'Kopier til et andet projekt', 'Duplicate' => 'Kopier', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Kommentaren blev fjernet.', 'Unable to remove this comment.' => 'Kommentaren kunne ikke fjernes.', 'Do you really want to remove this comment?' => 'Vil du virkelig fjerne denne kommentar?', - 'Only administrators or the creator of the comment can access to this page.' => 'Kun administratore eller brugeren, som har oprettet kommentaren har adgang til denne side.', 'Current password for the user "%s"' => 'Aktuelle adgangskode for brugeren "%s"', 'The current password is required' => 'Den aktuelle adgangskode er krævet', 'Wrong password' => 'Forkert adgangskode', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'E-Mail', - 'Link my Google Account' => 'Forbind min Google-konto', - 'Unlink my Google Account' => 'Fjern forbindelsen til min Google-konto', - 'Login with my Google Account' => 'Login med min Google-konto', - 'Project not found.' => 'Projekt ikke fundet.', 'Task removed successfully.' => 'Opgaven er fjernet.', 'Unable to remove this task.' => 'Opgaven kunne ikke fjernes.', 'Remove a task' => 'Fjern en opgave', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimum størrelse: ', 'Unable to upload the file.' => 'Filen kunne ikke uploades.', 'Display another project' => 'Vis et andet projekt...', - 'Login with my Github Account' => 'Login med min Github-konto', - 'Link my Github Account' => 'Forbind min Github-konto', - 'Unlink my Github Account' => 'Fjern forbindelsen til min Github-konto', 'Created by %s' => 'Oprettet af %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Sidst redigeret %d.%m.%Y - %H:%M', 'Tasks Export' => 'Opgave eksport', 'Tasks exportation for "%s"' => 'Opgave eksport for "%s"', 'Start Date' => 'Start-dato', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Jeg vil kun have notifikationer for disse projekter:', 'view the task on Kanboard' => 'se opgaven på Kanboard', 'Public access' => 'Offentlig adgang', - 'User management' => 'Brugerstyring', 'Active tasks' => 'Aktive opgaver', 'Disable public access' => 'Deaktiver offentlig adgang', 'Enable public access' => 'Aktivér offentlig adgang', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Skift adgangskode', 'Password modification' => 'Adgangskode ændring', 'External authentications' => 'Ekstern autentificering', - 'Google Account' => 'Google-konto', - 'Github Account' => 'Github-konto', 'Never connected.' => 'Aldrig forbundet.', - 'No account linked.' => 'Ingen kontoer forfundet.', - 'Account linked.' => 'Konto forbundet.', 'No external authentication enabled.' => 'Ingen eksterne autentificering aktiveret.', 'Password modified successfully.' => 'Adgangskode ændret.', 'Unable to change the password.' => 'Adgangskoden kunne ikke ændres.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Skift den ansvarlige baseret på et eksternt brugernavn', 'Change the category based on an external label' => 'Skift kategorien baseret på en ekstern label', 'Reference' => 'Reference', - 'Reference: %s' => 'Reference: %s', 'Label' => 'Label', 'Database' => 'Database', 'About' => 'Om', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frekevens i sekunder (60 sekunder som standard)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (0 for at deaktivere denne funktion, 10 sekunder som standard)', 'Application URL' => 'Applikation URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Eksempel: http://example.kanboard.net/ (bruges til email notifikationer)', 'Token regenerated.' => 'Token regenereret.', 'Date format' => 'Dato format', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er altid accepteret, eksempelvis: "%s" og "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Dette projekt er privat', 'Type here to create a new sub-task' => 'Skriv her for at tilføje en ny under-opgave', 'Add' => 'Tilføj', - 'Estimated time: %s hours' => 'Estimeret tid: %s timer', - 'Time spent: %s hours' => 'Tid brugt: %s timer', - 'Started on %B %e, %Y' => 'Startet %d.%m.%Y ', 'Start date' => 'Start dato', 'Time estimated' => 'Tid estimeret', 'There is nothing assigned to you.' => 'Der er ingenting tildelt til dig.', @@ -529,7 +493,6 @@ return array( // 'Do you really want to remove this swimlane: "%s"?' => '', // 'Inactive swimlanes' => '', // 'Remove a swimlane' => '', - // 'Rename' => '', // 'Show default swimlane' => '', // 'Swimlane modification for the project "%s"' => '', // 'Swimlane not found.' => '', @@ -537,7 +500,6 @@ return array( // 'Swimlanes' => '', // 'Swimlane updated successfully.' => '', // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', // 'Unable to remove this swimlane.' => '', // 'Unable to update this swimlane.' => '', // 'Your swimlane have been created successfully.' => '', @@ -612,7 +574,6 @@ return array( // 'This task' => '', // '<1h' => '', // '%dh' => '', - // '%b %e' => '', // 'Expand tasks' => '', // 'Collapse tasks' => '', // 'Expand/collapse tasks' => '', @@ -622,14 +583,11 @@ return array( // 'Keyboard shortcuts' => '', // 'Open board switcher' => '', // 'Application' => '', - // 'since %B %e, %Y at %k:%M %p' => '', // 'Compact view' => '', // 'Horizontal scrolling' => '', // 'Compact/wide view' => '', // 'No results match:' => '', // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', // 'Private project' => '', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', // 'I want to receive notifications for:' => '', // 'All tasks' => '', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( // 'Not assigned' => '', // 'View advanced search syntax' => '', // 'Overview' => '', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 80e77f90..864d3a94 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Bearbeiten', 'remove' => 'Entfernen', 'Remove' => 'Entfernen', - 'Update' => 'Aktualisieren', 'Yes' => 'Ja', 'No' => 'Nein', 'cancel' => 'Abbrechen', @@ -26,7 +25,7 @@ return array( 'Pink' => 'Pink', 'Teal' => 'Türkis', 'Cyan' => 'Cyan', - 'Lime' => 'Giftgrün', + 'Lime' => 'Limette', 'Light Green' => 'Hellgrün', 'Amber' => 'Bernstein', 'Save' => 'Speichern', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Aktionen', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', - 'Add this column' => 'Diese Spalte hinzufügen', '%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand', '%d tasks in total' => '%d Aufgaben insgesamt', 'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Projekt löschen', 'Edit the board for "%s"' => 'Pinnwand für "%s" bearbeiten', 'All projects' => 'Alle Projekte', - 'Change columns' => 'Spalten ändern', 'Add a new column' => 'Neue Spalte hinzufügen', 'Title' => 'Titel', 'Nobody assigned' => 'Nicht zugeordnet', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Öffne eine Aufgabe', 'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: "%s"?', 'Back to the board' => 'Zurück zur Pinnwand', - 'Created on %B %e, %Y at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M', 'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen', 'Column on the board:' => 'Spalte:', - 'Status is open' => 'Status ist geöffnet', - 'Status is closed' => 'Status ist geschlossen', 'Close this task' => 'Aufgabe schließen', 'Open this task' => 'Aufgabe wieder öffnen', 'There is no description.' => 'Keine Beschreibung vorhanden.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'In Arbeit', 'Done' => 'Erledigt', 'Application version:' => 'Version:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y um %H:%M', - 'Date created' => 'Erstellt am', - 'Date completed' => 'Abgeschlossen am', 'Id' => 'ID', '%d closed tasks' => '%d abgeschlossene Aufgaben', 'No task for this project' => 'Keine Aufgaben in diesem Projekt', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Aufgabe bearbeiten', 'Due Date' => 'Fällig am', 'Invalid date' => 'Ungültiges Datum', - 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden', - '%B %e, %Y' => '%d.%m.%Y', - '%b %e, %Y' => '%d.%m.%Y', 'Automatic actions' => 'Automatische Aktionen', 'Your automatic action have been created successfully.' => 'Die automatische Aktion wurde erfolgreich erstellt.', 'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.', 'Action removed successfully.' => 'Aktion erfolgreich gelöscht.', 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"', - 'Defined actions' => 'Definierte Aktionen', 'Add an action' => 'Aktion hinzufügen', 'Event name' => 'Ereignisname', 'Action name' => 'Aktionsname', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Wenn das gewählte Ereignis eintritt, führe die zugehörige Aktion aus.', 'Next step' => 'Weiter', 'Define action parameters' => 'Aktionsparameter definieren', - 'Save this action' => 'Aktion speichern', 'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: "%s"?', 'Remove an automatic action' => 'Löschen einer automatischen Aktion', 'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen', 'Column title' => 'Spaltentitel', 'Position' => 'Position', - 'Move Up' => 'nach oben', - 'Move Down' => 'nach unten', 'Duplicate to another project' => 'In ein anderes Projekt duplizieren', 'Duplicate' => 'Duplizieren', 'link' => 'Link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.', 'Unable to remove this comment.' => 'Löschen des Kommentars nicht möglich.', 'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?', - 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars haben Zugriff auf diese Seite.', 'Current password for the user "%s"' => 'Aktuelles Passwort des Benutzers "%s"', 'The current password is required' => 'Das aktuelle Passwort wird benötigt', 'Wrong password' => 'Falsches Passwort', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Externe Authentifizierung fehlgeschlagen', 'Your external account is linked to your profile successfully.' => 'Dein externer Account wurde erfolgreich mit deinem Profil verbunden', 'Email' => 'E-Mail', - 'Link my Google Account' => 'Verbinde meinen Google-Account', - 'Unlink my Google Account' => 'Verbindung mit meinem Google-Account trennen', - 'Login with my Google Account' => 'Anmelden mit meinem Google-Account', - 'Project not found.' => 'Das Projekt wurde nicht gefunden.', 'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.', 'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.', 'Remove a task' => 'Aufgabe löschen', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maximalgröße: ', 'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.', 'Display another project' => 'Zu Projekt wechseln', - 'Login with my Github Account' => 'Anmelden mit meinem Github-Account', - 'Link my Github Account' => 'Mit meinem Github-Account verbinden', - 'Unlink my Github Account' => 'Verbindung mit meinem Github-Account trennen', 'Created by %s' => 'Erstellt durch %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M', 'Tasks Export' => 'Aufgaben exportieren', 'Tasks exportation for "%s"' => 'Aufgaben exportieren für "%s"', 'Start Date' => 'Anfangsdatum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', 'view the task on Kanboard' => 'diese Aufgabe auf dem Kanboard zeigen', 'Public access' => 'Öffentlicher Zugriff', - 'User management' => 'Benutzer verwalten', 'Active tasks' => 'Aktive Aufgaben', 'Disable public access' => 'Öffentlichen Zugriff deaktivieren', 'Enable public access' => 'Öffentlichen Zugriff aktivieren', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Passwort ändern', 'Password modification' => 'Passwortänderung', 'External authentications' => 'Externe Authentisierungsmethoden', - 'Google Account' => 'Google-Account', - 'Github Account' => 'Github-Account', 'Never connected.' => 'Noch nie verbunden.', - 'No account linked.' => 'Kein Account verbunden.', - 'Account linked.' => 'Account verbunden', 'No external authentication enabled.' => 'Es sind keine externen Authentisierungsmethoden aktiv.', 'Password modified successfully.' => 'Passwort wurde erfolgreich geändert.', 'Unable to change the password.' => 'Passwort konnte nicht geändert werden.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen', 'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern', 'Reference' => 'Referenz', - 'Reference: %s' => 'Referenz: %s', 'Label' => 'Kennzeichnung', 'Database' => 'Datenbank', 'About' => 'Über', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequenz in Sekunden (standardmäßig 60 Sekunden)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequenz in Sekunden (0 um diese Funktion zu deaktivieren, standardmäßig 10 Sekunden)', 'Application URL' => 'Applikations-URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Beispiel: http://example.kanboard.net/ (wird für E-Mail-Benachrichtigungen verwendet)', 'Token regenerated.' => 'Token wurde neu generiert.', 'Date format' => 'Datumsformat', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Dieses Projekt ist privat', 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen', 'Add' => 'Hinzufügen', - 'Estimated time: %s hours' => 'Geplante Zeit: %s Stunden', - 'Time spent: %s hours' => 'Aufgewendete Zeit: %s Stunden', - 'Started on %B %e, %Y' => 'Gestartet am %B %e %Y', 'Start date' => 'Startdatum', 'Time estimated' => 'Geschätzte Zeit', 'There is nothing assigned to you.' => 'Ihnen ist nichts zugewiesen.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', 'Inactive swimlanes' => 'Inaktive Swimlane', 'Remove a swimlane' => 'Swimlane entfernen', - 'Rename' => 'umbenennen', 'Show default swimlane' => 'Standard-Swimlane anzeigen', 'Swimlane modification for the project "%s"' => 'Swimlane-Änderung für das Projekt "%s"', 'Swimlane not found.' => 'Swimlane nicht gefunden', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.', 'The default swimlane have been updated successfully.' => 'Die Standard-Swimlane wurden erfolgreich aktualisiert. Die Standard-Swimlane wurden erfolgreich aktualisiert.', - 'Unable to create your swimlane.' => 'Es ist nicht möglich, Swimlane zu erstellen.', 'Unable to remove this swimlane.' => 'Es ist nicht möglich, die Swimlane zu entfernen.', 'Unable to update this swimlane.' => 'Es ist nicht möglich, die Swimlane zu ändern.', 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', @@ -557,7 +519,7 @@ return array( 'All columns' => 'Alle Spalten', 'Calendar' => 'Kalender', 'Next' => 'Nächste', - // '#%d' => '', + '#%d' => 'Nr %d', 'All swimlanes' => 'Alle Swimlanes', 'All colors' => 'Alle Farben', 'Moved to column %s' => 'In Spalte %s verschoben', @@ -612,7 +574,6 @@ return array( 'This task' => 'Diese Aufgabe', '<1h' => '<1Std', '%dh' => '%dStd', - // '%b %e' => '', 'Expand tasks' => 'Aufgaben aufklappen', 'Collapse tasks' => 'Aufgaben zusammenklappen', 'Expand/collapse tasks' => 'Aufgaben auf/zuklappen', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Tastaturkürzel', 'Open board switcher' => 'Pinnwandauswahl öffnen', 'Application' => 'Anwendung', - 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p', 'Compact view' => 'Kompaktansicht', 'Horizontal scrolling' => 'Horizontales Scrollen', 'Compact/wide view' => 'Kompakt/Breite-Ansicht', 'No results match:' => 'Keine Ergebnisse:', 'Currency' => 'Währung', - 'Files' => 'Dateien', - 'Images' => 'Bilder', 'Private project' => 'privates Projekt', 'AUD - Australian Dollar' => 'AUD - Australische Dollar', 'CAD - Canadian Dollar' => 'CAD - Kanadische Dollar', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Teste dein Gerät', 'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'Hochgeladen von: %s', - 'uploaded on: %s' => 'Hochgeladen am: %s', - 'size: %s' => 'Größe: %s', 'Burndown chart for "%s"' => 'Burndown-Diagramm für "%s"', 'Burndown chart' => 'Burndown-Diagramm', 'This chart show the task complexity over the time (Work Remaining).' => 'Dieses Diagramm zeigt die Aufgabenkomplexität über den Faktor Zeit (Verbleibende Arbeit).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Eine Aufgabe kann nicht mit sich selber verbunden werden', 'The exact same link already exists' => 'Diese Verbindung existiert bereits', 'Recurrent task is scheduled to be generated' => 'Wiederkehrende Aufgabe ist zur Generierung eingeplant', - 'Recurring information' => 'Wiederkehrende Information', 'Score' => 'Wertung', 'The identifier must be unique' => 'Der Schlüssel muss einzigartig sein', 'This linked task id doesn\'t exists' => 'Die verbundene Aufgabe existiert nicht', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh', 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh', 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert', - 'The description have been modified' => 'Die Beschreibung wurde geändert', + 'The description has been modified' => 'Die Beschreibung wurde geändert', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)', - 'Swimlane: %s' => 'Swimlane: %s', 'I want to receive notifications for:' => 'Ich möchte Benachrichtigungen erhalten für:', 'All tasks' => 'Alle Aufgaben', 'Only for tasks assigned to me' => 'nur mir zugeordnete Aufgane', 'Only for tasks created by me' => 'nur von mir erstellte Aufgaben', 'Only for tasks created by me and assigned to me' => 'nur mir zugeordnete und von mir erstellte Aufgaben', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', - 'New due date: %B %e, %Y' => 'Neues Ablaufdatum: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Neues Beginndatum: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%Y-%%m-%%d' => '%%d.%%m.%%Y', 'Total for all columns' => 'Gesamt für alle Spalten', 'You need at least 2 days of data to show the chart.' => 'Es werden mindestens 2 Tage zur Darstellung benötigt', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Nicht zugewiesen', 'View advanced search syntax' => 'Zur erweiterten Suchsyntax', 'Overview' => 'Überblick', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Board-/Kalender-/Listen-Ansicht', 'Switch to the board view' => 'Zur Board-Ansicht', 'Switch to the calendar view' => 'Zur Kalender-Ansicht', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Das Diagramm zeigt die durchschnittliche Durchlauf- und Zykluszeit der letzten %d Aufgaben über die Zeit an.', 'Average time into each column' => 'Durchschnittzeit in jeder Spalte', 'Lead and cycle time' => 'Durchlauf- und Zykluszeit', - 'Google Authentication' => 'Google-Authentifizierung', - 'Help on Google authentication' => 'Hilfe bei Google-Authentifizierung', - 'Github Authentication' => 'Github-Authentifizierung', - 'Help on Github authentication' => 'Hilfe bei Github-Authentifizierung', 'Lead time: ' => 'Durchlaufzeit:', 'Cycle time: ' => 'Zykluszeit:', 'Time spent into each column' => 'zeit verbracht in jeder Spalte', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Wenn die Aufgabe nicht geschlossen ist, wird die aktuelle Zeit statt der Fertigstellung verwendet.', 'Set automatically the start date' => 'Setze Startdatum automatisch', 'Edit Authentication' => 'Authentifizierung bearbeiten', - 'Google Id' => 'Google Id', - 'Github Id' => 'Github Id', 'Remote user' => 'Remote-Benutzer', - 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Goole und Github Accounts', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Google und Github Accounts', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.', 'New remote user' => 'Neuer Remote-Benutzer', 'New local user' => 'Neuer lokaler Benutzer', 'Default task color' => 'Voreingestellte Aufgabenfarbe', - 'Hide sidebar' => 'Seitenleiste verstecken', - 'Expand sidebar' => 'Seitenleiste ausklappen', 'This feature does not work with all browsers.' => 'Diese Funktion funktioniert nicht mit allen Browsern', 'There is no destination project available.' => 'Es ist kein Zielprojekt vorhanden.', 'Trigger automatically subtask time tracking' => 'Teilaufgaben Zeiterfassung automatisch starten', @@ -901,7 +840,6 @@ return array( 'open file' => 'Datei öffnen', 'End date' => 'Endedatum', 'Users overview' => 'Benutzerübersicht', - 'Managers' => 'Manager', 'Members' => 'Mitglieder', 'Shared project' => 'Geteiltes Projekt', 'Project managers' => 'Projektmanager', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Endedatum:', 'There is no start date or end date for this project.' => 'Es gibt kein Startdatum oder Endedatum für dieses Projekt', 'Projects Gantt chart' => 'Projekt Gantt Diagramm', - 'Start date: %s' => 'Beginndatum: %s', - 'End date: %s' => 'Enddatum: %s', 'Link type' => 'Verbindungstyp', 'Change task color when using a specific task link' => 'Aufgabefarbe ändern bei bestimmter Aufgabenverbindung', 'Task link creation or modification' => 'Aufgabenverbindung erstellen oder bearbeiten', - 'Login with my Gitlab Account' => 'Mit Gitlab Account einloggen', 'Milestone' => 'Meilenstein', - 'Gitlab Authentication' => 'Gitlab-Authentifizierung', - 'Help on Gitlab authentication' => 'Hilfe bei Gitlab-Authentifizierung', - 'Gitlab Id' => 'Gitlab Id', - 'Gitlab Account' => 'Gitlab Account', - 'Link my Gitlab Account' => 'Verknüpfe mein Gitlab Account', - 'Unlink my Gitlab Account' => 'Trenne meinen Gitlab Account', 'Documentation: %s' => 'Dokumentation: %s', 'Switch to the Gantt chart view' => 'Zur Gantt-Diagramm Ansicht wechseln', 'Reset the search/filter box' => 'Suche/Filter-Box zurücksetzen', @@ -938,7 +867,7 @@ return array( 'Set maximum column height' => 'Setze maximale Spaltenhöhe', 'Remove maximum column height' => 'Entferne maximale Spaltenhöhe', 'My notifications' => 'Meine Benachrichtigungen', - 'Custom filters' => 'benutzerdefinierte Filter', + 'Custom filters' => 'Benutzerdefinierte Filter', 'Your custom filter have been created successfully.' => 'Benutzerdefinierten Filter erfolgreich erstellt.', 'Unable to create your custom filter.' => 'Benutzerdefinierter Filter konnte nicht erstellt werden.', 'Custom filter removed successfully.' => 'Benutzerdefinierten Filter erfolgreich entfernt.', @@ -1117,5 +1046,109 @@ return array( 'Lowest priority' => 'Niedrigste Priorität', 'Highest priority' => 'Höchste Priorität', 'If you put zero to the low and high priority, this feature will be disabled.' => 'Wenn Sie Null bei höchster und niedrigster Priorität eintragen, wird diese Funktion deaktiviert.', - 'Priority: %d' => 'Priorität: %d', + 'Close a task when there is no activity' => 'Schliesse eine Aufgabe, wenn keine Aktivitäten vorhanden sind', + 'Duration in days' => 'Dauer in Tagen', + 'Send email when there is no activity on a task' => 'Versende eine Email, wenn keine Aktivitäten an einer Aufgabe vorhanden sind', + 'List of external links' => 'Liste der externen Verbindungen', + 'Unable to fetch link information.' => 'Kann keine Informationen über Verbindungen holen', + 'Daily background job for tasks' => 'Tägliche Hintergrundarbeit für Aufgaben', + 'Auto' => 'Auto', + 'Related' => 'Verbunden', + 'Attachment' => 'Anhang', + 'Title not found' => 'Titel nicht gefunden', + 'Web Link' => 'Weblink', + 'External links' => 'Externe Verbindungen', + 'Add external link' => 'Externe Verbindung hinzufügen', + 'Type' => 'Typ', + 'Dependency' => 'Abhängigkeit', + 'Add internal link' => 'Füge interne Verbindung hinzu', + 'Add a new external link' => 'Füge eine neue externe Verbindung hinzu', + 'Edit external link' => 'Externe Verbindung bearbeiten', + 'External link' => 'Externe Verbindung', + 'Copy and paste your link here...' => 'Kopieren Sie Ihren Link hier...', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'Es gibt im Moment keine externe Verbindung.', + 'Internal links' => 'Interne Verbindungen', + 'There is no internal link for the moment.' => 'Es gibt im Moment keine interne Verbindung.', + 'Assign to me' => 'Mir zuweisen', + 'Me' => 'Mich', + 'Do not duplicate anything' => 'Nichts duplizieren', + 'Projects management' => 'Projektmanagement', + 'Users management' => 'Benutzermanagement', + 'Groups management' => 'Gruppenmanagement', + 'Create from another project' => 'Von einem anderen Projekt erstellen', + 'There is no subtask at the moment.' => 'Es gibt im Moment keine Teilaufgabe', + 'open' => 'offen', + 'closed' => 'geschlossen', + 'Priority:' => 'Priorität:', + 'Reference:' => 'Bezug:', + 'Complexity:' => 'Komplexität:', + 'Swimlane:' => 'Swimlane:', + 'Column:' => 'Spalte:', + 'Position:' => 'Position:', + 'Creator:' => 'Ersteller:', + 'Time estimated:' => 'Geschätzte Zeit:', + '%s hours' => '%s Stunden', + 'Time spent:' => 'Aufgewendete Zeit:', + 'Created:' => 'Erstellt:', + 'Modified:' => 'Geändert:', + 'Completed:' => 'Abgeschlossen:', + 'Started:' => 'Gestarted:', + 'Moved:' => 'Verschoben:', + 'Task #%d' => 'Aufgabe #%d', + 'Sub-tasks' => 'Teilaufgaben', + 'Date and time format' => 'Datums- und Zeitformat', + 'Time format' => 'Zeitformat', + 'Start date: ' => 'Anfangsdatum:', + 'End date: ' => 'Enddatum:', + 'New due date: ' => 'Neues Fälligkeitsdatum', + 'Start date changed: ' => 'Anfangsdatum geändert:', + 'Disable private projects' => 'Private Projekte deaktivieren', + 'Do you really want to remove this custom filter: "%s"?' => 'Wollen Sie diesen benutzerdefinierten Filter wirklich entfernen: "%s"?', + 'Remove a custom filter' => 'Benutzerdefinierten Filter entfernen', + 'User activated successfully.' => 'Benutzer erfolgreich aktiviert.', + 'Unable to enable this user.' => 'Dieser Benutzer kann nicht aktiviert werden.', + 'User disabled successfully.' => 'Benutzer erfolgreich deaktiviert.', + 'Unable to disable this user.' => 'Dieser Benutzer kann nicht deaktiviert werden.', + 'All files have been uploaded successfully.' => 'Alle Dateien wurden erfolgreich hochgeladen.', + 'View uploaded files' => 'Hochgeladene Dateien ansehen', + 'The maximum allowed file size is %sB.' => 'Die maximal erlaubte Dateigröße ist %sB.', + 'Choose files again' => 'Wählen Sie erneut Dateien aus', + 'Drag and drop your files here' => 'Ziehen Sie Ihre Dateien hier hin', + 'choose files' => 'Dateien auswählen', + 'View profile' => 'Profil ansehen', + 'Two Factor' => 'Zwei-Faktor', + 'Disable user' => 'Benutzer deaktivieren', + 'Do you really want to disable this user: "%s"?' => 'Wollen Sie diesen Benutzer wirklich deaktivieren: "%s"?', + 'Enable user' => 'Benutzer aktivieren', + 'Do you really want to enable this user: "%s"?' => 'Wollen Sie diesen Benutzer wirklich aktivieren: "%s"?', + 'Download' => 'Runterladen', + 'Uploaded: %s' => 'Heraufgeladen: %s', + 'Size: %s' => 'Größe: %s', + 'Uploaded by %s' => 'Heraufgeladen von %s', + 'Filename' => 'Dateiname', + 'Size' => 'Größe', + 'Column created successfully.' => 'Spalte erfolgreich erstellt.', + 'Another column with the same name exists in the project' => 'Es gibt bereits eine Spalte mit demselben Namen im Projekt', + 'Default filters' => 'Standard-Filter', + 'Your board doesn\'t have any column!' => 'Es gibt keine Spalten in diesem Projekt!', + 'Change column position' => 'Position der Spalte ändern', + 'Switch to the project overview' => 'Zur Projektübersicht wechseln', + 'User filters' => 'Benutzer-Filter', + 'Category filters' => 'Kategorie-Filter', + 'Upload a file' => 'Eine Datei hochladen', + 'There is no attachment at the moment.' => 'Es gibt zur Zeit keine Anhänge', + 'View file' => 'Datei ansehen', + 'Last activity' => 'Letzte Aktivität', + 'Change subtask position' => 'Position der Unteraufgabe ändern', + 'This value must be greater than %d' => 'Dieser Wert muss größer als %d sein', + 'Another swimlane with the same name exists in the project' => 'Es gibt bereits eine Swimlane mit diesem Namen im Projekt', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Beispiel: http://example.kanboard.net (wird zum Erstellen absoluter URLs genutzt)', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index d230aa33..7b09447b 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Διόρθωση', 'remove' => 'αφαιρετής', 'Remove' => 'Αφαίρεση', - 'Update' => 'Ενημέρωση', 'Yes' => 'Ναι', 'No' => 'Όχι', 'cancel' => 'ακύρωση', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Ενέργειες', 'Inactive' => 'Ανενεργός', 'Active' => 'Ενεργός', - 'Add this column' => 'Προσθήκη αυτής της στήλης', '%d tasks on the board' => '%d εργασίες στον κεντρικό πίνακα έργου', '%d tasks in total' => '%d εργασιών στο σύνολο', 'Unable to update this board.' => 'Αδύνατη η ενημέρωση αυτού του πίνακα', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Αφαίρεση του έργου', 'Edit the board for "%s"' => 'Διόρθωση πίνακα από « %s »', 'All projects' => 'Όλα τα έργα', - 'Change columns' => 'Αλλαγή στηλών', 'Add a new column' => 'Πρόσθήκη στήλης', 'Title' => 'Τίτλος', 'Nobody assigned' => 'Δεν έχει ανατεθεί', @@ -154,7 +151,7 @@ return array( 'Backlog' => 'Πρέπει να', 'Work in progress' => 'Σε πρόοδο', 'Done' => 'Ολοκληρωμένα', - 'Application version:' => 'Version εφαρμογής:', + 'Application version:' => 'Version εφαρμογής :', 'Id' => 'Αναγνωριστικό.', '%d closed tasks' => '%d κλειστές εργασίες', 'No task for this project' => 'Αριθμός εργασιών για το έργο', @@ -185,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Δεν είναι δυνατή η αφαίρεση αυτής της ενέργειας', 'Action removed successfully.' => 'Η ενέργεια αφαιρέθηκε με επιτυχία.', 'Automatic actions for the project "%s"' => 'Αυτόματες ενέργειες για το έργο « %s »', - 'Defined actions' => 'Ορισμένες ενέργειες', 'Add an action' => 'Προσθήκη ενέργειας', 'Event name' => 'Ονομασία συμβάντος', 'Action name' => 'Ονομασία ενέργειας', @@ -195,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Όταν εμφανίζεται το επιλεγμένο συμβάν εκτελέστε την αντίστοιχη ενέργεια.', 'Next step' => 'Επόμενο βήμα', 'Define action parameters' => 'Ορισμός παραμέτρων ενέργειας', - 'Save this action' => 'Αποθήκευση ενέργειας', 'Do you really want to remove this action: "%s"?' => 'Αφαίρεση της ενέργειας: « %s » ?', 'Remove an automatic action' => 'Αφαίρεση της αυτόματης ενέργειας', 'Assign the task to a specific user' => 'Ανάθεση της εργασίας σε συγκεκριμένο χρήστη', @@ -208,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Ανάθεση χρώματος σε συγκεκριμένο χρήστη', 'Column title' => 'Τίτλος στήλης', 'Position' => 'Θέση', - 'Move Up' => 'Μετακίνηση πάνω', - 'Move Down' => 'Μετακίνηση κάτω', 'Duplicate to another project' => 'Αντιγραφή σε άλλο έργο', 'Duplicate' => 'Αντιγραφή', 'link' => 'σύνδεσμος', @@ -434,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Συχνότητα σε δευτερόλεπτα (60 δευτερόλεπτα από προεπιλογή)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Συχνότητα σε δευτερόλεπτα (0 για να απενεργοποιήσετε αυτή τη λειτουργία, 10 δευτερόλεπτα από προεπιλογή)', 'Application URL' => 'Application URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Παράδειγμα: http://exemple.kanboard.net/ (χρησιμοποιείται για ειδοποιήσεις μέσω email)', 'Token regenerated.' => 'Token regenerated.', 'Date format' => 'Μορφή ημερομηνίας', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format είναι πάντα αποδεκτό, π.χ.: « %s » και « %s »', @@ -509,7 +501,6 @@ return array( 'Swimlanes' => 'Λωρίδες', 'Swimlane updated successfully.' => 'Η λωρίδα ενημερώθηκε με επιτυχία.', 'The default swimlane have been updated successfully.' => 'Η προεπιλεγμένη λωρίδα ενημερώθηκε με επιτυχία.', - 'Unable to create your swimlane.' => 'Αδύνατο να δημιουργηθεί η λωρίδα.', 'Unable to remove this swimlane.' => 'Αδύνατο να αφαιρεθεί η λωρίδα.', 'Unable to update this swimlane.' => 'Αδύνατο να ενημερωθεί η λωρίδα.', 'Your swimlane have been created successfully.' => 'Η λωρίδα δημιουργήθηκε με επιτυχία.', @@ -598,8 +589,6 @@ return array( 'Compact/wide view' => 'Συμπυκνωμένη/Ευρεία Προβολή', 'No results match:' => 'Δεν ταιριάζει κανένα αποτέλεσμα:', 'Currency' => 'Νόμισμα', - 'Files' => 'Αρχεία', - 'Images' => 'Εικόνες', 'Private project' => 'Ιδιωτικό έργο', 'AUD - Australian Dollar' => 'AUD - Australian Dollar', 'CAD - Canadian Dollar' => 'CAD - Canadian Dollar', @@ -645,9 +634,6 @@ return array( 'Test your device' => 'Ελέγξτε τη συσκευή σας', 'Assign a color when the task is moved to a specific column' => 'Αντιστοίχιση χρώματος όταν η εργασία κινείται σε μια συγκεκριμένη στήλη', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'ανέβασμα από %s', - 'uploaded on: %s' => 'ανέβασμα την %s', - 'size: %s' => 'μέγεθος: %s', 'Burndown chart for "%s"' => 'Δημιουργία διαγράμματος για « %s »', 'Burndown chart' => 'Δημιουργία διαγράμματος', 'This chart show the task complexity over the time (Work Remaining).' => 'Αυτό το γράφημα δείχνει την πολυπλοκότητα του έργου κατά την πάροδο του χρόνου (Εργασία που παραμένει).', @@ -745,9 +731,10 @@ return array( 'Time spent changed: %sh' => 'Ο χρόνος που πέρασε έχει αλλάξει: %sh', 'Time estimated changed: %sh' => 'Ο εκτιμώμενος χρόνος άλλαξε: %sh', 'The field "%s" have been updated' => 'Το πεδίο « %s » έχει ενημερωθεί', + 'The description has been modified' => 'Η περιγραφή έχει ενημερωθεί', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Σίγουρα θέλετε να κλείσετε την εργασία « %s » και την υπο-εργασία ?', + 'I want to receive notifications for:' => 'Επιθυμώ να λαμβάνω ενημερώσεις για :', 'The description have been modified' => 'Η περιγραφή έχει ενημερωθεί', - 'Do you really want to close the task "%s" as well as all subtasks?' => 'Σίγουρα θέλετε να κλείσετε την εργασία « %s » και όλες τις υπο-εργασίες της (αν υπάρχουν)?', - 'I want to receive notifications for:' => 'Επιθυμώ να λαμβάνω ενημερώσεις για:', 'All tasks' => 'Όλες οι εργασίες', 'Only for tasks assigned to me' => 'Μόνο σε εργασίες που μου έχουν ανατεθεί', 'Only for tasks created by me' => 'Μόνο σε εργασίες που έχουν δημιουργηθεί από εμένα', @@ -808,8 +795,8 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Αυτό το γράφημα δείχνει το average lead and cycle time για τις τελευταίες %d εργασίες κατά τη διάρκεια του χρόνου.', 'Average time into each column' => 'Μέσος χρόνος σε κάθε στήλη', 'Lead and cycle time' => 'Lead et cycle time', - 'Lead time: ' => 'Lead time: ', - 'Cycle time: ' => 'Cycle time: ', + 'Lead time: ' => 'Lead time : ', + 'Cycle time: ' => 'Cycle time : ', 'Time spent into each column' => 'Ο χρόνος που δαπανήθηκε σε κάθε στήλη', 'The lead time is the duration between the task creation and the completion.' => 'Το <lead time> είναι η διάρκεια μεταξύ της δημιουργίας του έργου και της ολοκλήρωσης του.', 'The cycle time is the duration between the start date and the completion.' => 'Το <cycle time> είναι η διάρκεια μεταξύ της ημερομηνίας εκκίνησης και της ολοκλήρωσης του.', @@ -1103,7 +1090,7 @@ return array( 'Position:' => 'Θέση:', 'Creator:' => 'Δημιουργός:', 'Time estimated:' => 'Προβλεπόμενη ώρα:', - '%s hours' => 'ώρες', + '%s hours' => '%s ώρες', 'Time spent:' => 'χρόνος που καταναλώθηκε:', 'Created:' => 'Δημιουργήθηκε:', 'Modified:' => 'Διορθώθηκε:', @@ -1119,7 +1106,7 @@ return array( 'New due date: ' => 'Νέα ημερομηνία λήξης: ', 'Start date changed: ' => 'Αλλαγμένη ημερομηνία έναρξης: ', 'Disable private projects' => 'Απενεργοποίηση ιδιωτικών έργων', - 'Do you really want to remove this custom filter: "%s"?' => 'Αφαίρεση αυτού του οριζόμενου από το χρήστη φίλτρου;', + 'Do you really want to remove this custom filter: "%s"?' => 'Αφαίρεση αυτού του οριζόμενου από το χρήστη φίλτρου %s ?', 'Remove a custom filter' => 'Αφαίρεση του οριζόμενου από το χρήστη φίλτρου', 'User activated successfully.' => 'Ο χρήστης ενεργοποιήθηκε με επιτυχία', 'Unable to enable this user.' => 'Δεν είναι δυνατή η ενεργοποίηση του χρήστη', diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 16c96ec5..6e51ed04 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Modificar', 'remove' => 'suprimir', 'Remove' => 'Suprimir', - 'Update' => 'Actualizar', 'Yes' => 'Sí', 'No' => 'No', 'cancel' => 'cancelar', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Acciones', 'Inactive' => 'Inactivo', 'Active' => 'Activo', - 'Add this column' => 'Añadir esta columna', '%d tasks on the board' => '%d tareas en el tablero', '%d tasks in total' => '%d tareas en total', 'Unable to update this board.' => 'No se puede actualizar este tablero.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Suprimir el proyecto', 'Edit the board for "%s"' => 'Modificar el tablero para « %s »', 'All projects' => 'Todos los proyectos', - 'Change columns' => 'Cambiar las columnas', 'Add a new column' => 'Añadir una nueva columna', 'Title' => 'Título', 'Nobody assigned' => 'Nadie asignado', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Abrir una tarea', 'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?', 'Back to the board' => 'Volver al tablero', - 'Created on %B %e, %Y at %k:%M %p' => 'Creado el %e de %B de %Y a las %k:%M %p', 'There is nobody assigned' => 'No hay nadie asignado a esta tarea', 'Column on the board:' => 'Columna en el tablero: ', - 'Status is open' => 'Estado abierto', - 'Status is closed' => 'Estado cerrado', 'Close this task' => 'Cerrar esta tarea', 'Open this task' => 'Abrir esta tarea', 'There is no description.' => 'No hay descripción.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'En curso', 'Done' => 'Hecho', 'Application version:' => 'Versión de la aplicación:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Completado el %e de %B de %Y a las %k:%M %p', - '%B %e, %Y at %k:%M %p' => '%e de %B de %Y a las %k:%M %p', - 'Date created' => 'Fecha de creación', - 'Date completed' => 'Fecha de terminación', 'Id' => 'Identificador', '%d closed tasks' => '%d tareas completadas', 'No task for this project' => 'Ninguna tarea para este proyecto', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Editar esta tarea', 'Due Date' => 'Fecha límite', 'Invalid date' => 'Fecha no válida', - 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %e de %B de %Y', - '%B %e, %Y' => '%e de %B de %Y', - '%b %e, %Y' => '%e de %B de %Y', 'Automatic actions' => 'Acciones automatizadas', '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.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'No se puede suprimir esta accción.', 'Action removed successfully.' => 'La acción ha sido borrada correctamente.', 'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »', - 'Defined actions' => 'Acciones definidas', 'Add an action' => 'Agregar una acción', 'Event name' => 'Nombre del evento', 'Action name' => 'Nombre de la acción', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Cuando tiene lugar el evento seleccionado, ejecutar la acción correspondiente.', 'Next step' => 'Etapa siguiente', 'Define action parameters' => 'Definición de los parametros de la acción', - 'Save this action' => 'Guardar esta acción', 'Do you really want to remove this action: "%s"?' => '¿Realmente desea suprimir esta acción « %s » ?', 'Remove an automatic action' => 'Suprimir una acción automatizada', 'Assign the task to a specific user' => 'Asignar una tarea a un usuario especifico', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Asignar un color a un usuario específico', 'Column title' => 'Título de la columna', 'Position' => 'Posición', - 'Move Up' => 'Mover hacia arriba', - 'Move Down' => 'Mover hacia abajo', 'Duplicate to another project' => 'Duplicar a otro proyecto', 'Duplicate' => 'Duplicar', 'link' => 'vinculación', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'El comentario ha sido suprimido correctamente.', 'Unable to remove this comment.' => 'No se puede suprimir este comentario.', 'Do you really want to remove this comment?' => '¿Desea suprimir este comentario?', - 'Only administrators or the creator of the comment can access to this page.' => 'Sólo los administradores o el autor del comentario tienen acceso a esta página.', 'Current password for the user "%s"' => 'Contraseña actual para el usuario: « %s »', 'The current password is required' => 'La contraseña es obligatoria', 'Wrong password' => 'contraseña incorrecta', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Falló la autenticación externa', 'Your external account is linked to your profile successfully.' => 'Su cuenta externa se ha vinculado exitosamente con su perfil.', 'Email' => 'Correo', - 'Link my Google Account' => 'Vincular con mi Cuenta en Google', - 'Unlink my Google Account' => 'Desvincular de mi Cuenta en Google', - 'Login with my Google Account' => 'Ingresar con mi Cuenta de Google', - 'Project not found.' => 'Proyecto no hallado.', 'Task removed successfully.' => 'Tarea suprimida correctamente.', 'Unable to remove this task.' => 'No pude suprimir esta tarea.', 'Remove a task' => 'Borrar una tarea', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Tamaño máximo', 'Unable to upload the file.' => 'No pude cargar el fichero.', 'Display another project' => 'Mostrar otro proyecto', - 'Login with my Github Account' => 'Ingresar con mi cuenta de Github', - 'Link my Github Account' => 'Vincular mi cuenta de Github', - 'Unlink my Github Account' => 'Desvincular mi cuenta de Github', 'Created by %s' => 'Creado por %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificación %e de %B de %Y a las %k:%M %p', 'Tasks Export' => 'Exportar tareas', 'Tasks exportation for "%s"' => 'Exportación de tareas para "%s"', 'Start Date' => 'Fecha de inicio', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:', 'view the task on Kanboard' => 'ver la tarea en Kanboard', 'Public access' => 'Acceso público', - 'User management' => 'Gestión de Usuarios', 'Active tasks' => 'Tareas activas', 'Disable public access' => 'Desactivar acceso público', 'Enable public access' => 'Activar acceso público', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Cambiar contraseña', 'Password modification' => 'Modificacion de contraseña', 'External authentications' => 'Autenticación externa', - 'Google Account' => 'Cuenta de Google', - 'Github Account' => 'Cuenta de Github', 'Never connected.' => 'Nunca se ha conectado.', - 'No account linked.' => 'Sin vínculo con cuenta.', - 'Account linked.' => 'Vinculada con Cuenta.', 'No external authentication enabled.' => 'Sin autenticación externa activa.', 'Password modified successfully.' => 'Contraseña cambiada correctamente.', 'Unable to change the password.' => 'No pude cambiar la contraseña.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Cambiar el concesionario basado en un nombre de usuario externo', 'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa', 'Reference' => 'Referencia', - 'Reference: %s' => 'Referencia: %s', 'Label' => 'Etiqueta', 'Database' => 'Base de Datos', 'About' => 'Acerca de', @@ -459,7 +427,6 @@ return array( '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', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado por las notificaciones de correo)', 'Token regenerated.' => 'Ficha regenerada.', 'Date format' => 'Formato de la fecha', 'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Este proyecto es privado', 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea', 'Add' => 'Añadir', - 'Estimated time: %s hours' => 'Tiempo estimado: %s horas', - 'Time spent: %s hours' => 'Tiempo invertido: %s horas', - 'Started on %B %e, %Y' => 'Iniciado el %e de %B de %Y', 'Start date' => 'Fecha de inicio', 'Time estimated' => 'Tiempo estimado', 'There is nothing assigned to you.' => 'No tiene nada asignado.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere quitar esta calle: "%s"?', 'Inactive swimlanes' => 'Calles inactivas', 'Remove a swimlane' => 'Quitar un calle', - 'Rename' => 'Renombrar', 'Show default swimlane' => 'Mostrar calle por defecto', 'Swimlane modification for the project "%s"' => 'Modificación de la calle para el proyecto "%s"', 'Swimlane not found.' => 'Calle no encontrada', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Calles', 'Swimlane updated successfully.' => 'Calle actualizada correctamente', 'The default swimlane have been updated successfully.' => 'La calle por defecto ha sido actualizada correctamente', - 'Unable to create your swimlane.' => 'Imposible crear su calle', 'Unable to remove this swimlane.' => 'Imposible de quitar esta calle', 'Unable to update this swimlane.' => 'Imposible de actualizar esta calle', 'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente', @@ -612,7 +574,6 @@ return array( 'This task' => 'Esta tarea', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e de %b', 'Expand tasks' => 'Espande tareas', 'Collapse tasks' => 'Colapsa tareas', 'Expand/collapse tasks' => 'Expande/colapasa tareas', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Atajos de teclado', 'Open board switcher' => 'Abrir conmutador de tablero', 'Application' => 'Aplicación', - 'since %B %e, %Y at %k:%M %p' => 'desde %e de %B de %Y a las %k:%M %p', 'Compact view' => 'Compactar vista', 'Horizontal scrolling' => 'Desplazamiento horizontal', 'Compact/wide view' => 'Vista compacta/amplia', 'No results match:' => 'No hay resultados coincidentes:', 'Currency' => 'Moneda', - 'Files' => 'Ficheros', - 'Images' => 'Imágenes', 'Private project' => 'Proyecto privado', 'AUD - Australian Dollar' => 'AUD - Dólar australiano', 'CAD - Canadian Dollar' => 'CAD - Dólar canadiense', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Probar su dispositivo', 'Assign a color when the task is moved to a specific column' => 'Asignar un color al mover la tarea a una columna específica', '%s via Kanboard' => '%s vía Kanboard', - 'uploaded by: %s' => 'cargado por: %s', - 'uploaded on: %s' => 'cargado en: %s', - 'size: %s' => 'tamaño: %s', '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 las tareas a lo largo del tiempo (Trabajo restante)', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Una tarea no puede se enlazada 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', - 'Recurring information' => 'Información recurrente', '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', @@ -776,19 +730,13 @@ 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 have 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?' => '¿De verdad que quiere cerra la tarea "%s" así como todas las subtareas?', - 'Swimlane: %s' => 'Calle: %s', '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', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%e de %B de %Y a las %k:%M %p', - 'New due date: %B %e, %Y' => 'Nueva fecha de entrega: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Cambiadad fecha de inicio: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%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.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'No asignada', 'View advanced search syntax' => 'Ver sintáxis avanzada de búsqueda', 'Overview' => 'Resumen', - '%b %e %Y' => '%e de %B de %Y', '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', @@ -846,10 +793,6 @@ return array( '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 time into each column' => 'Tiempo medio en cada columna', 'Lead and cycle time' => 'Plazo de entrega y de ciclo', - 'Google Authentication' => 'Autenticación de Google', - 'Help on Google authentication' => 'Ayuda con la aAutenticación de Google', - 'Github Authentication' => 'Autenticación de Github', - 'Help on Github authentication' => 'Ayuda con la autenticación de Github', 'Lead time: ' => 'Plazo de entrega: ', 'Cycle time: ' => 'Tiempo de Ciclo: ', 'Time spent into each column' => 'Tiempo empleado en cada columna', @@ -858,16 +801,12 @@ return array( '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.', 'Set automatically the start date' => 'Poner la fecha de inicio de forma automática', 'Edit Authentication' => 'Editar autenticación', - 'Google Id' => 'Id de Google', - 'Github Id' => 'Id de Github', '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.', 'New remote user' => 'Nuevo usuario remoto', 'New local user' => 'Nuevo usuario local', 'Default task color' => 'Color por defecto de tarea', - 'Hide sidebar' => 'Ocultar barra lateral', - 'Expand sidebar' => 'Expandir barra lateral', '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', @@ -901,7 +840,6 @@ return array( 'open file' => 'abrir fichero', 'End date' => 'Fecha de fin', 'Users overview' => 'Resumen de usuarios', - 'Managers' => 'Administradores', 'Members' => 'Miembros', 'Shared project' => 'Proyecto compartido', 'Project managers' => 'Administradores de proyecto', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Fecha final', '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', - 'Start date: %s' => 'Fecha inicial: %s', - 'End date: %s' => 'Fecha final: %s', 'Link type' => 'Tipo de enlace', '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', - 'Login with my Gitlab Account' => 'Ingresar usando mi Cuenta en Gitlab', 'Milestone' => 'Hito', - 'Gitlab Authentication' => 'Autenticación Gitlab', - 'Help on Gitlab authentication' => 'Ayuda con autenticación Gitlab', - 'Gitlab Id' => 'Id de Gitlab', - 'Gitlab Account' => 'Cuenta de Gitlab', - 'Link my Gitlab Account' => 'Enlazar con mi Cuenta en Gitlab', - 'Unlink my Gitlab Account' => 'Desenlazar con mi Cuenta en Gitlab', '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', @@ -1010,64 +939,64 @@ return array( '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', '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' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - 'Project members' => 'Miembros de proyecto', + '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s', + 'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace', + 'BAM - Konvertible Mark' => 'BAM - marco convertible', + 'Assignee Username' => 'Nombre de usuario del concesionario', + 'Assignee Name' => 'Nombre del concesionario', + 'Groups' => 'Grupos', + 'Members of %s' => 'Miembros de %s', + 'New group' => 'Nuevo grupo', + 'Group created successfully.' => 'Grupo creado exitosamente', + 'Unable to create your group.' => 'No es posible crear el grupo', + 'Edit group' => 'Editar grupo', + 'Group updated successfully.' => 'Grupo actualizado exitosamente', + 'Unable to update your group.' => 'No es posible actualizar el grupo', + 'Add group member to "%s"' => 'Agregar miembro del grupo a "%s"', + 'Group member added successfully.' => 'Miembro del grupo agregado exitosamente', + 'Unable to add group member.' => 'No es posible agregar miembro del grupo', + 'Remove user from group "%s"' => 'Eliminar usuario del grupo "%s"', + 'User removed successfully from this group.' => 'Usuario eliminado exitosamente 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 exitosamente', + 'Unable to remove this group.' => 'No es posible eliminar este grupo', + 'Project Permissions' => 'Permisos del proyecto', + 'Manager' => 'Administrador', + 'Project Manager' => 'Administrador de 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)', + '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' => 'Agregar miembro de 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', + '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', + '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', + 'Group' => 'Grupo', + 'Group Name' => 'Nombre del grupo', + 'Enter group name...' => 'Ingresa el nombre del grupo...', + 'Role:' => 'Rol:', + 'Project members' => 'Miembros del proyecto', // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', + '%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', + 'Mentioned' => 'Mencionado', // 'Compare Estimated Time vs Actual Time' => '', // 'Estimated hours: ' => '', // 'Actual hours: ' => '', @@ -1076,46 +1005,150 @@ return array( // 'Estimated Time' => '', // 'Actual Time' => '', 'Estimated vs actual time' => 'Tiempo estimado vs real', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', - // 'Time-based One-time Password Algorithm' => '', - // 'Two-Factor Provider: ' => '', - // 'Disable two-factor authentication' => '', - // 'Enable two-factor authentication' => '', - // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', + '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', + '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', + 'Password Reset for Kanboard' => 'Restablecimiento de contraseña para Kanboard', + 'Forgot password?' => '¿Olvidó contraseña?', + 'Enable "Forget Password"' => 'Habilitar "olvidar contraseña"', + 'Password Reset' => 'Restablecer contraseña', + 'New password' => 'Nueva contraseña', + 'Change Password' => 'Cambiar contraseña', + 'To reset your password click on this link:' => 'Para cambiar tu contraseña haz clic en el siguiente enlace:', + 'Last Password Reset' => 'Último cambio de contraseña', // 'The password has never been reinitialized.' => '', - // 'Creation' => '', - // 'Expiration' => '', - // 'Password reset history' => '', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + '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 exitosamente', + '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)', + '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', + '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', + 'Priority' => 'Prioridad', + 'Task priority' => 'Prioridad de la tarea', + 'General' => 'General', + 'Dates' => 'Fechas', + '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', + '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', + 'List of external links' => 'Lista de enlaces externos', + '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', + 'Auto' => 'Automático', + 'Related' => 'Relacionado', + 'Attachment' => 'Adjunto', + 'Title not found' => 'Título no ha sido encontrado', + 'Web Link' => 'Enlace web', + 'External links' => 'Enlaces externos', + 'Add external link' => 'Añadir enlace externo', + 'Type' => 'Tipo', + 'Dependency' => 'Dependencia', + 'Add internal link' => 'Añadir enlace interno', + 'Add a new external link' => 'Añadir un nuevo enlace externo', + 'Edit external link' => 'Modificar enlace externo', + 'External link' => 'Enlace externo', + 'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'No existe un enlace externo por el momento', + 'Internal links' => 'Enlaces internos', + 'There is no internal link for the moment.' => 'No existe un enlace interno por el momento', + 'Assign to me' => 'Asignar 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', + 'There is no subtask at the moment.' => 'No existe subtarea por el momento', + 'open' => 'abierto', + 'closed' => 'cerrado', + 'Priority:' => 'Prioridad', + 'Reference:' => 'Referencia', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + '%s hours' => '%s horas', + // 'Time spent:' => '', + 'Created:' => 'Creado', + 'Modified:' => 'Modificado', + 'Completed:' => 'Terminado', + 'Started:' => 'Iniciado', + 'Moved:' => 'Movido', + 'Task #%d' => 'Tarea #%d', + 'Sub-tasks' => 'Subtareas', + 'Date and time format' => 'Formato de hora y fecha', + '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', + '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 exitosamente', + 'Unable to enable this user.' => 'No es posible habilitar este usuario', + 'User disabled successfully.' => 'Usuario deshabilitado exitosamente', + 'Unable to disable this user.' => 'No es posible deshabilitar este usuario', + 'All files have been uploaded successfully.' => 'Todos los archivos han sido subidos exitosamente', + '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', + 'Drag and drop your files here' => 'Arrastra y suelta tus archivos aquí', + 'choose files' => 'Elegir archivos', + 'View profile' => 'Ver perfil', + 'Two Factor' => 'Dos factores', + 'Disable user' => 'Deshabilitar usuario', + 'Do you really want to disable this user: "%s"?' => '¿Realmente desea deshabilitar este usuario: "%s"?', + 'Enable user' => 'Habilitar usuario', + 'Do you really want to enable this user: "%s"?' => '¿Realmente desea habilitar este usuario: "%s"?', + 'Download' => 'Descargar', + 'Uploaded: %s' => 'Subido: %s', + 'Size: %s' => 'Tamaño: %s', + 'Uploaded by %s' => 'Subido por %s', + 'Filename' => 'Nombre de archivo', + 'Size' => 'Tamaño', + 'Column created successfully.' => 'Columna creada exitosamente', + 'Another column with the same name exists in the project' => 'Ya existe una columna con el mismo nombre en el proyecto', + 'Default filters' => 'Filtros predeterminados', + 'Your board doesn\'t have any column!' => '¡Tu tablero no tiene ninguna columna', + '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', + 'There is no attachment at the moment.' => 'No existe ningún adjunto por el momento', + '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 a %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.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index cde825e2..b0d1c9e1 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Muokkaa', 'remove' => 'poista', 'Remove' => 'Poista', - 'Update' => 'Päivitä', 'Yes' => 'Kyllä', 'No' => 'Ei', 'cancel' => 'peruuta', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Toiminnot', 'Inactive' => 'Ei aktiivinen', 'Active' => 'Aktiivinen', - 'Add this column' => 'Lisää tämä sarake', '%d tasks on the board' => '%d tehtävää taululla', '%d tasks in total' => '%d tehtävää yhteensä', 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Poista projekti', 'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"', 'All projects' => 'Kaikki projektit', - 'Change columns' => 'Muokkaa sarakkeita', 'Add a new column' => 'Lisää uusi sarake', 'Title' => 'Nimi', 'Nobody assigned' => 'Ei suorittajaa', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Avaa tehtävä', 'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?', 'Back to the board' => 'Takaisin tauluun', - 'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M', 'There is nobody assigned' => 'Ei suorittajaa', 'Column on the board:' => 'Sarake taululla: ', - 'Status is open' => 'Status on avoin', - 'Status is closed' => 'Status on suljettu', 'Close this task' => 'Sulje tämä tehtävä', 'Open this task' => 'Avaa tämä tehtävä', 'There is no description.' => 'Ei kuvausta.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Työnalla', 'Done' => 'Tehty', 'Application version:' => 'Ohjelman versio:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M', - 'Date created' => 'Luomispäivä', - 'Date completed' => 'Valmistumispäivä', 'Id' => 'Id', '%d closed tasks' => '%d suljettua tehtävää', 'No task for this project' => 'Ei tehtävää tälle projektille', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Muokkaa tehtävää', 'Due Date' => 'Deadline', 'Invalid date' => 'Virheellinen päiväys', - 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y', - '%B %e, %Y' => '%d.%m.%Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'Automaattiset toiminnot', 'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.', 'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.', 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.', 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"', - 'Defined actions' => 'Määritellyt toiminnot', // 'Add an action' => '', 'Event name' => 'Tapahtuman nimi', 'Action name' => 'Toiminnon nimi', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.', 'Next step' => 'Seuraava vaihe', 'Define action parameters' => 'Määrittele toiminnon parametrit', - 'Save this action' => 'Tallenna toiminto', 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?', 'Remove an automatic action' => 'Poista automaattintn toiminto', 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Valitse väri käyttäjälle', 'Column title' => 'Sarakkeen nimi', 'Position' => 'Positio', - 'Move Up' => 'Siirrä ylös', - 'Move Down' => 'Siirrä alas', 'Duplicate to another project' => 'Kopioi toiseen projektiin', 'Duplicate' => 'Monista', 'link' => 'linkki', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.', 'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.', 'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?', - 'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.', 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana', 'The current password is required' => 'Salasana vaaditaan', 'Wrong password' => 'Väärä salasana', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'Sähköposti', - 'Link my Google Account' => 'Linkitä Google-tili', - 'Unlink my Google Account' => 'Poista Google-tilin linkitys', - 'Login with my Google Account' => 'Kirjaudu Google tunnuksella', - 'Project not found.' => 'Projektia ei löytynyt.', 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.', 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.', 'Remove a task' => 'Poista tehtävä', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimikoko: ', 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.', 'Display another project' => 'Näytä toinen projekti', - 'Login with my Github Account' => 'Kirjaudu sisään Github-tililläni', - 'Link my Github Account' => 'Liitä Github-tilini', - 'Unlink my Github Account' => 'Poista liitos Github-tiliini', 'Created by %s' => 'Luonut: %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M', 'Tasks Export' => 'Tehtävien vienti', 'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"', 'Start Date' => 'Aloituspäivä', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Haluan vastaanottaa ilmoituksia ainoastaan näistä projekteista:', 'view the task on Kanboard' => 'katso tehtävää Kanboardissa', 'Public access' => 'Julkinen käyttöoikeus', - 'User management' => 'Käyttäjähallinta', 'Active tasks' => 'Aktiiviset tehtävät', 'Disable public access' => 'Poista käytöstä julkinen käyttöoikeus', 'Enable public access' => 'Ota käyttöön ', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Vaihda salasana', 'Password modification' => 'Salasanan vaihto', 'External authentications' => 'Muut tunnistautumistavat', - 'Google Account' => 'Google-tili', - 'Github Account' => 'Github-tili', 'Never connected.' => 'Ei koskaan liitetty.', - 'No account linked.' => 'Tiliä ei ole liitetty.', - 'Account linked.' => 'Tili on liitetty.', 'No external authentication enabled.' => 'Muita tunnistautumistapoja ei ole otettu käyttöön.', 'Password modified successfully.' => 'Salasana vaihdettu onnistuneesti.', 'Unable to change the password.' => 'Salasanan vaihto epäonnistui.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen', 'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin', 'Reference' => 'Viite', - 'Reference: %s' => 'Viite: %s', 'Label' => 'Label', 'Database' => 'Tietokanta', 'About' => 'Tietoja', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Päivitystiheys sekunteina (60 sekuntia oletuksena)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Päivitystiheys sekunteina (0 poistaa toiminnon käytöstä, oletuksena 10 sekuntia)', 'Application URL' => 'Sovelluksen URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esimerkiksi: http://example.kanboard.net/ (käytetään sähköposti-ilmoituksissa)', 'Token regenerated.' => 'Token uudelleenluotu.', 'Date format' => 'Päiväyksen muoto', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Tämä projekti on yksityinen', 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän', 'Add' => 'Lisää', - 'Estimated time: %s hours' => 'Arvioitu aika: %s tuntia', - 'Time spent: %s hours' => 'Aikaa kulunut: %s tuntia', - 'Started on %B %e, %Y' => 'Aloitettu %B %e, %Y', 'Start date' => 'Aloituspäivä', 'Time estimated' => 'Arvioitu aika', 'There is nothing assigned to you.' => 'Ei tehtäviä, joihin sinut olisi merkitty tekijäksi.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Haluatko varmasti poistaa tämän kaistan: "%s"?', 'Inactive swimlanes' => 'Passiiviset kaistat', 'Remove a swimlane' => 'Poista kaista', - 'Rename' => 'Uudelleennimeä', 'Show default swimlane' => 'Näytä oletuskaista', 'Swimlane modification for the project "%s"' => 'Kaistamuutos projektille "%s"', 'Swimlane not found.' => 'Kaistaa ei löydy', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Kaistat', 'Swimlane updated successfully.' => 'Kaista päivitetty onnistuneesti.', 'The default swimlane have been updated successfully.' => 'Oletuskaista päivitetty onnistuneesti.', - 'Unable to create your swimlane.' => 'Kaistan luonti epäonnistui.', 'Unable to remove this swimlane.' => 'Kaistan poisto epäonnistui.', 'Unable to update this swimlane.' => 'Kaistan päivittäminen epäonnistui.', 'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.', @@ -612,7 +574,6 @@ return array( // 'This task' => '', // '<1h' => '', // '%dh' => '', - // '%b %e' => '', // 'Expand tasks' => '', // 'Collapse tasks' => '', // 'Expand/collapse tasks' => '', @@ -622,14 +583,11 @@ return array( // 'Keyboard shortcuts' => '', // 'Open board switcher' => '', // 'Application' => '', - // 'since %B %e, %Y at %k:%M %p' => '', // 'Compact view' => '', // 'Horizontal scrolling' => '', // 'Compact/wide view' => '', // 'No results match:' => '', // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', // 'Private project' => '', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', // 'I want to receive notifications for:' => '', // 'All tasks' => '', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( // 'Not assigned' => '', // 'View advanced search syntax' => '', // 'Overview' => '', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 32110e1c..1755f990 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Modifier', 'remove' => 'supprimer', 'Remove' => 'Supprimer', - 'Update' => 'Mettre à jour', 'Yes' => 'Oui', 'No' => 'Non', 'cancel' => 'annuler', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Actions', 'Inactive' => 'Inactif', 'Active' => 'Actif', - 'Add this column' => 'Ajouter cette colonne', '%d tasks on the board' => '%d tâches sur le tableau', '%d tasks in total' => '%d tâches au total', 'Unable to update this board.' => 'Impossible de mettre à jour ce tableau.', @@ -72,10 +70,9 @@ return array( 'Remove project' => 'Supprimer le projet', 'Edit the board for "%s"' => 'Modifier le tableau pour « %s »', 'All projects' => 'Tous les projets', - 'Change columns' => 'Changer les colonnes', 'Add a new column' => 'Ajouter une nouvelle colonne', 'Title' => 'Titre', - 'Nobody assigned' => 'Personne assignée', + 'Nobody assigned' => 'Personne assigné', 'Assigned to %s' => 'Assigné à %s', 'Remove a column' => 'Supprimer une colonne', 'Remove a column from a board' => 'Supprimer une colonne d\'un tableau', @@ -96,17 +93,14 @@ return array( 'Edit a task' => 'Modifier une tâche', 'Column' => 'Colonne', 'Color' => 'Couleur', - 'Assignee' => 'Personne assigné', + 'Assignee' => 'Personne assignée', 'Create another task' => 'Créer une autre tâche', 'New task' => 'Nouvelle tâche', 'Open a task' => 'Ouvrir une tâche', 'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?', 'Back to the board' => 'Retour au tableau', - 'Created on %B %e, %Y at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M', 'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche', 'Column on the board:' => 'Colonne sur le tableau : ', - 'Status is open' => 'État ouvert', - 'Status is closed' => 'État fermé', 'Close this task' => 'Fermer cette tâche', 'Open this task' => 'Ouvrir cette tâche', 'There is no description.' => 'Il n\'y a pas de description.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'En cours', 'Done' => 'Terminé', 'Application version:' => 'Version de l\'application :', - 'Completed on %B %e, %Y at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', - 'Date created' => 'Date de création', - 'Date completed' => 'Date de clôture', 'Id' => 'Id.', '%d closed tasks' => '%d tâches terminées', 'No task for this project' => 'Aucune tâche pour ce projet', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Modifier cette tâche', 'Due Date' => 'Date d\'échéance', 'Invalid date' => 'Date invalide', - 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Actions automatisées', 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajoutée avec succès.', 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Impossible de supprimer cette action', 'Action removed successfully.' => 'Action supprimée avec succès.', 'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »', - 'Defined actions' => 'Actions définies', 'Add an action' => 'Ajouter une action', 'Event name' => 'Nom de l\'événement', 'Action name' => 'Nom de l\'action', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Lorsque l\'événement sélectionné se déclenche, exécuter l\'action correspondante.', 'Next step' => 'Étape suivante', 'Define action parameters' => 'Définition des paramètres de l\'action', - 'Save this action' => 'Sauvegarder cette action', 'Do you really want to remove this action: "%s"?' => 'Voulez-vous vraiment supprimer cette action « %s » ?', 'Remove an automatic action' => 'Supprimer une action automatisée', 'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Assigner une couleur à un utilisateur', 'Column title' => 'Titre de la colonne', 'Position' => 'Position', - 'Move Up' => 'Déplacer vers le haut', - 'Move Down' => 'Déplacer vers le bas', 'Duplicate to another project' => 'Dupliquer dans un autre projet', 'Duplicate' => 'Dupliquer', 'link' => 'lien', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Commentaire supprimé avec succès.', 'Unable to remove this comment.' => 'Impossible de supprimer ce commentaire.', 'Do you really want to remove this comment?' => 'Voulez-vous vraiment supprimer ce commentaire ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Seuls les administrateurs ou le créateur du commentaire peuvent accéder à cette page.', 'Current password for the user "%s"' => 'Mot de passe actuel pour l\'utilisateur « %s »', 'The current password is required' => 'Le mot de passe actuel est obligatoire', 'Wrong password' => 'Mot de passe invalide', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'L’authentification externe a échoué', 'Your external account is linked to your profile successfully.' => 'Votre compte externe est désormais lié à votre profil.', 'Email' => 'Email', - 'Link my Google Account' => 'Lier mon compte Google', - 'Unlink my Google Account' => 'Ne plus utiliser mon compte Google', - 'Login with my Google Account' => 'Se connecter avec mon compte Google', - 'Project not found.' => 'Projet introuvable.', 'Task removed successfully.' => 'Tâche supprimée avec succès.', 'Unable to remove this task.' => 'Impossible de supprimer cette tâche.', 'Remove a task' => 'Supprimer une tâche', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Taille maximum : ', 'Unable to upload the file.' => 'Impossible de transférer le fichier.', 'Display another project' => 'Afficher un autre projet', - 'Login with my Github Account' => 'Se connecter avec mon compte Github', - 'Link my Github Account' => 'Lier mon compte Github', - 'Unlink my Github Account' => 'Ne plus utiliser mon compte Github', 'Created by %s' => 'Créé par %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M', 'Tasks Export' => 'Exportation des tâches', 'Tasks exportation for "%s"' => 'Exportation des tâches pour « %s »', 'Start Date' => 'Date de début', @@ -369,7 +343,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :', 'view the task on Kanboard' => 'voir la tâche sur Kanboard', 'Public access' => 'Accès public', - 'User management' => 'Gestion des utilisateurs', 'Active tasks' => 'Tâches actives', 'Disable public access' => 'Désactiver l\'accès public', 'Enable public access' => 'Activer l\'accès public', @@ -397,11 +370,7 @@ return array( 'Change password' => 'Changer le mot de passe', 'Password modification' => 'Changement de mot de passe', 'External authentications' => 'Authentifications externes', - 'Google Account' => 'Compte Google', - 'Github Account' => 'Compte Github', 'Never connected.' => 'Jamais connecté.', - 'No account linked.' => 'Aucun compte attaché.', - 'Account linked.' => 'Compte attaché.', 'No external authentication enabled.' => 'Aucune authentication externe activée.', 'Password modified successfully.' => 'Mot de passe changé avec succès.', 'Unable to change the password.' => 'Impossible de changer le mot de passe.', @@ -443,7 +412,6 @@ return array( 'Change the assignee based on an external username' => 'Changer l\'assigné en fonction d\'un utilisateur externe', 'Change the category based on an external label' => 'Changer la catégorie en fonction d\'un libellé externe', 'Reference' => 'Référence', - 'Reference: %s' => 'Référence : %s', 'Label' => 'Libellé', 'Database' => 'Base de données', 'About' => 'À propos', @@ -461,7 +429,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Fréquence en seconde (60 secondes par défaut)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Fréquence en seconde (0 pour désactiver, 10 secondes par défaut)', 'Application URL' => 'URL de l\'application', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemple : http://exemple.kanboard.net/ (utilisé pour les notifications)', 'Token regenerated.' => 'Jeton de sécurité regénéré.', 'Date format' => 'Format des dates', 'ISO format is always accepted, example: "%s" and "%s"' => 'Le format ISO est toujours accepté, exemple : « %s » et « %s »', @@ -469,9 +436,6 @@ return array( 'This project is private' => 'Ce projet est privé', 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici', 'Add' => 'Ajouter', - 'Estimated time: %s hours' => 'Temps estimé: %s heures', - 'Time spent: %s hours' => 'Temps passé : %s heures', - 'Started on %B %e, %Y' => 'Commençé le %d/%m/%Y', 'Start date' => 'Date de début', 'Time estimated' => 'Temps estimé', 'There is nothing assigned to you.' => 'Il n\'y a rien d\'assigné pour vous.', @@ -531,7 +495,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Voulez-vous vraiment supprimer cette swimlane : « %s » ?', 'Inactive swimlanes' => 'Swimlanes inactives', 'Remove a swimlane' => 'Supprimer une swimlane', - 'Rename' => 'Renommer', 'Show default swimlane' => 'Afficher la swimlane par défaut', 'Swimlane modification for the project "%s"' => 'Modification d\'une swimlane pour le projet « %s »', 'Swimlane not found.' => 'Cette swimlane est introuvable.', @@ -539,7 +502,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane mise à jour avec succès.', 'The default swimlane have been updated successfully.' => 'La swimlane par défaut a été mise à jour avec succès.', - 'Unable to create your swimlane.' => 'Impossible de créer votre swimlane.', 'Unable to remove this swimlane.' => 'Impossible de supprimer cette swimlane.', 'Unable to update this swimlane.' => 'Impossible de mettre à jour cette swimlane.', 'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.', @@ -614,7 +576,6 @@ return array( 'This task' => 'Cette tâche', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Déplier les tâches', 'Collapse tasks' => 'Replier les tâches', 'Expand/collapse tasks' => 'Plier/déplier les tâches', @@ -624,14 +585,11 @@ return array( 'Keyboard shortcuts' => 'Raccourcis clavier', 'Open board switcher' => 'Ouvrir le sélecteur de tableau', 'Application' => 'Application', - 'since %B %e, %Y at %k:%M %p' => 'depuis le %d/%m/%Y à %H:%M', 'Compact view' => 'Vue compacte', 'Horizontal scrolling' => 'Défilement horizontal', 'Compact/wide view' => 'Basculer entre la vue compacte et étendue', 'No results match:' => 'Aucun résultat :', 'Currency' => 'Devise', - 'Files' => 'Fichiers', - 'Images' => 'Images', 'Private project' => 'Projet privé', 'AUD - Australian Dollar' => 'AUD - Dollar australien', 'CAD - Canadian Dollar' => 'CAD - Dollar canadien', @@ -677,9 +635,6 @@ return array( 'Test your device' => 'Testez votre appareil', 'Assign a color when the task is moved to a specific column' => 'Assigner une couleur lorsque la tâche est déplacée dans une colonne spécifique', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'Téléchargé par %s', - 'uploaded on: %s' => 'Téléchargé le %s', - 'size: %s' => 'Taille : %s', 'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »', 'Burndown chart' => 'Graphique d\'avancement', 'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).', @@ -696,7 +651,6 @@ return array( 'A task cannot be linked to itself' => 'Une tâche ne peut être liée à elle-même', 'The exact same link already exists' => 'Un lien identique existe déjà', 'Recurrent task is scheduled to be generated' => 'La tâche récurrente est programmée pour être créée', - 'Recurring information' => 'Information sur la récurrence', 'Score' => 'Complexité', 'The identifier must be unique' => 'L\'identifiant doit être unique', 'This linked task id doesn\'t exists' => 'L\'identifiant de la task liée n\'existe pas', @@ -778,19 +732,13 @@ return array( 'Time spent changed: %sh' => 'Le temps passé a été changé : %sh', 'Time estimated changed: %sh' => 'Le temps estimé a été changé : %sh', 'The field "%s" have been updated' => 'Le champ « %s » a été mis à jour', - 'The description have been modified' => 'La description a été modifiée', + 'The description has been modified' => 'La description a été modifiée', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Voulez-vous vraiment fermer la tâche « %s » ainsi que toutes ses sous-tâches ?', - 'Swimlane: %s' => 'Swimlane : %s', 'I want to receive notifications for:' => 'Je veux reçevoir les notifications pour :', 'All tasks' => 'Toutes les Tâches', 'Only for tasks assigned to me' => 'Seulement les tâches qui me sont assignées', 'Only for tasks created by me' => 'Seulement les tâches que j\'ai créées', 'Only for tasks created by me and assigned to me' => 'Seulement les tâches créées par moi-même et celles qui me sont assignées', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Nouvelle date d\'échéance : %d/%m/%Y', - 'Start date changed: %B %e, %Y' => 'Date de début modifiée : %d/%m/%Y', - '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', 'Total for all columns' => 'Total pour toutes les colonnes', 'You need at least 2 days of data to show the chart.' => 'Vous avez besoin d\'au minimum 2 jours de données pour afficher le graphique.', @@ -815,7 +763,6 @@ return array( 'Not assigned' => 'Non assignées', 'View advanced search syntax' => 'Voir la syntaxe pour la recherche avancée', 'Overview' => 'Vue d\'ensemble', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Vue Tableau/Calendrier/Liste', 'Switch to the board view' => 'Basculer vers le tableau', 'Switch to the calendar view' => 'Basculer vers le calendrier', @@ -848,10 +795,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Ce graphique montre la durée moyenne du lead et cycle time pour les %d dernières tâches.', 'Average time into each column' => 'Temps moyen dans chaque colonne', 'Lead and cycle time' => 'Lead et cycle time', - 'Google Authentication' => 'Authentification Google', - 'Help on Google authentication' => 'Aide sur l\'authentification Google', - 'Github Authentication' => 'Authentification Github', - 'Help on Github authentication' => 'Aide sur l\'authentification Github', 'Lead time: ' => 'Lead time : ', 'Cycle time: ' => 'Temps de cycle : ', 'Time spent into each column' => 'Temps passé dans chaque colonne', @@ -860,16 +803,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Si la tâche n\'est pas fermée, l\'heure courante est utilisée à la place de la date de complétion.', 'Set automatically the start date' => 'Définir automatiquement la date de début', 'Edit Authentication' => 'Modifier l\'authentification', - 'Google Id' => 'Identifiant Google', - 'Github Id' => 'Identifiant Github', 'Remote user' => 'Utilisateur distant', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Les utilisateurs distants ne stockent pas leur mot de passe dans la base de données de Kanboard, exemples : comptes LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si vous cochez la case « Interdire le formulaire d\'authentification », les identifiants entrés dans le formulaire d\'authentification seront ignorés.', 'New remote user' => 'Créer un utilisateur distant', 'New local user' => 'Créer un utilisateur local', 'Default task color' => 'Couleur par défaut des tâches', - 'Hide sidebar' => 'Cacher la barre latérale', - 'Expand sidebar' => 'Déplier la barre latérale', 'This feature does not work with all browsers.' => 'Cette fonctionnalité n\'est pas compatible avec tous les navigateurs', 'There is no destination project available.' => 'Il n\'y a pas de projet de destination disponible.', 'Trigger automatically subtask time tracking' => 'Déclencher automatiquement le suivi du temps pour les sous-tâches', @@ -903,7 +842,6 @@ return array( 'open file' => 'ouvrir le fichier', 'End date' => 'Date de fin', 'Users overview' => 'Vue d\'ensemble des utilisateurs', - 'Managers' => 'Gérants', 'Members' => 'Membres', 'Shared project' => 'Projet partagé', 'Project managers' => 'Gestionnaires de projet', @@ -914,19 +852,10 @@ return array( 'End date:' => 'Date de fin :', 'There is no start date or end date for this project.' => 'Il n\'y a pas de date de début ou de date de fin pour ce projet.', 'Projects Gantt chart' => 'Diagramme de Gantt des projets', - 'Start date: %s' => 'Date de début : %s', - 'End date: %s' => 'Date de fin : %s', 'Link type' => 'Type de lien', 'Change task color when using a specific task link' => 'Changer la couleur de la tâche lorsqu\'un lien spécifique est utilisé', 'Task link creation or modification' => 'Création ou modification d\'un lien sur une tâche', - 'Login with my Gitlab Account' => 'Se connecter avec mon compte Gitlab', 'Milestone' => 'Étape importante', - 'Gitlab Authentication' => 'Authentification Gitlab', - 'Help on Gitlab authentication' => 'Aide sur l\'authentification Gitlab', - 'Gitlab Id' => 'Identifiant Gitlab', - 'Gitlab Account' => 'Compte Gitlab', - 'Link my Gitlab Account' => 'Lier mon compte Gitlab', - 'Unlink my Gitlab Account' => 'Ne plus utiliser mon compte Gitlab', 'Documentation: %s' => 'Documentation : %s', 'Switch to the Gantt chart view' => 'Passer à la vue en diagramme de Gantt', 'Reset the search/filter box' => 'Réinitialiser le champ de recherche', @@ -1111,7 +1040,7 @@ return array( 'Project owner' => 'Responsable du projet', 'Those dates are useful for the project Gantt chart.' => 'Ces dates sont utiles pour le diagramme de Gantt des projets.', 'Private projects do not have users and groups management.' => 'Les projets privés n\'ont pas de gestion d\'utilisateurs et de groupes.', - 'There is no project member.' => 'Il y a aucun membre du projet.', + 'There is no project member.' => 'Il n\'y a aucun membre du projet.', 'Priority' => 'Priorité', 'Task priority' => 'Priorité des tâches', 'General' => 'Général', @@ -1120,5 +1049,109 @@ return array( 'Lowest priority' => 'Priorité basse', 'Highest priority' => 'Priorité haute', 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si vous mettez zéro pour la priorité basse et haute, cette fonctionnalité sera désactivée.', - 'Priority: %d' => 'Priorité : %d', + 'Close a task when there is no activity' => 'Fermer une tâche sans activité', + 'Duration in days' => 'Durée en jours', + 'Send email when there is no activity on a task' => 'Envoyer un email lorsqu\'il n\'y a pas d\'activité sur une tâche', + 'List of external links' => 'Liste des liens externes', + 'Unable to fetch link information.' => 'Impossible de récupérer les informations sur le lien.', + 'Daily background job for tasks' => 'Tâche planifiée quotidienne pour les tâches', + 'Auto' => 'Auto', + 'Related' => 'Relié', + 'Attachment' => 'Pièce-jointe', + 'Title not found' => 'Titre non trouvé', + 'Web Link' => 'Lien web', + 'External links' => 'Liens externes', + 'Add external link' => 'Ajouter un lien externe', + 'Type' => 'Type', + 'Dependency' => 'Dépendance', + 'Add internal link' => 'Ajouter un lien interne', + 'Add a new external link' => 'Ajouter un nouveau lien externe', + 'Edit external link' => 'Modifier un lien externe', + 'External link' => 'Lien externe', + 'Copy and paste your link here...' => 'Copier-coller vôtre lien ici...', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'Il n\'y a pas de lien externe pour le moment.', + 'Internal links' => 'Liens internes', + 'There is no internal link for the moment.' => 'Il n\'y a pas de lien interne pour le moment.', + 'Assign to me' => 'Assigner à moi', + 'Me' => 'Moi', + 'Do not duplicate anything' => 'Ne rien dupliquer', + 'Projects management' => 'Gestion des projets', + 'Users management' => 'Gestion des utilisateurs', + 'Groups management' => 'Gestion des groupes', + 'Create from another project' => 'Créer depuis un autre projet', + 'There is no subtask at the moment.' => 'Il n\'y a aucune sous-tâche pour le moment.', + 'open' => 'ouvert', + 'closed' => 'fermé', + 'Priority:' => 'Priorité :', + 'Reference:' => 'Référence :', + 'Complexity:' => 'Complexité :', + 'Swimlane:' => 'Swimlane :', + 'Column:' => 'Colonne :', + 'Position:' => 'Position :', + 'Creator:' => 'Créateur :', + 'Time estimated:' => 'Temps estimé :', + '%s hours' => '%s heures', + 'Time spent:' => 'Temps passé :', + 'Created:' => 'Créé le :', + 'Modified:' => 'Modifié le :', + 'Completed:' => 'Terminé le :', + 'Started:' => 'Commençé le :', + 'Moved:' => 'Déplacé le : ', + 'Task #%d' => 'Tâche n°%d', + 'Sub-tasks' => 'Sous-tâches', + 'Date and time format' => 'Format de la date et de l\'heure', + 'Time format' => 'Format de l\'heure', + 'Start date: ' => 'Date de début : ', + 'End date: ' => 'Date de fin : ', + 'New due date: ' => 'Nouvelle date d\'échéance : ', + 'Start date changed: ' => 'Date de début modifiée : ', + 'Disable private projects' => 'Désactiver les projets privés', + 'Do you really want to remove this custom filter: "%s"?' => 'Voulez-vous vraiment supprimer ce filtre personnalisé : « %s » ?', + 'Remove a custom filter' => 'Supprimer un filtre personnalisé', + 'User activated successfully.' => 'Utilisateur activé avec succès.', + 'Unable to enable this user.' => 'Impossible d\'activer cet utilisateur.', + 'User disabled successfully.' => 'Utilisateur désactivé avec succès.', + 'Unable to disable this user.' => 'Impossible de désactiver cet utilisateur.', + 'All files have been uploaded successfully.' => 'Tous les fichiers ont été uploadés avec succès.', + 'View uploaded files' => 'Voir les fichiers uploadés', + 'The maximum allowed file size is %sB.' => 'La taille maximale autorisée pour les fichiers est de %so.', + 'Choose files again' => 'Choisir de nouveau des fichiers', + 'Drag and drop your files here' => 'Glissez-déposez vos fichiers ici', + 'choose files' => 'choisissez des fichiers', + 'View profile' => 'Voir le profile', + 'Two Factor' => 'Deux-Facteurs', + 'Disable user' => 'Désactiver l\'utilisateur', + 'Do you really want to disable this user: "%s"?' => 'Voulez-vous vraiment désactiver cet utilisateur : « %s » ?', + 'Enable user' => 'Activer un utilisateur', + 'Do you really want to enable this user: "%s"?' => 'Voulez-vous vraiment activer cet utilisateur : « %s » ?', + 'Download' => 'Télécharger', + 'Uploaded: %s' => 'Uploadé : %s', + 'Size: %s' => 'Taille : %s', + 'Uploaded by %s' => 'Uploadé par %s', + 'Filename' => 'Nom du fichier', + 'Size' => 'Taille', + 'Column created successfully.' => 'La colonne a été créée avec succès.', + 'Another column with the same name exists in the project' => 'Une autre colonne existe avec le même nom dans le projet', + 'Default filters' => 'Filtres par défaut', + 'Your board doesn\'t have any column!' => 'Votre tableau n\'a aucune colonne', + 'Change column position' => 'Changer la position de la colonne', + 'Switch to the project overview' => 'Aller à l\'aperçu du projet', + 'User filters' => 'Filtres des utilisateurs', + 'Category filters' => 'Filtres des catégories', + 'Upload a file' => 'Uploader un fichier', + 'There is no attachment at the moment.' => 'Il n\'y a aucune pièce-jointe pour le moment.', + 'View file' => 'Voir le fichier', + 'Last activity' => 'Dernières activités', + 'Change subtask position' => 'Changer la position de la sous-tâche', + 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', + 'Another swimlane with the same name exists in the project' => 'Une autre swimlane existe avec le même nom dans le projet', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemple : http://exemple.kanboard.net/ (utilisé pour générer les URLs absolues)', + 'Actions duplicated successfully.' => 'Actions dupliquées avec succès.', + 'Unable to duplicate actions.' => 'Impossible de dupliquer les actions.', + 'Add a new action' => 'Ajouter une nouvelle action', + 'Import from another project' => 'Importer depuis un autre projet', + 'There is no action at the moment.' => 'Il n\'y a aucune action pour le moment.', + 'Import actions from another project' => 'Importer les actions depuis un autre projet', + 'There is no available project.' => 'Il n\'y a pas de projet disponible.', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 25d55bb2..2eb1372d 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Szerkesztés', 'remove' => 'törlés', 'Remove' => 'Törlés', - 'Update' => 'Frissítés', 'Yes' => 'Igen', 'No' => 'Nem', 'cancel' => 'Mégsem', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Műveletek', 'Inactive' => 'Inaktív', 'Active' => 'Aktív', - 'Add this column' => 'Oszlop hozzáadása', '%d tasks on the board' => '%d feladat a táblán', '%d tasks in total' => 'Összesen %d feladat', 'Unable to update this board.' => 'Nem lehet frissíteni a táblát.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Projekt törlése', 'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"', 'All projects' => 'Minden projekt', - 'Change columns' => 'Oszlop módosítása', 'Add a new column' => 'Új oszlop', 'Title' => 'Cím', 'Nobody assigned' => 'Nincs felelős', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Feladat felnyitás', 'Do you really want to open this task: "%s"?' => 'Tényleg meg akarja nyitni ezt a feladatot: "%s"?', 'Back to the board' => 'Vissza a táblához', - 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y. %m. %d. %H:%M', 'There is nobody assigned' => 'Nincs felelős', 'Column on the board:' => 'Tábla oszlopa: ', - 'Status is open' => 'Nyitott állapot', - 'Status is closed' => 'Zárt állapot', 'Close this task' => 'Feladat lezárása', 'Open this task' => 'Feladat felnyitása', 'There is no description.' => 'Nincs elérhető leírás.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Folyamatban', 'Done' => 'Kész', 'Application version:' => 'Alkalmazás verzió:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Elkészült: %Y. %m. %d. %H:%M', - '%B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M', - 'Date created' => 'Létrehozás időpontja', - 'Date completed' => 'Befejezés időpontja', 'Id' => 'ID', '%d closed tasks' => '%d lezárt feladat', 'No task for this project' => 'Nincs feladat ebben a projektben', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Feladat módosítása', 'Due Date' => 'Határidő', 'Invalid date' => 'Érvénytelen dátum', - 'Must be done before %B %e, %Y' => 'Kész kell lennie %Y. %m. %d. előtt', - '%B %e, %Y' => '%Y. %m. %d.', - '%b %e, %Y' => '%Y. %m. %d.', 'Automatic actions' => 'Automatikus intézkedések', 'Your automatic action have been created successfully.' => 'Az automatikus intézkedés sikeresen elkészült.', 'Unable to create your automatic action.' => 'Automatikus intézkedés létrehozása nem lehetséges.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Intézkedés törlése nem lehetséges.', 'Action removed successfully.' => 'Intézkedés sikeresen törölve.', 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben: "%s"', - 'Defined actions' => 'Intézkedések', 'Add an action' => 'Intézkedés létrehozása', 'Event name' => 'Esemény neve', 'Action name' => 'Intézkedés neve', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Ha a kiválasztott esemény bekövetkezik, hajtsa végre a megfelelő intézkedéseket.', 'Next step' => 'Következő lépés', 'Define action parameters' => 'Határozza meg az intézkedés paramétereit', - 'Save this action' => 'Intézkedés mentése', 'Do you really want to remove this action: "%s"?' => 'Valóban törölni akarja ezt az intézkedést: "%s"?', 'Remove an automatic action' => 'Automatikus intézkedés törlése', 'Assign the task to a specific user' => 'Feladat kiosztása megadott felhasználónak', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Szín hozzárendelése a felhasználóhoz', 'Column title' => 'Oszlopfejléc', 'Position' => 'Pozíció', - 'Move Up' => 'Fel', - 'Move Down' => 'Le', 'Duplicate to another project' => 'Másolás másik projektbe', 'Duplicate' => 'Másolás', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Megjegyzés sikeresen törölve.', 'Unable to remove this comment.' => 'Megjegyzés törölése nem lehetséges.', 'Do you really want to remove this comment?' => 'Valóban törölni szeretné ezt a megjegyzést?', - 'Only administrators or the creator of the comment can access to this page.' => 'Csak a rendszergazdák és a megjegyzés létrehozója férhet hozzá az oldalhoz.', 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava: "%s"', 'The current password is required' => 'A jelenlegi jelszót meg kell adni', 'Wrong password' => 'Hibás jelszó', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'E-mail', - 'Link my Google Account' => 'Kapcsold össze a Google fiókkal', - 'Unlink my Google Account' => 'Válaszd le a Google fiókomat', - 'Login with my Google Account' => 'Jelentkezzen be Google fiókkal', - 'Project not found.' => 'A projekt nem található.', 'Task removed successfully.' => 'Feladat sikeresen törölve.', 'Unable to remove this task.' => 'A feladatot nem lehet törölni.', 'Remove a task' => 'Feladat törlése', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maximális méret: ', 'Unable to upload the file.' => 'Fájl feltöltése nem lehetséges.', 'Display another project' => 'Másik projekt megjelenítése', - 'Login with my Github Account' => 'Jelentkezzen be Github fiókkal', - 'Link my Github Account' => 'Github fiók csatolása', - 'Unlink my Github Account' => 'Github fiók leválasztása', 'Created by %s' => 'Készítette: %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás: %Y. %m. %d. %H:%M', 'Tasks Export' => 'Feladatok exportálása', 'Tasks exportation for "%s"' => 'Feladatok exportálása: "%s"', 'Start Date' => 'Kezdés dátuma', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Csak ezekről a projektekről kérek értesítést:', 'view the task on Kanboard' => 'feladat megtekintése a Kanboardon', 'Public access' => 'Nyilvános hozzáférés', - 'User management' => 'Felhasználók kezelése', 'Active tasks' => 'Aktív feladatok', 'Disable public access' => 'Nyilvános hozzáférés letiltása', 'Enable public access' => 'Nyilvános hozzáférés engedélyezése', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Jelszó módosítása', 'Password modification' => 'Jelszó módosítása', 'External authentications' => 'Külső azonosítás', - 'Google Account' => 'Google fiók', - 'Github Account' => 'Github fiók', 'Never connected.' => 'Sosem csatlakozva.', - 'No account linked.' => 'Nincs csatlakoztatott fiók.', - 'Account linked.' => 'Fiók csatlakoztatva.', 'No external authentication enabled.' => 'A külső azonosítás nincs engedélyezve.', 'Password modified successfully.' => 'A jelszó sikeresen módosítva.', 'Unable to change the password.' => 'A jelszó módosítása sikertelen.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján', 'Change the category based on an external label' => 'Kategória módosítása külső címke alapján', 'Reference' => 'Hivatkozás', - 'Reference: %s' => 'Hivatkozás: %s', 'Label' => 'Címke', 'Database' => 'Adatbázis', 'About' => 'Kanboard információ', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Gyakoriság másodpercben (alapértelmezetten 60 másodperc)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Gyakoriság másodpercben (0 funkció letiltva, alapértelmezetten 10 másodperc)', 'Application URL' => 'Alkalmazás URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben használt)', 'Token regenerated.' => 'Token újragenerálva.', 'Date format' => 'Dátum formátum', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Ez egy privát projekt', 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot', 'Add' => 'Hozzáadás', - 'Estimated time: %s hours' => 'Becsült idő: %s óra', - 'Time spent: %s hours' => 'Eltöltött idő: %s óra', - 'Started on %B %e, %Y' => 'Elkezdve: %Y. %m. %d.', 'Start date' => 'Kezdés dátuma', 'Time estimated' => 'Becsült időtartam', 'There is nothing assigned to you.' => 'Nincs kiosztott feladat.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?', 'Inactive swimlanes' => 'Inaktív folyamatok', 'Remove a swimlane' => 'Folyamat törlés', - 'Rename' => 'Átnevezés', 'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése', 'Swimlane modification for the project "%s"' => '%s projekt folyamatainak módosítása', 'Swimlane not found.' => 'Folyamat nem található', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Folyamatok', 'Swimlane updated successfully.' => 'Folyamat sikeresn frissítve', 'The default swimlane have been updated successfully.' => 'Az alapértelmezett folyamat sikeresen frissítve.', - 'Unable to create your swimlane.' => 'A folyamat létrehozása sikertelen.', 'Unable to remove this swimlane.' => 'A folyamat törlése sikertelen.', 'Unable to update this swimlane.' => 'A folyamat frissítése sikertelen.', 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Ez a feladat', '<1h' => '<1ó', '%dh' => '%dó', - '%b %e' => '%b %e', 'Expand tasks' => 'Feladatok lenyitása', 'Collapse tasks' => 'Feladatok összecsukása', 'Expand/collapse tasks' => 'Feladatok lenyitása/összecsukása', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Billentyű kombinációk', 'Open board switcher' => 'Tábla választó lenyitása', 'Application' => 'Alkalmazás', - 'since %B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M óta', 'Compact view' => 'Kompakt nézet', 'Horizontal scrolling' => 'Vízszintes görgetés', 'Compact/wide view' => 'Kompakt/széles nézet', 'No results match:' => 'Nincs találat:', 'Currency' => 'Pénznem', - 'Files' => 'Fájlok', - 'Images' => 'Képek', 'Private project' => 'Privát projekt', 'AUD - Australian Dollar' => 'AUD - Ausztrál dollár', 'CAD - Canadian Dollar' => 'CAD - Kanadai dollár', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', // 'I want to receive notifications for:' => '', // 'All tasks' => '', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( // 'Not assigned' => '', // 'View advanced search syntax' => '', // 'Overview' => '', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index e3316405..dbbf4db5 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Modifikasi', 'remove' => 'hapus', 'Remove' => 'Hapus', - 'Update' => 'Perbaharui', 'Yes' => 'Ya', 'No' => 'Tidak', 'cancel' => 'batal', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Tindakan', 'Inactive' => 'Non Aktif', 'Active' => 'Aktif', - 'Add this column' => 'Tambahkan kolom ini', '%d tasks on the board' => '%d tugas di papan', '%d tasks in total' => '%d tugas di total', 'Unable to update this board.' => 'Tidak dapat memperbaharui papan ini', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Hapus proyek', 'Edit the board for "%s"' => 'Rubah papan untuk « %s »', 'All projects' => 'Semua proyek', - 'Change columns' => 'Rubah kolom', 'Add a new column' => 'Tambah kolom baru', 'Title' => 'Judul', 'Nobody assigned' => 'Tidak ada yang ditugaskan', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Buka tugas', 'Do you really want to open this task: "%s"?' => 'Apakah anda yakin akan membuka tugas ini : « %s » ?', 'Back to the board' => 'Kembali ke papan', - 'Created on %B %e, %Y at %k:%M %p' => 'Dibuat pada tanggal %d/%m/%Y à %H:%M', 'There is nobody assigned' => 'Tidak ada orang yand ditugaskan', 'Column on the board:' => 'Kolom di dalam papan : ', - 'Status is open' => 'Status terbuka', - 'Status is closed' => 'Status ditutup', 'Close this task' => 'Tutup tugas ini', 'Open this task' => 'Buka tugas ini', 'There is no description.' => 'Tidak ada deskripsi.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Sedang dalam pengerjaan', 'Done' => 'Selesai', 'Application version:' => 'Versi aplikasi :', - 'Completed on %B %e, %Y at %k:%M %p' => 'Diselesaikan pada tanggal %d/%m/%Y à %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', - 'Date created' => 'Tanggal dibuat', - 'Date completed' => 'Tanggal diselesaikan', 'Id' => 'Id.', '%d closed tasks' => '%d tugas yang ditutup', 'No task for this project' => 'Tidak ada tugas dalam proyek ini', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Modifikasi tugas ini', 'Due Date' => 'Batas Tanggal Terakhir', 'Invalid date' => 'Tanggal tidak valid', - 'Must be done before %B %e, %Y' => 'Harus diselesaikan sebelum tanggal %d/%m/%Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Tindakan otomatis', 'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.', 'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Tidak dapat menghapus tindakan ini', 'Action removed successfully.' => 'Tindakan berhasil dihapus.', 'Automatic actions for the project "%s"' => 'Tindakan otomatis untuk proyek ini « %s »', - 'Defined actions' => 'Tindakan didefinisikan', 'Add an action' => 'Tambah tindakan', 'Event name' => 'Nama acara', 'Action name' => 'Nama tindakan', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Ketika acara yang dipilih terjadi, melakukan tindakan yang sesuai.', 'Next step' => 'Langkah selanjutnya', 'Define action parameters' => 'Definisi parameter tindakan', - 'Save this action' => 'Simpan tindakan ini', 'Do you really want to remove this action: "%s"?' => 'Apakah anda yakin akan menghapus tindakan ini « %s » ?', 'Remove an automatic action' => 'Hapus tindakan otomatis', 'Assign the task to a specific user' => 'Menetapkan tugas untuk pengguna tertentu', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu', 'Column title' => 'Judul kolom', 'Position' => 'Posisi', - 'Move Up' => 'Pindah ke atas', - 'Move Down' => 'Pindah ke bawah', 'Duplicate to another project' => 'Duplikasi ke proyek lain', 'Duplicate' => 'Duplikasi', 'link' => 'tautan', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Komentar berhasil dihapus.', 'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.', 'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Hanya administrator atau pembuat komentar yang dapat mengakses halaman ini.', 'Current password for the user "%s"' => 'Kata sandi saat ini untuk pengguna « %s »', 'The current password is required' => 'Kata sandi saat ini diperlukan', 'Wrong password' => 'Kata sandi salah', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Otentifikasi eksternal gagal', 'Your external account is linked to your profile successfully.' => 'Akun eksternal anda berhasil dihubungkan ke profil anda.', 'Email' => 'Email', - 'Link my Google Account' => 'Hubungkan akun Google saya', - 'Unlink my Google Account' => 'Putuskan akun Google saya', - 'Login with my Google Account' => 'Masuk menggunakan akun Google saya', - 'Project not found.' => 'Proyek tidak ditemukan.', 'Task removed successfully.' => 'Tugas berhasil dihapus.', 'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.', 'Remove a task' => 'Hapus tugas', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Ukuran maksimum: ', 'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.', 'Display another project' => 'Lihat proyek lain', - 'Login with my Github Account' => 'Masuk menggunakan akun Github saya', - 'Link my Github Account' => 'Hubungkan akun Github saya ', - 'Unlink my Github Account' => 'Putuskan akun Github saya', 'Created by %s' => 'Dibuat oleh %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M', 'Tasks Export' => 'Ekspor Tugas', 'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »', 'Start Date' => 'Tanggal Mulai', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk proyek-proyek yang dipilih :', 'view the task on Kanboard' => 'lihat tugas di Kanboard', 'Public access' => 'Akses publik', - 'User management' => 'Manajemen pengguna', 'Active tasks' => 'Tugas aktif', 'Disable public access' => 'Nonaktifkan akses publik', 'Enable public access' => 'Aktifkan akses publik', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Rubah kata sandri', 'Password modification' => 'Modifikasi kata sandi', 'External authentications' => 'Otentifikasi eksternal', - 'Google Account' => 'Akun Google', - 'Github Account' => 'Akun Github', 'Never connected.' => 'Tidak pernah terhubung.', - 'No account linked.' => 'Tidak ada akun terhubung.', - 'Account linked.' => 'Akun terhubung.', 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', 'Password modified successfully.' => 'Kata sandi berhasil dimodifikasi.', 'Unable to change the password.' => 'Tidak dapat merubah kata sandir.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', 'Reference' => 'Referensi', - 'Reference: %s' => 'Referensi : %s', 'Label' => 'Label', 'Database' => 'Basis data', 'About' => 'Tentang', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 detik)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)', 'Application URL' => 'URL Aplikasi', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Contoh : http://exemple.kanboard.net/ (digunakan untuk pemberitahuan email)', 'Token regenerated.' => 'Token diregenerasi.', 'Date format' => 'Format tanggal', 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalu diterima, contoh : « %s » et « %s »', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Proyek ini adalah pribadi', 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', 'Add' => 'Tambah', - 'Estimated time: %s hours' => 'Perkiraan waktu: %s jam', - 'Time spent: %s hours' => 'Waktu dihabiskan : %s jam', - 'Started on %B %e, %Y' => 'Dimulai pada %d/%m/%Y', 'Start date' => 'Tanggal mulai', 'Time estimated' => 'Perkiraan waktu', 'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Apakah anda yakin akan menghapus swimlane ini : « %s » ?', 'Inactive swimlanes' => 'Swimlanes tidak aktif', 'Remove a swimlane' => 'Supprimer une swimlane', - 'Rename' => 'Ganti nama', 'Show default swimlane' => 'Perlihatkan standar swimlane', 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk proyek « %s »', 'Swimlane not found.' => 'Swimlane tidak ditemukan.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane berhasil diperbaharui.', 'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.', - 'Unable to create your swimlane.' => 'Tidak dapat membuat swimlane anda.', 'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.', 'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.', 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Tugas ini', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Perluas tugas', 'Collapse tasks' => 'Lipat tugas', 'Expand/collapse tasks' => 'Perluas/lipat tugas', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'pintas keyboard', 'Open board switcher' => 'Buka table switcher', 'Application' => 'Aplikasi', - 'since %B %e, %Y at %k:%M %p' => 'sejak %d/%m/%Y à %H:%M', 'Compact view' => 'Tampilan kompak', 'Horizontal scrolling' => 'Horisontal bergulir', 'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas', 'No results match:' => 'Tidak ada hasil :', 'Currency' => 'Mata uang', - 'Files' => 'Arsip', - 'Images' => 'Gambar', 'Private project' => 'Proyek pribadi', 'AUD - Australian Dollar' => 'AUD - Dollar Australia', 'CAD - Canadian Dollar' => 'CAD - Dollar Kanada', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Menguji perangkat anda', 'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'diunggah oleh %s', - 'uploaded on: %s' => 'diunggah pada %s', - 'size: %s' => 'ukuran : %s', 'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »', 'Burndown chart' => 'Grafik Burndown', 'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri', 'The exact same link already exists' => 'Tautan yang sama persis sudah ada', 'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan', - 'Recurring information' => 'Informasi berulang', 'Score' => 'Skor', 'The identifier must be unique' => 'Identifier harus unik', 'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Waktu yang dihabiskan berubah : %sh', 'Time estimated changed: %sh' => 'Perkiraan waktu berubah : %sh', 'The field "%s" have been updated' => 'Field « %s » telah diperbaharui', - 'The description have been modified' => 'Deskripsi telah dimodifikasi', + 'The description has been modified' => 'Deskripsi telah dimodifikasi', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?', - 'Swimlane: %s' => 'Swimlane : %s', 'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :', 'All tasks' => 'Semua tugas', 'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya', 'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya', 'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Tanggal jatuh tempo baru : %d/%m/%Y', - 'Start date changed: %B %e, %Y' => 'Tanggal mulai berubah : %d/%m/%Y', - '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', 'Total for all columns' => 'Total untuk semua kolom', 'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Tidak ditugaskan', 'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan', 'Overview' => 'Ikhtisar', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar', 'Switch to the board view' => 'Beralih ke tampilan papan', 'Switch to the calendar view' => 'Beralih ke tampilan kalender', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.', 'Average time into each column' => 'Rata-rata waktu ke setiap kolom', 'Lead and cycle time' => 'Lead dan siklus waktu', - 'Google Authentication' => 'Google Otentifikasi', - 'Help on Google authentication' => 'Bantuan pada otentifikasi Google', - 'Github Authentication' => 'Otentifikasi Github', - 'Help on Github authentication' => 'Bantuan pada otentifikasi Github', 'Lead time: ' => 'Lead time : ', 'Cycle time: ' => 'Siklus waktu : ', 'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.', 'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai', 'Edit Authentication' => 'Modifikasi Otentifikasi', - 'Google Id' => 'Id Google', - 'Github Id' => 'Id Github', 'Remote user' => 'Pengguna jauh', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata sandi mereka dalam basis data Kanboard, contoh: akun LDAP, Google dan Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', 'New remote user' => 'Pengguna baru jauh', 'New local user' => 'Pengguna baru lokal', 'Default task color' => 'Standar warna tugas', - 'Hide sidebar' => 'Sembunyikan sidebar', - 'Expand sidebar' => 'Perluas sidebar', 'This feature does not work with all browsers.' => 'Fitur ini tidak dapat digunakan di semua browsers', 'There is no destination project available.' => 'Tidak ada destinasi proyek yang tersedia.', 'Trigger automatically subtask time tracking' => 'Otomatis memicu pelacakan untuk subtugas', @@ -901,7 +840,6 @@ return array( 'open file' => 'buka berkas', 'End date' => 'Waktu berakhir', 'Users overview' => 'Ikhtisar pengguna', - 'Managers' => 'Manajer', 'Members' => 'Anggota', 'Shared project' => 'Proyek bersama', 'Project managers' => 'Manajer proyek', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Waktu berakhir :', 'There is no start date or end date for this project.' => 'Tidak ada waktu mulai atau waktu berakhir untuk proyek ini', 'Projects Gantt chart' => 'Proyek grafik Gantt', - 'Start date: %s' => 'Waktu mulai : %s', - 'End date: %s' => 'Waktu berakhir : %s', 'Link type' => 'Tipe tautan', 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan tautan tugas yang spesifik', 'Task link creation or modification' => 'Tautan pembuatan atau modifikasi tugas ', - 'Login with my Gitlab Account' => 'Masuk menggunakan akun Gitlab saya', 'Milestone' => 'Milestone', - 'Gitlab Authentication' => 'Authentification Gitlab', - 'Help on Gitlab authentication' => 'Bantuan pada otentifikasi Gitlab', - 'Gitlab Id' => 'Id Gitlab', - 'Gitlab Account' => 'Akun Gitlab', - 'Link my Gitlab Account' => 'Hubungkan akun Gitlab saya', - 'Unlink my Gitlab Account' => 'Putuskan akun Gitlab saya', 'Documentation: %s' => 'Dokumentasi : %s', 'Switch to the Gantt chart view' => 'Beralih ke tampilan grafik Gantt', 'Reset the search/filter box' => 'Atur ulang pencarian/kotak filter', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 30a21bd9..afde8265 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Modifica', 'remove' => 'cancella', 'Remove' => 'Cancella', - 'Update' => 'Aggiorna', 'Yes' => 'Si', 'No' => 'No', 'cancel' => 'annulla', @@ -55,12 +54,11 @@ return array( 'No project' => 'Nessun progetto', 'Project' => 'Progetto', 'Status' => 'Stato', - 'Tasks' => 'Tasks', + 'Tasks' => 'Task', 'Board' => 'Bacheca', 'Actions' => 'Azioni', 'Inactive' => 'Inattivo', 'Active' => 'Attivo', - 'Add this column' => 'Aggiungi questa colonna', '%d tasks on the board' => '%d task sulla bacheca', '%d tasks in total' => '%d task in totale', 'Unable to update this board.' => 'Impossibile aggiornare questa bacheca.', @@ -68,11 +66,10 @@ return array( 'Disable' => 'Disattiva', 'Enable' => 'Attiva', 'New project' => 'Nuovo progetto', - 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare il seguente progetto: "%s" ?', + 'Do you really want to remove this project: "%s"?' => 'Vuoi davvero eliminare il seguente progetto: "%s" ?', 'Remove project' => 'Cancella il progetto', 'Edit the board for "%s"' => 'Modifica la bacheca per "%s"', 'All projects' => 'Tutti i progetti', - 'Change columns' => 'Cambia le colonne', 'Add a new column' => 'Aggiungi una nuova colonna', 'Title' => 'Titolo', 'Nobody assigned' => 'Nessuno assegnato', @@ -80,7 +77,7 @@ return array( 'Remove a column' => 'Cancella questa colonna', 'Remove a column from a board' => 'Cancella una colonna da una bacheca', 'Unable to remove this column.' => 'Impossibile cancellare questa colonna.', - 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna: "%s" ?', + 'Do you really want to remove this column: "%s"?' => 'Desideri davvero cancellare questa colonna: "%s" ?', 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I TASK legati a questa colonna!', 'Settings' => 'Impostazioni', 'Application settings' => 'Impostazioni dell\'applicazione', @@ -100,13 +97,10 @@ return array( 'Create another task' => 'Crea un nuovo task', 'New task' => 'Nuovo task', 'Open a task' => 'Apri un task', - 'Do you really want to open this task: "%s"?' => 'Veramente desideri aprire questo task: "%s" ?', + 'Do you really want to open this task: "%s"?' => 'Desideri davvero aprire questo task: "%s" ?', 'Back to the board' => 'Torna alla bacheca', - 'Created on %B %e, %Y at %k:%M %p' => 'Creato il %B %e, %Y alle %k:%M %p', 'There is nobody assigned' => 'Nessuno è assegnato a questo task', 'Column on the board:' => 'Colonna sulla bacheca: ', - 'Status is open' => 'Stato aperto', - 'Status is closed' => 'Stato chiuso', 'Close this task' => 'Chiudi questo task', 'Open this task' => 'Apri questo task', 'There is no description.' => 'Nessuna descrizione presente.', @@ -125,43 +119,39 @@ return array( 'The project id is required' => 'Si richiede l\'identificatore del progetto', 'The project name is required' => 'Si richiede il nome del progetto', 'The title is required' => 'Si richiede un titolo', - 'Settings saved successfully.' => 'Impostazioni salvate correttamente.', + 'Settings saved successfully.' => 'Impostazioni salvate con successo.', 'Unable to save your settings.' => 'Impossibile salvare le impostazioni.', 'Database optimization done.' => 'Ottimizzazione della base dati conclusa.', - 'Your project have been created successfully.' => 'Il tuo progetto è stato creato correttamente.', + 'Your project have been created successfully.' => 'Il tuo progetto è stato creato con successo.', 'Unable to create your project.' => 'Impossibile creare il progetto.', - 'Project updated successfully.' => 'Progetto aggiornato correttamente.', + 'Project updated successfully.' => 'Progetto aggiornato con successo.', 'Unable to update this project.' => 'Impossibile aggiornare il progetto.', 'Unable to remove this project.' => 'Impossibile cancellare questo progetto.', - 'Project removed successfully.' => 'Progetto cancellato correttamente.', - 'Project activated successfully.' => 'Progetto attivato correttamente.', + 'Project removed successfully.' => 'Progetto cancellato con successo.', + 'Project activated successfully.' => 'Progetto attivato con successo.', 'Unable to activate this project.' => 'Impossibile attivare il progetto.', - 'Project disabled successfully.' => 'Progetto disattivato correttamente.', + 'Project disabled successfully.' => 'Progetto disattivato con successo.', 'Unable to disable this project.' => 'Impossibile disattivare il progetto.', 'Unable to open this task.' => 'Impossibile aprire questo task.', - 'Task opened successfully.' => 'Il task è stato aperto correttamente.', + 'Task opened successfully.' => 'Il task è stato aperto con successo.', 'Unable to close this task.' => 'Impossibile chiudere questo task.', - 'Task closed successfully.' => 'Task chiuso correttamente.', + 'Task closed successfully.' => 'Task chiuso con successo.', 'Unable to update your task.' => 'Impossibile modificare questo task.', - 'Task updated successfully.' => 'Task modificato correttamente.', + 'Task updated successfully.' => 'Task modificato con successo.', 'Unable to create your task.' => 'Impossibile creare questo task.', - 'Task created successfully.' => 'Task creato correttamente.', - 'User created successfully.' => 'Utente creato correttamente.', + 'Task created successfully.' => 'Task creato con successo.', + 'User created successfully.' => 'Utente creato con successo.', 'Unable to create your user.' => 'Impossibile creare l\'utente.', - 'User updated successfully.' => 'Utente aggiornato correttamente.', + 'User updated successfully.' => 'Utente aggiornato con successo.', 'Unable to update your user.' => 'Impossibile aggiornare questo utente.', - 'User removed successfully.' => 'Utente cancellato correttamente.', + 'User removed successfully.' => 'Utente cancellato con successo.', 'Unable to remove this user.' => 'Impossibile cancellare questo utente.', - 'Board updated successfully.' => 'Bacheca aggiornata correttamente.', + 'Board updated successfully.' => 'Bacheca aggiornata con successo.', 'Ready' => 'Pronto', 'Backlog' => 'In attesa', 'Work in progress' => 'In corso', 'Done' => 'Fatto', 'Application version:' => 'Versione dell\'applicazione:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Completato il %B %e, %Y alle %k:%M %p', - // '%B %e, %Y at %k:%M %p' => '', - 'Date created' => 'Data di creazione', - 'Date completed' => 'Data di termine', // 'Id' => '', '%d closed tasks' => '%d task chiusi', 'No task for this project' => 'Nessun task per questo progetto', @@ -180,22 +170,18 @@ return array( 'Leave a comment' => 'Lascia un commento', 'Comment is required' => 'Si richiede un commento', 'Leave a description' => 'Lascia una descrizione', - 'Comment added successfully.' => 'Commenti aggiunti correttamente.', + 'Comment added successfully.' => 'Commenti aggiunti con successo.', 'Unable to create your comment.' => 'Impossibile creare questo commento.', 'Edit this task' => 'Modifica questo task', 'Due Date' => 'Data di scadenza', 'Invalid date' => 'Data non valida', - 'Must be done before %B %e, %Y' => 'Deve essere completato prima del %B %e, %Y', - // '%B %e, %Y' => '', - // '%b %e, %Y' => '', 'Automatic actions' => 'Azioni automatiche', - 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.', + 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata con successo.', 'Unable to create your automatic action.' => 'Impossibile creare quest\'azione automatica.', 'Remove an action' => 'Cancellare un\'azione', 'Unable to remove this action.' => 'Impossibile cancellare questa azione.', - 'Action removed successfully.' => 'Azione cancellata correttamente.', + 'Action removed successfully.' => 'Azione cancellata con successo.', 'Automatic actions for the project "%s"' => 'Azioni automatiche per il progetto "%s"', - 'Defined actions' => 'Azioni definite', 'Add an action' => 'Aggiungi un\'azione', 'Event name' => 'Nome dell\'evento', 'Action name' => 'Nome dell\'azione', @@ -205,8 +191,7 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Quando si verifica l\'evento selezionato, eseguire l\'azione corrispondente.', 'Next step' => 'Passo successivo', 'Define action parameters' => 'Definire i parametri dell\'azione', - 'Save this action' => 'Salva questa azione', - 'Do you really want to remove this action: "%s"?' => 'Vuoi veramente cancellare la seguente azione: "%s"?', + 'Do you really want to remove this action: "%s"?' => 'Vuoi davvero cancellare la seguente azione: "%s"?', 'Remove an automatic action' => 'Cancella un\'azione automatica', 'Assign the task to a specific user' => 'Assegna il task ad un utente specifico', 'Assign the task to the person who does the action' => 'Assegna il task all\'utente che compie l\'azione', @@ -218,18 +203,15 @@ return array( 'Assign a color to a specific user' => 'Assegna un colore ad un utente specifico', 'Column title' => 'Titolo della colonna', 'Position' => 'Posizione', - 'Move Up' => 'Sposta in alto', - 'Move Down' => 'Sposta in basso', 'Duplicate to another project' => 'Duplica in un altro progetto', 'Duplicate' => 'Duplica', 'link' => 'relazione', - 'Comment updated successfully.' => 'Commento aggiornato correttamente.', + 'Comment updated successfully.' => 'Commento aggiornato con successo.', 'Unable to update your comment.' => 'Impossibile aggiornare questo commento.', 'Remove a comment' => 'Cancella un commento', - 'Comment removed successfully.' => 'Commento cancellato correttamente.', + 'Comment removed successfully.' => 'Commento cancellato con successo.', 'Unable to remove this comment.' => 'Impossibile cancellare questo commento.', 'Do you really want to remove this comment?' => 'Vuoi davvero cancellare questo commento?', - 'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.', 'Current password for the user "%s"' => 'Password attuale per l\'utente "%s"', 'The current password is required' => 'Si richiede la password attuale', 'Wrong password' => 'Password errata', @@ -260,11 +242,7 @@ return array( 'External authentication failed' => 'Autenticazione esterna fallita', 'Your external account is linked to your profile successfully.' => 'Il tuo account esterno è stato collegato al tuo profilo con successo.', 'Email' => 'E-mail', - 'Link my Google Account' => 'Collegare il mio Account di Google', - 'Unlink my Google Account' => 'Scollegare il mio account di Google', - 'Login with my Google Account' => 'Entra con il mio Account di Google', - 'Project not found.' => 'progetto non trovato.', - 'Task removed successfully.' => 'Task cancellato correttamente.', + 'Task removed successfully.' => 'Task cancellato con successo.', 'Unable to remove this task.' => 'Impossibile cancellare questo task.', 'Remove a task' => 'Cancella un task', 'Do you really want to remove this task: "%s"?' => 'Vuoi davvero cancellare questo task: "%s"?', @@ -275,25 +253,25 @@ return array( 'Category:' => 'Categoria:', 'Categories' => 'Categorie', 'Category not found.' => 'Categoria non trovata.', - 'Your category have been created successfully.' => 'La tua categoria è stata creata correttamente.', + 'Your category have been created successfully.' => 'La tua categoria è stata creata con successo.', 'Unable to create your category.' => 'Impossibile creare la tua categoria.', - 'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata correttamente.', + 'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata con successo.', 'Unable to update your category.' => 'Impossibile aggiornare la tua categoria.', 'Remove a category' => 'Cancella una categoria', - 'Category removed successfully.' => 'Categoria cancellata correttamente.', + 'Category removed successfully.' => 'Categoria cancellata con successo.', 'Unable to remove this category.' => 'Impossibile cancellare questa categoria.', 'Category modification for the project "%s"' => 'Modifica della categoria per il progetto "%s"', 'Category Name' => 'Nome della categoria', 'Add a new category' => 'Aggiungere una nuova categoria', - 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare la seguente categoria: "%s"?', + 'Do you really want to remove this category: "%s"?' => 'Vuoi davvero cancellare la seguente categoria: "%s"?', 'All categories' => 'Tutte le categorie', 'No category' => 'Senza categoria', 'The name is required' => 'Si richiede un nome', 'Remove a file' => 'Cancella un file', 'Unable to remove this file.' => 'Impossibile cancellare questo file.', - 'File removed successfully.' => 'File cancellato correttamente.', + 'File removed successfully.' => 'File cancellato con successo.', 'Attach a document' => 'Allega un documento', - 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?', + 'Do you really want to remove this file: "%s"?' => 'Vuoi davvero cancellare questo file: "%s"?', 'Attachments' => 'Allegati', 'Edit the task' => 'Modifica il task', 'Edit the description' => 'Modifica la descrizione', @@ -303,12 +281,12 @@ return array( // 'Time tracking' => '', 'Estimate:' => 'Stimato:', 'Spent:' => 'Trascorso:', - 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-task?', + 'Do you really want to remove this sub-task?' => 'Vuoi davvero cancellare questo sotto-task?', 'Remaining:' => 'Rimangono', 'hours' => 'ore', 'spent' => 'trascorse', 'estimated' => 'stimate', - 'Sub-Tasks' => 'Sotto-tasks', + 'Sub-Tasks' => 'Sotto-task', 'Add a sub-task' => 'Aggiungi un sotto-task', 'Original estimate' => 'Stima originale', 'Create another sub-task' => 'Crea un altro sotto-task', @@ -318,20 +296,16 @@ return array( 'The time must be a numeric value' => 'Il tempo deve essere un valore numerico', 'Todo' => 'Da fare', 'In progress' => 'In corso', - 'Sub-task removed successfully.' => 'Sotto-task cancellato correttamente.', + 'Sub-task removed successfully.' => 'Sotto-task cancellato con successo.', 'Unable to remove this sub-task.' => 'Impossibile cancellare questo sotto-task.', - 'Sub-task updated successfully.' => 'Sotto-task aggiornato correttamente.', + 'Sub-task updated successfully.' => 'Sotto-task aggiornato con successo.', 'Unable to update your sub-task.' => 'Impossibile aggiornare il tuo sotto-task.', 'Unable to create your sub-task.' => 'Impossibile creare il tuo sotto-task.', - 'Sub-task added successfully.' => 'Sotto-task aggiunto correttamente.', + 'Sub-task added successfully.' => 'Sotto-task aggiunto con successo.', 'Maximum size: ' => 'Dimensioni massime: ', 'Unable to upload the file.' => 'Impossibile caricare il file.', 'Display another project' => 'Mostra un altro progetto', - 'Login with my Github Account' => 'Accedi col tuo account di Github', - 'Link my Github Account' => 'Collega il mio account Github', - 'Unlink my Github Account' => 'Scollega il mio account di Github', 'Created by %s' => 'Creato da %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M', 'Tasks Export' => 'Export dei task', 'Tasks exportation for "%s"' => 'Export dei task per "%s"', 'Start Date' => 'Data d\'inizio', @@ -352,7 +326,7 @@ return array( 'Title:' => 'Titolo', 'Status:' => 'Stato', 'Assignee:' => 'Assegnatario:', - // 'Time tracking:' => 'Gestione del tempo:', + // 'Time tracking:' => '', 'New sub-task' => 'Nuovo sotto-task', 'New attachment added "%s"' => 'Nuovo allegato aggiunto "%s"', 'Comment updated' => 'Commento aggiornato', @@ -365,9 +339,8 @@ return array( 'Task closed' => 'Task chiuso', 'Task opened' => 'Task aperto', 'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:', - 'view the task on Kanboard' => 'vedi il task su Kanboard', + 'view the task on Kanboard' => 'visualizza il task su Kanboard', 'Public access' => 'Accesso pubblico', - 'User management' => 'Gestione utenti', 'Active tasks' => 'Task attivi', 'Disable public access' => 'Disabilita l\'accesso pubblico', 'Enable public access' => 'Abilita l\'accesso pubblico', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Cambia password', 'Password modification' => 'Modifica della password', 'External authentications' => 'Autenticazione esterna', - 'Google Account' => 'Account Google', - 'Github Account' => 'Account Github', 'Never connected.' => 'Mai connesso.', - 'No account linked.' => 'Nessun account collegato.', - 'Account linked.' => 'Account collegato.', 'No external authentication enabled.' => 'Nessuna autenticazione esterna abilitata.', 'Password modified successfully.' => 'Password modificata con successo.', 'Unable to change the password.' => 'Impossibile cambiare la password.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno', 'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna', 'Reference' => 'Riferimento', - 'Reference: %s' => 'Riferimento :%s', 'Label' => 'Etichetta', // 'Database' => '', 'About' => 'Informazioni', @@ -449,7 +417,7 @@ return array( 'Board settings' => 'Impostazioni bacheca', 'URL and token' => 'URL e token', 'Webhook settings' => 'Impostazione Webhook', - 'URL for task creation:' => 'URL per la creazione dei tasks:', + 'URL for task creation:' => 'URL per la creazione dei task:', 'Reset token' => 'Rigenera il token', 'API endpoint:' => 'Endpoint dell\'API:', 'Refresh interval for private board' => 'Intervallo di refresh per le bacheche private', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequenza in secondi (60 secondi di default)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequenza in secondi (0 secondi di default)', 'Application URL' => 'URL dell\'applicazione', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esempio: http://example.kanboard.net/ (usato dalle notifiche email)', 'Token regenerated.' => 'Token rigenerato.', 'Date format' => 'Formato data', 'ISO format is always accepted, example: "%s" and "%s"' => 'Il formato ISO è sempre accettato, esempio: "%s" e "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Questo progetto è privato', 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-task', 'Add' => 'Aggiungi', - 'Estimated time: %s hours' => 'Tempo stimato: %s ore', - 'Time spent: %s hours' => 'Tempo trascorso: %s ore', - 'Started on %B %e, %Y' => 'Avviato il %B %e, %Y', 'Start date' => 'Data di inizio', 'Time estimated' => 'Tempo stimato', 'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Vuoi davvero rimuovere la seguente corsia: "%s"?', 'Inactive swimlanes' => 'Corsie inattive', 'Remove a swimlane' => 'Rimuovi una corsia', - 'Rename' => 'Rinomina', 'Show default swimlane' => 'Mostra la corsia predefinita', 'Swimlane modification for the project "%s"' => 'Modifica corsia per il progetto "%s"', 'Swimlane not found.' => 'Corsia non trovata.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Corsie', 'Swimlane updated successfully.' => 'Corsia aggiornata con successo.', 'The default swimlane have been updated successfully.' => 'La corsia predefinita è stata aggiornata con successo.', - 'Unable to create your swimlane.' => 'Impossibile creare la corsia.', 'Unable to remove this swimlane.' => 'Impossibile rimuovere questa corsia.', 'Unable to update this swimlane.' => 'Impossibile aggiornare questa corsia.', 'Your swimlane have been created successfully.' => 'La tua corsia è stata creata con successo', @@ -568,7 +530,7 @@ return array( 'Select the new status of the subtask: "%s"' => 'Seleziona il nuovo status per il sotto-task: "%s"', 'Subtask timesheet' => 'Timesheet del sotto-task', 'There is nothing to show.' => 'Nulla da mostrare.', - // 'Time Tracking' => 'Gestione del tempo', + // 'Time Tracking' => '', 'You already have one subtask in progress' => 'Hai già un sotto-task in corso', 'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?', 'Disallow login form' => 'Disabilita il form di login', @@ -576,7 +538,7 @@ return array( 'End' => 'Fine', 'Task age in days' => 'Anzianità del task in giorni', 'Days in this column' => 'Giorni in questa colonna', - // '%dd' => '', + '%dd' => '%dg', 'Add a link' => 'Aggiungi una relazione', 'Add a new link' => 'Aggiungi una nuova relazione', 'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere la seguente relazione: "%s"?', @@ -584,7 +546,7 @@ return array( 'Field required' => 'Campo necessario', 'Link added successfully.' => 'Relazione aggiunta con successo.', 'Link updated successfully.' => 'Relazione aggiornata con successo.', - 'Link removed successfully.' => 'Relazione rimosso con successo.', + 'Link removed successfully.' => 'Relazione rimossa con successo.', 'Link labels' => 'Etichette delle relazioni', 'Link modification' => 'Modifica relazione', 'Links' => 'Relazioni', @@ -612,7 +574,6 @@ return array( 'This task' => 'Questo task', // '<1h' => '', // '%dh' => '', - // '%b %e' => '', 'Expand tasks' => 'Espandi i task', 'Collapse tasks' => 'Minimizza i task', 'Expand/collapse tasks' => 'Espandi/minimizza i task', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Scorciatoie da tastiera', 'Open board switcher' => 'Apri il selettore di bacheche', 'Application' => 'Applicazione', - 'since %B %e, %Y at %k:%M %p' => 'dal %B %e, %Y alle %k:%M %p', 'Compact view' => 'Vista compatta', 'Horizontal scrolling' => 'Scrolling orizzontale', 'Compact/wide view' => 'Vista compatta/estesa', 'No results match:' => 'Nessun risultato trovato:', 'Currency' => 'Valuta', - 'Files' => 'File', - 'Images' => 'Immagini', 'Private project' => 'Progetto privato', 'AUD - Australian Dollar' => 'AUD - Dollari Australiani', 'CAD - Canadian Dollar' => 'CAD - Dollari Canadesi', @@ -675,18 +633,14 @@ return array( 'Test your device' => 'Testa il tuo dispositivo', 'Assign a color when the task is moved to a specific column' => 'Assegna un colore quando il task viene spostato in una colonna specifica', '%s via Kanboard' => '%s tramite Kanboard', - 'uploaded by: %s' => 'caricato da: %s', - 'uploaded on: %s' => 'caricato su: %s', - 'size: %s' => 'Dimensione: %s', 'Burndown chart for "%s"' => 'Grafico Burndown per "%s"', 'Burndown chart' => 'Grafico Burndown', 'This chart show the task complexity over the time (Work Remaining).' => 'Questo grafico mostra la complessità dei task nel tempo (Lavoro residuo).', 'Screenshot taken %s' => 'Schermata catturata %s', 'Add a screenshot' => 'Aggiungi una schermata', 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Cattura una schermata e premi CTRL+V o ⌘+V per incollarla qui.', - 'Screenshot uploaded successfully.' => 'Schermata caricata correttamente.', + 'Screenshot uploaded successfully.' => 'Schermata caricata con successo.', 'SEK - Swedish Krona' => 'SEK - Corona svedese', - 'The project identifier is an optional alphanumeric code used to identify your project.' => 'L\'identificatore di progetto è un codice alfanumerico usato per indentificare il tuo progetto. ', 'Identifier' => 'Identificatore', 'Disable two factor authentication' => 'Disabilita l\'autenticazione "two-factor"', 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vuoi davvero disabilitare l\'autenticazione "two-factor" per questo utente: "%s"?', @@ -695,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Un task non può essere correlato a se stesso', 'The exact same link already exists' => 'La stessa relazione risulta già esistente', 'Recurrent task is scheduled to be generated' => 'Il task ricorrente è pianificato per essere generato', - 'Recurring information' => 'Informazione di ricorrenza', 'Score' => 'Punteggio', 'The identifier must be unique' => 'L\'identificatore deve essere univoco', 'This linked task id doesn\'t exists' => 'L\'id del task correlato non esiste', @@ -777,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Tempo trascorso modificato: %sh', 'Time estimated changed: %sh' => 'Tempo stimato modificato: %sh', 'The field "%s" have been updated' => 'Il campo %s è stato aggiornato', - 'The description have been modified' => 'La descrizione è stata modificata', + 'The description has been modified' => 'La descrizione è stata modificata', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Vuoi veramente chiudere il task "%s" e i relativi sotto-task?', - 'Swimlane: %s' => 'Corsia: %s', 'I want to receive notifications for:' => 'Voglio ricevere le notifiche per:', 'All tasks' => 'Tutti i task', 'Only for tasks assigned to me' => 'Solo per i task assegnati a me', 'Only for tasks created by me' => 'Solo per i task creati da me', 'Only for tasks created by me and assigned to me' => 'Solo per i task creati da me e assegnati a me', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - 'New due date: %B %e, %Y' => 'Nuova data di scadenza: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Data di inizio cambiata: %B %e, %Y', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', 'Total for all columns' => 'Totale per tutte le colonne', 'You need at least 2 days of data to show the chart.' => 'Hai bisogno di almeno 2 giorni di dati per mostrare il grafico.', @@ -814,8 +761,7 @@ return array( 'Not assigned' => 'Non assegnato', 'View advanced search syntax' => 'Visualizza la sintassi di ricerca avanzata', 'Overview' => 'Panoramica', - // '%b %e %Y' => '', - 'Board/Calendar/List view' => '', + 'Board/Calendar/List view' => 'Vista Bacheca/Calendario/Lista', 'Switch to the board view' => 'Passa alla vista "bacheca"', 'Switch to the calendar view' => 'Passa alla vista "calendario"', 'Switch to the list view' => 'Passa alla vista "elenco"', @@ -847,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Questo grafico mostra i tempi medi di consegna (Lead Time) e lavorazione (Cycle Time) per gli ultimi %d task.', 'Average time into each column' => 'Tempo medio in ogni colonna', 'Lead and cycle time' => 'Tempo di consegna e lavorazione', - 'Google Authentication' => 'Autenticazione con Google', - 'Help on Google authentication' => 'Aiuto sull\'autenticazione con Google', - 'Github Authentication' => 'Autenticazione con Github', - 'Help on Github authentication' => 'Aiuto sull\'autenticazione con Github', 'Lead time: ' => 'Tempo di consegna (Lead Time): ', 'Cycle time: ' => 'Tempo di lavorazione (Cycle Time): ', 'Time spent into each column' => 'Tempo trascorso in ogni colonna', @@ -859,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Se il task non è chiuso sarà usata la data attuale invece della data di completamento.', 'Set automatically the start date' => 'Imposta automaticamente la data di inzio', 'Edit Authentication' => 'Modifica Autenticazione', - 'Google Id' => 'Id Google', - 'Github Id' => 'Id Github', 'Remote user' => 'Utente remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'La password degli utenti remoti (ad esempio: LDAP, account Google e Github) non è salvata nel database di Kanboard', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se imposti l\'opzione "Disabilita il form di login", le credenzali inserite nella saranno ignorate.', 'New remote user' => 'Nuovo utente remoto', 'New local user' => 'Nuovo utente locale', 'Default task color' => 'Colore predefinito dei task', - 'Hide sidebar' => 'Nascondi barra laterale', - 'Expand sidebar' => 'Espandi barra laterale', 'This feature does not work with all browsers.' => 'Questa feature non funziona con tutti i browser.', 'There is no destination project available.' => 'Non ci sono progetti disponbili come destinazione.', 'Trigger automatically subtask time tracking' => 'Attiva automaticamente il time-tracking per i sotto-task', @@ -902,7 +840,6 @@ return array( 'open file' => 'apri file', 'End date' => 'Data di fine', 'Users overview' => 'Panoramica utenti', - // 'Managers' => '', 'Members' => 'Membri', 'Shared project' => 'Progetto condiviso', 'Project managers' => 'Manager del progetto', @@ -913,19 +850,10 @@ return array( 'End date:' => 'Data di fine:', 'There is no start date or end date for this project.' => 'Non è prevista una data di inzio o fine per questo progetto.', 'Projects Gantt chart' => 'Grafico Gantt dei progetti', - 'Start date: %s' => 'Data di inizio: %s', - 'End date: %s' => 'Data di fine: %s', 'Link type' => 'Tipo di link', 'Change task color when using a specific task link' => 'Cambia colore del task quando si un utilizza una determinata relazione di task', 'Task link creation or modification' => 'Creazione o modifica di relazione di task', - 'Login with my Gitlab Account' => 'Accedi tramite il mio account Gitlab', // 'Milestone' => '', - 'Gitlab Authentication' => 'Autenticazione con Gitlab', - 'Help on Gitlab authentication' => 'Aiuto sull\'autenticazione con Gitlab', - 'Gitlab Id' => 'Id Gitlab', - 'Gitlab Account' => 'Account Gitlab', - 'Link my Gitlab Account' => 'Collega il mio Account Gitlab', - 'Unlink my Gitlab Account' => 'Scollega il mio Account Gitlab', 'Documentation: %s' => 'Documentazione: %s', 'Switch to the Gantt chart view' => 'Passa alla vista Grafico Gantt', 'Reset the search/filter box' => 'Resetta la riceca/filtro', @@ -978,7 +906,7 @@ return array( 'Notification methods:' => 'Metodi di notifica', 'Import tasks from CSV file' => 'Importa task da file CSV', 'Unable to read your file' => 'Impossibile leggere il file', - '%d task(s) have been imported successfully.' => '%d task sono stati importati con sucesso.', + '%d task(s) have been imported successfully.' => '%d task sono stati importati con successo.', 'Nothing have been imported!' => 'Non è stato importato nulla!', 'Import users from CSV file' => 'Importa utenti da file CSV', '%d user(s) have been imported successfully.' => '%d utenti importati con successo.', @@ -994,7 +922,6 @@ return array( 'Append/Replace' => 'Aggiungi/Sostituisci', 'Append' => 'Aggiungi', 'Replace' => 'Sostituisci', - 'There is no notification method registered.' => 'Nessun metodo di notifica definito.', 'Import' => 'Importa', 'change sorting' => 'cambia ordinamento', 'Tasks Importation' => 'Importazione task', @@ -1014,7 +941,7 @@ return array( 'Passwords will be encrypted if present' => 'Se presenti, le password verranno criptate', '%s attached a new file to the task %s' => '%s ha allegato un nuovo file al task %s', 'Assign automatically a category based on a link' => 'Assegna automaticamente una categoria sulla base di una relazione', - // 'BAM - Konvertible Mark' => '', + 'BAM - Konvertible Mark' => 'BAM - Marco bosniaco', 'Assignee Username' => 'Nome utente dell\'assegnatario', 'Assignee Name' => 'Nome dell\'assegnatario', 'Groups' => 'Gruppi', @@ -1023,7 +950,7 @@ return array( 'Group created successfully.' => 'Gruppo creato con successo', 'Unable to create your group.' => 'Impossibile creare il gruppo', 'Edit group' => 'Modifica gruppo', - 'Group updated successfully.' => 'Gruppo aggiornato con sucesso.', + 'Group updated successfully.' => 'Gruppo aggiornato con successo.', 'Unable to update your group.' => 'Impossibile aggiornare il gruppo', 'Add group member to "%s"' => 'Aggiungi un membro al gruppo "%s"', 'Group member added successfully.' => 'Membro del gruppo aggiunto con successo.', @@ -1032,7 +959,7 @@ return array( 'User removed successfully from this group.' => 'Utente rimosso dal gruppo con successo.', 'Unable to remove this user from the group.' => 'Impossibile rimuovere l\'utentei dal gruppo.', 'Remove group' => 'Rimuovi gruppo', - 'Group removed successfully.' => 'Gruppo rimosso con sucesso.', + 'Group removed successfully.' => 'Gruppo rimosso con successo.', 'Unable to remove this group.' => 'Impossibile rimuovere questo gruppo.', 'Project Permissions' => 'Permessi del progetto', // 'Manager' => '', @@ -1099,7 +1026,7 @@ return array( 'Expiration' => 'Scadenza', 'Password reset history' => 'Storico cambio password', 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Tutti i task della colonna "%s" e della corsia "%s" sono stati chiusi con successo.', - 'Do you really want to close all tasks of this column?' => 'Vuoi veramente chiudere tutti i task di questa colonna?', + 'Do you really want to close all tasks of this column?' => 'Vuoi davvero chiudere tutti i task di questa colonna?', '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d task della colonna "%s" e della corsia "%s" saranno chiusi.', 'Close all tasks of this column' => 'Chiudi tutti i task di questa colonna', 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Nessun plugin ha caricato un metodo di notifica di progetto. Puoi tuttavia configurare le notifiche personali dal tuo profilo utente.', @@ -1119,5 +1046,109 @@ return array( 'Lowest priority' => 'Priorità minima', 'Highest priority' => 'Priorità massima', 'If you put zero to the low and high priority, this feature will be disabled.' => 'Se imposti a zero la priorità massima e minima, questa funzionalità sarà disabilitata.', - 'Priority: %d' => 'Priorità: %d', + 'Close a task when there is no activity' => 'Chiudi un task quando non c\'è nessuna attività', + 'Duration in days' => 'Durata in giorni', + 'Send email when there is no activity on a task' => 'Invia un\'email quando non c\'è attività sul task', + 'List of external links' => 'Elenco dei link esterni', + 'Unable to fetch link information.' => 'Impossibile recuperare informazioni sul link.', + 'Daily background job for tasks' => 'Job giornaliero in background per i task', + // 'Auto' => '', + 'Related' => 'Correlato', + 'Attachment' => 'Allegato', + 'Title not found' => 'Titolo non trovato', + 'Web Link' => 'Link Web', + 'External links' => 'Link esterni', + 'Add external link' => 'Aggiungi link esterno', + 'Type' => 'Tipo', + 'Dependency' => 'Dipendenza', + 'Add internal link' => 'Aggiungi link interno', + 'Add a new external link' => 'Aggiungi un nuovo link esterno', + 'Edit external link' => 'Modifica link esterno', + 'External link' => 'Link esterno', + 'Copy and paste your link here...' => 'Copia e incolla il tuo link qui...', + // 'URL' => '', + 'There is no external link for the moment.' => 'Nessun link esterno presente al momento.', + 'Internal links' => 'Link interni', + 'There is no internal link for the moment.' => 'Nessun link interno presente al momento.', + 'Assign to me' => 'Assegna a me', + // 'Me' => '', + 'Do not duplicate anything' => 'Non duplicare nulla', + 'Projects management' => 'Gestione progetti', + 'Users management' => 'Gestione utenti', + 'Groups management' => 'Gestione gruppi', + 'Create from another project' => 'Crea da un\'altro progetto', + 'There is no subtask at the moment.' => 'Nessun sotto-task presente al momento.', + 'open' => 'aperto', + 'closed' => 'chiuso', + 'Priority:' => 'Priorità:', + 'Reference:' => 'Riferimento:', + 'Complexity:' => 'Complessità:', + 'Swimlane:' => 'Corsia:', + 'Column:' => 'Colonna:', + 'Position:' => 'Posizione:', + 'Creator:' => 'Creatore:', + 'Time estimated:' => 'Tempo stimato:', + '%s hours' => '%s ore', + 'Time spent:' => 'Tempo trascorso:', + 'Created:' => 'Creato:', + 'Modified:' => 'Modificato:', + 'Completed:' => 'Completato:', + 'Started:' => 'Iniziato:', + 'Moved:' => 'Spostato:', + // 'Task #%d' => '', + 'Sub-tasks' => 'Sotto-task', + 'Date and time format' => 'Formato data e ora', + 'Time format' => 'Formato ora', + 'Start date: ' => 'Data di inizio: ', + 'End date: ' => 'Data di fine: ', + 'New due date: ' => 'Nuova data di scadenza: ', + 'Start date changed: ' => 'Data di inizio cambiata: ', + 'Disable private projects' => 'Disabilita progetti privati', + 'Do you really want to remove this custom filter: "%s"?' => 'Vuoi davvero rimuovere questo filtro personalizato: "%s"?', + 'Remove a custom filter' => 'Rimuovi un filtro personalizzato', + 'User activated successfully.' => 'Utente attivato con successo.', + 'Unable to enable this user.' => 'Impossibile abilitare questo utente.', + 'User disabled successfully.' => 'Utente disabilitato con successo.', + 'Unable to disable this user.' => 'Impossibile disabilitare questo utente.', + 'All files have been uploaded successfully.' => 'Tutti i file sono stati caricati con successo.', + 'View uploaded files' => 'Visualizza i file caricati', + 'The maximum allowed file size is %sB.' => 'La dimensione massima consentita del file è %sB.', + 'Choose files again' => 'Seleziona nuovamente i file', + 'Drag and drop your files here' => 'Trascina i tuoi file qui', + 'choose files' => 'seleziona i file', + 'View profile' => 'Guarda il profilo', + // 'Two Factor' => '', + 'Disable user' => 'Disabilita utente', + 'Do you really want to disable this user: "%s"?' => 'Vuoi davvero disabilitare questo utente: "%s"?', + 'Enable user' => 'Abilita utente', + 'Do you really want to enable this user: "%s"?' => 'Vuoi davvero abilitare questo utente: "%s"?', + 'Download' => 'Scarica', + 'Uploaded: %s' => 'Caricato: %s', + 'Size: %s' => 'Dimensione: %s', + 'Uploaded by %s' => 'Caricato da %s', + 'Filename' => 'Nome del file', + 'Size' => 'Dimensione', + 'Column created successfully.' => 'Colonna creata con successo.', + 'Another column with the same name exists in the project' => 'Un\'altra colonna con lo stesso nome è già esistente in questo progetto', + 'Default filters' => 'Filtri predefiniti', + 'Your board doesn\'t have any column!' => 'La tua bacheca non ha nessuna colonna!', + 'Change column position' => 'Modifica la posizione della colonna', + 'Switch to the project overview' => 'Passa alla panoramica di progetto', + 'User filters' => 'Filtri utente', + 'Category filters' => 'Filtri di categorie', + 'Upload a file' => 'Carica un file', + 'There is no attachment at the moment.' => 'Nessun allegato presente.', + 'View file' => 'Visualizza file', + 'Last activity' => 'Attività recente', + 'Change subtask position' => 'Cambia la posizione del sotto-task', + 'This value must be greater than %d' => 'Questo valore deve essere magiore di %d', + 'Another swimlane with the same name exists in the project' => 'Un\'altra corsia con lo stesso nome è già esistente in questo progetto', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Esempio: http://example.kanboard.net/ (usato per generare URL assolute)', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index b9cde718..77dbeaed 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => '変更', 'remove' => '削除する', 'Remove' => '削除する', - 'Update' => '変更', 'Yes' => 'はい', 'No' => 'いいえ', 'cancel' => 'キャンセル', @@ -60,7 +59,6 @@ return array( 'Actions' => 'アクション', 'Inactive' => '無効', 'Active' => '有効', - 'Add this column' => 'カラムを追加する', '%d tasks on the board' => '%d 個のタスク', '%d tasks in total' => '合計 %d 個のタスク', 'Unable to update this board.' => 'ボードを更新できませんでした', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'プロジェクトの削除', 'Edit the board for "%s"' => 'ボード「%s」を変更する', 'All projects' => 'すべてのプロジェクト', - 'Change columns' => 'カラムの変更', 'Add a new column' => 'カラムの追加', 'Title' => 'タイトル', 'Nobody assigned' => '担当なし', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'タスクをオープンする', 'Do you really want to open this task: "%s"?' => 'タスク「%s」をオープンしますか?', 'Back to the board' => 'ボードに戻る', - 'Created on %B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M に作成', 'There is nobody assigned' => '担当者がいません', 'Column on the board:' => 'カラム: ', - 'Status is open' => 'ステータスはオープンです', - 'Status is closed' => 'ステータスはクローズです', 'Close this task' => 'タスクをクローズする', 'Open this task' => 'タスクをオープンする', 'There is no description.' => '説明がありません', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Work in progress', 'Done' => 'Done', 'Application version:' => 'アプリケーションのバージョン:', - 'Completed on %B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M に完了', - '%B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M', - 'Date created' => '作成日', - 'Date completed' => '完了日', 'Id' => 'ID', '%d closed tasks' => '%d 個のクローズしたタスク', 'No task for this project' => 'このプロジェクトにタスクがありません', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'タスクを変更する', 'Due Date' => '期限', 'Invalid date' => '日付が無効です', - 'Must be done before %B %e, %Y' => '%Y/%m/%d までに完了', - '%B %e, %Y' => '%Y %B %e', - '%b %e, %Y' => '%Y %b %e', 'Automatic actions' => '自動アクションを管理する', 'Your automatic action have been created successfully.' => '自動アクションを作成しました。', 'Unable to create your automatic action.' => '自動アクションの作成に失敗しました。', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => '自動アクションの削除に失敗しました。', 'Action removed successfully.' => '自動アクションの削除に成功しました。', 'Automatic actions for the project "%s"' => 'プロジェクト「%s」の自動アクション', - 'Defined actions' => '定義された自動アクション', 'Add an action' => '自動アクションの追加', 'Event name' => 'イベント名', 'Action name' => 'アクション名', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => '選択されたイベントが発生した時、対応するアクションを実行する。', 'Next step' => '次のステップ', 'Define action parameters' => 'アクションのパラメーター', - 'Save this action' => 'このアクションを保存する', 'Do you really want to remove this action: "%s"?' => '自動アクション「%s」を削除しますか?', 'Remove an automatic action' => '自動アクションの削除', 'Assign the task to a specific user' => 'タスクの担当者を割り当てる', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => '色をユーザに割り当てる', 'Column title' => 'カラムのタイトル', 'Position' => '位置', - 'Move Up' => '上に動かす', - 'Move Down' => '下に動かす', 'Duplicate to another project' => '別のプロジェクトに複製する', 'Duplicate' => '複製する', 'link' => 'リンク', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'コメントを削除しました。', 'Unable to remove this comment.' => 'コメントの削除に失敗しました。', 'Do you really want to remove this comment?' => 'コメントを削除しますか?', - 'Only administrators or the creator of the comment can access to this page.' => '管理者かコメントの作成者のみがこのページアクセスできます。', 'Current password for the user "%s"' => 'ユーザ「%s」の現在のパスワード', 'The current password is required' => '現在のパスワードを入力してください', 'Wrong password' => 'パスワードが違います', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'Email', - 'Link my Google Account' => 'Google アカウントをリンクする', - 'Unlink my Google Account' => 'Google アカウントのリンクを解除する', - 'Login with my Google Account' => 'Google アカウントでログインする', - 'Project not found.' => 'プロジェクトが見つかりません。', 'Task removed successfully.' => 'タスクを削除しました。', 'Unable to remove this task.' => 'タスクの削除に失敗しました。', 'Remove a task' => 'タスクの削除', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => '最大: ', 'Unable to upload the file.' => 'ファイルのアップロードに失敗しました。', 'Display another project' => '別のプロジェクトを表示', - 'Login with my Github Account' => 'Github アカウントでログインする', - 'Link my Github Account' => 'Github アカウントをリンクする', - 'Unlink my Github Account' => 'Github アカウントとのリンクを解除する', 'Created by %s' => '%s が作成', - 'Last modified on %B %e, %Y at %k:%M %p' => ' %Y/%m/%d %H:%M に変更', 'Tasks Export' => 'タスクの出力', 'Tasks exportation for "%s"' => '「%s」のタスク出力', 'Start Date' => '開始日', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => '以下のプロジェクトにのみ通知を受け取る:', 'view the task on Kanboard' => 'Kanboard でタスクを見る', 'Public access' => '公開アクセス設定', - 'User management' => 'ユーザを管理する', 'Active tasks' => 'アクティブなタスク', 'Disable public access' => '公開アクセスを無効にする', 'Enable public access' => '公開アクセスを有効にする', @@ -395,11 +368,7 @@ return array( 'Change password' => 'パスワードの変更', 'Password modification' => 'パスワードの変更', 'External authentications' => '外部認証', - 'Google Account' => 'Google アカウント', - 'Github Account' => 'Github アカウント', 'Never connected.' => '未接続。', - 'No account linked.' => 'アカウントがリンクしていません。', - 'Account linked.' => 'アカウントがリンクしました。', 'No external authentication enabled.' => '外部認証が設定されていません。', 'Password modified successfully.' => 'パスワードを変更しました。', 'Unable to change the password.' => 'パスワードが変更できませんでした。', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => '担当者を外部サービスに基いて変更する', 'Change the category based on an external label' => 'カテゴリを外部サービスに基いて変更する', 'Reference' => '参照', - 'Reference: %s' => '参照: %s', 'Label' => 'ラベル', 'Database' => 'データベース', 'About' => '情報', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => '秒数 (デフォルト 60 秒)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '秒数 (0 は機能を無効化、デフォルト 10 秒)', 'Application URL' => 'アプリケーションの URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemple : http://exemple.kanboard.net/ (Email 通知に利用)', 'Token regenerated.' => 'トークンが再生成されました。', 'Date format' => 'データのフォーマット', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO フォーマットが入力できます(例: %s または %s)', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'このプロジェクトは非公開です', 'Type here to create a new sub-task' => 'サブタスクを追加するにはここに入力してください', 'Add' => '追加', - 'Estimated time: %s hours' => '予想時間: %s 時間', - 'Time spent: %s hours' => '経過: %s 時間', - 'Started on %B %e, %Y' => '開始 %Y/%m/%d', 'Start date' => '開始時間', 'Time estimated' => '予想時間', 'There is nothing assigned to you.' => '何もアサインされていません。', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'このスイムレーン「%s」を本当に削除しますか?', 'Inactive swimlanes' => 'インタラクティブなスイムレーン', 'Remove a swimlane' => 'スイムレーンの削除', - 'Rename' => '名前の変更', 'Show default swimlane' => 'デフォルトスイムレーンの表示', 'Swimlane modification for the project "%s"' => '「%s」に対するスイムレーン変更', 'Swimlane not found.' => 'スイムレーンが見つかりません。', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'スイムレーン', 'Swimlane updated successfully.' => 'スイムレーンを更新しました。', 'The default swimlane have been updated successfully.' => 'デフォルトスイムレーンを更新しました。', - 'Unable to create your swimlane.' => 'スイムレーンを追加できませんでした。', 'Unable to remove this swimlane.' => 'スイムレーンを削除できませんでした。', 'Unable to update this swimlane.' => 'スイムレーンを更新できませんでした。', 'Your swimlane have been created successfully.' => 'スイムレーンが作成されました。', @@ -612,7 +574,6 @@ return array( 'This task' => 'このタスクは', '<1h' => '<1時間', '%dh' => '%d 時間', - '%b %e' => '%b/%e', 'Expand tasks' => 'タスクを展開する', 'Collapse tasks' => 'タスクを閉じる', 'Expand/collapse tasks' => 'タスクの展開/閉じる', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'キーボードショートカット', 'Open board switcher' => 'ボード切り替えを開く', 'Application' => 'アプリケーション', - 'since %B %e, %Y at %k:%M %p' => '%Y/%m/%d %k:%M から', 'Compact view' => 'コンパクトビュー', 'Horizontal scrolling' => '縦スクロール', 'Compact/wide view' => 'コンパクト/ワイドビュー', 'No results match:' => '結果が一致しませんでした', 'Currency' => '通貨', - 'Files' => 'ファイル', - 'Images' => '画像', 'Private project' => 'プライベートプロジェクト', 'AUD - Australian Dollar' => 'AUD - 豪ドル', 'CAD - Canadian Dollar' => 'CAD - 加ドル', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'デバイスをテストする', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', // 'I want to receive notifications for:' => '', // 'All tasks' => '', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( // 'Not assigned' => '', // 'View advanced search syntax' => '', // 'Overview' => '', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index 43c288c7..8eac45be 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Sunting', 'remove' => 'hapus', 'Remove' => 'Hapus', - 'Update' => 'Kemaskini', 'Yes' => 'Ya', 'No' => 'Tidak', 'cancel' => 'batal', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Tindakan', 'Inactive' => 'Tidak Aktif', 'Active' => 'Aktif', - 'Add this column' => 'Tambahkan kolom ini', '%d tasks on the board' => '%d tugasan di papan', '%d tasks in total' => 'Sejumlah %d tugasan', 'Unable to update this board.' => 'Tidak berupaya mengemaskini papan ini', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Hapus projek', 'Edit the board for "%s"' => 'Ubah papan untuk « %s »', 'All projects' => 'Semua projek', - 'Change columns' => 'Ubah kolom', 'Add a new column' => 'Tambah kolom baru', 'Title' => 'Judul', 'Nobody assigned' => 'Tidak ada yang ditugaskan', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Buka tugas', 'Do you really want to open this task: "%s"?' => 'Anda yakin untuk buka tugas ini : « %s » ?', 'Back to the board' => 'Kembali ke papan', - 'Created on %B %e, %Y at %k:%M %p' => 'Dicipta pada tanggal %d/%m/%Y à %H:%M', 'There is nobody assigned' => 'Tidak ada orang yand ditugaskan', 'Column on the board:' => 'Kolom di dalam papan : ', - 'Status is open' => 'Status terbuka', - 'Status is closed' => 'Status ditutup', 'Close this task' => 'Tutup tugas ini', 'Open this task' => 'Buka tugas ini', 'There is no description.' => 'Tidak ada keterangan.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Sedang dalam pengerjaan', 'Done' => 'Selesai', 'Application version:' => 'Versi aplikasi :', - 'Completed on %B %e, %Y at %k:%M %p' => 'Diselesaikan pada tanggal %d/%m/%Y à %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', - 'Date created' => 'Tanggal dibuat', - 'Date completed' => 'Tanggal diselesaikan', 'Id' => 'Id.', '%d closed tasks' => '%d tugas yang ditutup', 'No task for this project' => 'Tidak ada tugas dalam projek ini', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Modifikasi tugas ini', 'Due Date' => 'Batas Tanggal Terakhir', 'Invalid date' => 'Tanggal tidak valid', - 'Must be done before %B %e, %Y' => 'Harus diselesaikan sebelum tanggal %d/%m/%Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Tindakan otomatis', 'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.', 'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Tidak dapat menghapus tindakan ini', 'Action removed successfully.' => 'Tindakan berhasil dihapus.', 'Automatic actions for the project "%s"' => 'Tindakan otomatis untuk projek ini « %s »', - 'Defined actions' => 'Tindakan didefinisikan', 'Add an action' => 'Tambah tindakan', 'Event name' => 'Nama acara', 'Action name' => 'Nama tindakan', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Ketika acara yang dipilih terjadi, melakukan tindakan yang sesuai.', 'Next step' => 'Langkah selanjutnya', 'Define action parameters' => 'Definisi parameter tindakan', - 'Save this action' => 'Simpan tindakan ini', 'Do you really want to remove this action: "%s"?' => 'Apakah anda yakin akan menghapus tindakan ini « %s » ?', 'Remove an automatic action' => 'Hapus tindakan otomatis', 'Assign the task to a specific user' => 'Menetapkan tugas untuk pengguna tertentu', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu', 'Column title' => 'Judul kolom', 'Position' => 'Posisi', - 'Move Up' => 'Pindah ke atas', - 'Move Down' => 'Pindah ke bawah', 'Duplicate to another project' => 'Duplikasi ke projek lain', 'Duplicate' => 'Duplikasi', 'link' => 'Pautan', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Komentar berhasil dihapus.', 'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.', 'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Hanya administrator atau pembuat komentar yang dapat mengakses halaman ini.', 'Current password for the user "%s"' => 'Kata laluan saat ini untuk pengguna « %s »', 'The current password is required' => 'Kata laluan saat ini diperlukan', 'Wrong password' => 'Kata laluan salah', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Otentifikasi eksternal gagal', 'Your external account is linked to your profile successfully.' => 'Akaun eksternal anda berhasil dihubungkan ke profil anda.', 'Email' => 'Email', - 'Link my Google Account' => 'Hubungkan Akaun Google saya', - 'Unlink my Google Account' => 'Putuskan Akaun Google saya', - 'Login with my Google Account' => 'Masuk menggunakan Akaun Google saya', - 'Project not found.' => 'projek tidak ditemukan.', 'Task removed successfully.' => 'Tugas berhasil dihapus.', 'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.', 'Remove a task' => 'Hapus tugas', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Ukuran maksimum: ', 'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.', 'Display another project' => 'Lihat projek lain', - 'Login with my Github Account' => 'Masuk menggunakan Akaun Github saya', - 'Link my Github Account' => 'Hubungkan Akaun Github saya ', - 'Unlink my Github Account' => 'Putuskan Akaun Github saya', 'Created by %s' => 'Dibuat oleh %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M', 'Tasks Export' => 'Ekspor Tugas', 'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »', 'Start Date' => 'Tanggal Mulai', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk projek-projek yang dipilih :', 'view the task on Kanboard' => 'lihat tugas di Kanboard', 'Public access' => 'Akses awam', - 'User management' => 'Manajemen pengguna', 'Active tasks' => 'Tugas aktif', 'Disable public access' => 'Nyahaktifkan akses awam', 'Enable public access' => 'Aktifkan akses awam', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Rubah kata sandri', 'Password modification' => 'Modifikasi kata laluan', 'External authentications' => 'Otentifikasi eksternal', - 'Google Account' => 'Akaun Google', - 'Github Account' => 'Akaun Github', 'Never connected.' => 'Tidak pernah terhubung.', - 'No account linked.' => 'Tidak ada Akaun terhubung.', - 'Account linked.' => 'Akaun terhubung.', 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', 'Password modified successfully.' => 'Kata laluan telah berjaya ditukar.', 'Unable to change the password.' => 'Tidak dapat merubah kata laluanr.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', 'Reference' => 'Referensi', - 'Reference: %s' => 'Referensi : %s', 'Label' => 'Label', 'Database' => 'Pengkalan data', 'About' => 'Tentang', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 saat)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekuensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)', 'Application URL' => 'URL Aplikasi', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Contoh: http://example.kanboard.net/ (digunakan untuk pemberitahuan email)', 'Token regenerated.' => 'Token diregenerasi.', 'Date format' => 'Format tarikh', 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'projek ini adalah peribadi', 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', 'Add' => 'Tambah', - 'Estimated time: %s hours' => 'Anggaran waktu: %s jam', - 'Time spent: %s hours' => 'Waktu dihabiskan : %s jam', - 'Started on %B %e, %Y' => 'Dimulai pada %d/%m/%Y', 'Start date' => 'Tarikh mula', 'Time estimated' => 'Anggaran masa', 'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Anda yakin untuk menghapus swimlane ini : « %s » ?', 'Inactive swimlanes' => 'Swimlanes tidak aktif', 'Remove a swimlane' => 'Padam swimlane', - 'Rename' => 'Namakan semula', 'Show default swimlane' => 'Tampilkan piawai swimlane', 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk projek « %s »', 'Swimlane not found.' => 'Swimlane tidak ditemui.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane telah dikemaskini.', 'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.', - 'Unable to create your swimlane.' => 'Tidak dapat membuat swimlane anda.', 'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.', 'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.', 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Tugas ini', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Perluas tugas', 'Collapse tasks' => 'Lipat tugas', 'Expand/collapse tasks' => 'Perluas/lipat tugas', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'pintas keyboard', 'Open board switcher' => 'Buka table switcher', 'Application' => 'Aplikasi', - 'since %B %e, %Y at %k:%M %p' => 'sejak %d/%m/%Y à %H:%M', 'Compact view' => 'Tampilan kompak', 'Horizontal scrolling' => 'Horisontal bergulir', 'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas', 'No results match:' => 'Tidak ada hasil :', 'Currency' => 'Mata uang', - 'Files' => 'Arsip', - 'Images' => 'Gambar', 'Private project' => 'projek pribadi', 'AUD - Australian Dollar' => 'AUD - Dollar Australia', 'CAD - Canadian Dollar' => 'CAD - Dollar Kanada', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Menguji perangkat anda', 'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'diunggah oleh %s', - 'uploaded on: %s' => 'diunggah pada %s', - 'size: %s' => 'ukuran : %s', 'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »', 'Burndown chart' => 'Grafik Burndown', 'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri', 'The exact same link already exists' => 'Pautan yang sama persis sudah ada', 'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan', - 'Recurring information' => 'Informasi berulang', 'Score' => 'Skor', 'The identifier must be unique' => 'Identifier harus unik', 'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Waktu yang dihabiskan berubah : %sh', 'Time estimated changed: %sh' => 'Perkiraan waktu berubah : %sh', 'The field "%s" have been updated' => 'Field « %s » telah diperbaharui', - 'The description have been modified' => 'Deskripsi telah dimodifikasi', + 'The description has been modified' => 'Deskripsi telah dimodifikasi', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?', - 'Swimlane: %s' => 'Swimlane : %s', 'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :', 'All tasks' => 'Semua tugas', 'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya', 'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya', 'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Tanggal jatuh tempo baru : %d/%m/%Y', - 'Start date changed: %B %e, %Y' => 'Tanggal mulai berubah : %d/%m/%Y', - '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', 'Total for all columns' => 'Total untuk semua kolom', 'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Tidak ditugaskan', 'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan', 'Overview' => 'Ikhtisar', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar', 'Switch to the board view' => 'Beralih ke tampilan papan', 'Switch to the calendar view' => 'Beralih ke tampilan kalender', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.', 'Average time into each column' => 'Rata-rata waktu ke setiap kolom', 'Lead and cycle time' => 'Lead dan siklus waktu', - 'Google Authentication' => 'Google Otentifikasi', - 'Help on Google authentication' => 'Bantuan pada otentifikasi Google', - 'Github Authentication' => 'Otentifikasi Github', - 'Help on Github authentication' => 'Bantuan pada otentifikasi Github', 'Lead time: ' => 'Lead time : ', 'Cycle time: ' => 'Siklus waktu : ', 'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.', 'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai', 'Edit Authentication' => 'Modifikasi Otentifikasi', - 'Google Id' => 'Id Google', - 'Github Id' => 'Id Github', 'Remote user' => 'Pengguna jauh', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata laluan mereka dalam basis data Kanboard, contoh: Akaun LDAP, Google dan Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', 'New remote user' => 'Pengguna baru jauh', 'New local user' => 'Pengguna baru lokal', 'Default task color' => 'Standar warna tugas', - 'Hide sidebar' => 'Sembunyikan sidebar', - 'Expand sidebar' => 'Perluas sidebar', 'This feature does not work with all browsers.' => 'Ciri ini tidak dapat digunakan pada semua browsers', 'There is no destination project available.' => 'Tiada destinasi projek yang tersedia.', 'Trigger automatically subtask time tracking' => 'Picu pengesanan subtugas secara otomatik', @@ -901,7 +840,6 @@ return array( 'open file' => 'buka fail', 'End date' => 'Waktu berakhir', 'Users overview' => 'Ikhtisar pengguna', - 'Managers' => 'Pengurus', 'Members' => 'Anggota', 'Shared project' => 'projek bersama', 'Project managers' => 'Pengurus projek', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Waktu berakhir :', 'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini', 'Projects Gantt chart' => 'projekkan carta Gantt', - 'Start date: %s' => 'Waktu mulai: %s', - 'End date: %s' => 'Waktu berakhir: %s', 'Link type' => 'Jenis pautan', 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik', 'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan', - 'Login with my Gitlab Account' => 'Masuk menggunakan Akaun Gitlab saya', 'Milestone' => 'Batu Tanda', - 'Gitlab Authentication' => 'Otentifikasi Gitlab', - 'Help on Gitlab authentication' => 'Bantuan pada otentifikasi Gitlab', - 'Gitlab Id' => 'Id Gitlab', - 'Gitlab Account' => 'Akaun Gitlab', - 'Link my Gitlab Account' => 'Hubungkan akaun Gitlab saya', - 'Unlink my Gitlab Account' => 'Putuskan akaun Gitlab saya', 'Documentation: %s' => 'Dokumentasi : %s', 'Switch to the Gantt chart view' => 'Beralih ke tampilan Carta Gantt', 'Reset the search/filter box' => 'Tetap semula pencarian/saringan', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 682f44a8..fba22ffe 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Rediger', 'remove' => 'fjern', 'Remove' => 'Fjern', - 'Update' => 'Oppdater', 'Yes' => 'Ja', 'No' => 'Nei', 'cancel' => 'avbryt', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Handlinger', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', - 'Add this column' => 'Legg til denne kolonnen', '%d tasks on the board' => '%d Oppgaver på hovedsiden', '%d tasks in total' => '%d Oppgaver i alt', 'Unable to update this board.' => 'Ikke mulig at oppdatere hovedsiden', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Fjern prosjekt', 'Edit the board for "%s"' => 'Endre prosjektsiden for "%s"', 'All projects' => 'Alle prosjekter', - 'Change columns' => 'Endre kolonner', 'Add a new column' => 'Legg til en ny kolonne', 'Title' => 'Tittel', 'Nobody assigned' => 'Ikke tildelt', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Åpne en oppgave', 'Do you really want to open this task: "%s"?' => 'Vil du åpne denne oppgaven: "%s"?', 'Back to the board' => 'Tilbake til prosjektsiden', - 'Created on %B %e, %Y at %k:%M %p' => 'Opprettet %d.%m.%Y - %H:%M', 'There is nobody assigned' => 'Mangler tildeling', 'Column on the board:' => 'Kolonne:', - 'Status is open' => 'Status: åpen', - 'Status is closed' => 'Status: lukket', 'Close this task' => 'Lukk oppgaven', 'Open this task' => 'Åpne denne oppgaven', 'There is no description.' => 'Det er ingen beskrivelse.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Under arbeid', 'Done' => 'Utført', 'Application version:' => 'Versjon:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Fullført %d.%m.%Y - %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y - %H:%M', - 'Date created' => 'Dato for opprettelse', - 'Date completed' => 'Dato for fullført', 'Id' => 'ID', '%d closed tasks' => '%d lukkede oppgaver', 'No task for this project' => 'Ingen oppgaver i dette prosjektet', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Rediger oppgaven', 'Due Date' => 'Forfallsdato', 'Invalid date' => 'Ugyldig dato', - 'Must be done before %B %e, %Y' => 'Skal være utført innen %d.%m.%Y', - '%B %e, %Y' => '%d.%m.%Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'Automatiske handlinger', 'Your automatic action have been created successfully.' => 'Din automatiske handling er opprettet.', 'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke opprettes.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Handlingen kunne ikke fjernes.', 'Action removed successfully.' => 'Handlingen er fjernet.', 'Automatic actions for the project "%s"' => 'Automatiske handlinger for prosjektet "%s"', - 'Defined actions' => 'Definerte handlinger', 'Add an action' => 'Legg til en handling', 'Event name' => 'Hendelsehet', 'Action name' => 'Handling', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Når den valgte hendelsen oppstår, utfør tilsvarende handling.', 'Next step' => 'Neste', 'Define action parameters' => 'Definer handlingsparametre', - 'Save this action' => 'Lagre handlingen', 'Do you really want to remove this action: "%s"?' => 'Vil du slette denne handlingen: "%s"?', 'Remove an automatic action' => 'Fjern en automatisk handling', 'Assign the task to a specific user' => 'Tildel oppgaven til en bestemt bruker', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Tildel en farge til en bestemt bruker', 'Column title' => 'Kolonne tittel', 'Position' => 'Posisjon', - 'Move Up' => 'Flytt opp', - 'Move Down' => 'Flytt ned', 'Duplicate to another project' => 'Kopier til et annet prosjekt', 'Duplicate' => 'Kopier', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Kommentaren ble fjernet.', 'Unable to remove this comment.' => 'Kommentaren kunne ikke fjernes.', 'Do you really want to remove this comment?' => 'Vil du fjerne denne kommentaren?', - 'Only administrators or the creator of the comment can access to this page.' => 'Kun administrator eller brukeren, som har oprettet kommentaren har adgang til denne siden.', 'Current password for the user "%s"' => 'Aktivt passord for brukeren "%s"', 'The current password is required' => 'Passord er påkrevet', 'Wrong password' => 'Feil passord', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'Epost', - 'Link my Google Account' => 'Knytt til min Google-konto', - 'Unlink my Google Account' => 'Fjern knytningen til min Google-konto', - 'Login with my Google Account' => 'Login med min Google-konto', - 'Project not found.' => 'Prosjekt ikke funnet.', 'Task removed successfully.' => 'Oppgaven er fjernet.', 'Unable to remove this task.' => 'Oppgaven kunne ikke fjernes.', 'Remove a task' => 'Fjern en oppgave', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimum størrelse: ', 'Unable to upload the file.' => 'Filen kunne ikke lastes opp.', 'Display another project' => 'Vis annet prosjekt...', - // 'Login with my Github Account' => '', - // 'Link my Github Account' => '', - // 'Unlink my Github Account' => '', 'Created by %s' => 'Opprettet av %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Sist endret %d.%m.%Y - %H:%M', 'Tasks Export' => 'Oppgave eksport', 'Tasks exportation for "%s"' => 'Oppgaveeksportering for "%s"', 'Start Date' => 'Start-dato', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Jeg vil kun ha varslinger for disse prosjekter:', 'view the task on Kanboard' => 'se oppgaven påhovedsiden', 'Public access' => 'Offentlig tilgang', - 'User management' => 'Brukere', 'Active tasks' => 'Aktive oppgaver', 'Disable public access' => 'Deaktiver offentlig tilgang', 'Enable public access' => 'Aktiver offentlig tilgang', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Endre passord', 'Password modification' => 'Passordendring', 'External authentications' => 'Ekstern godkjenning', - 'Google Account' => 'Google-konto', - 'Github Account' => 'GitHub-konto', 'Never connected.' => 'Aldri innlogget.', - 'No account linked.' => 'Ingen kontoer knyttet.', - 'Account linked.' => 'Konto knyttet.', 'No external authentication enabled.' => 'Ingen eksterne godkjenninger aktiveret.', 'Password modified successfully.' => 'Passord er endret.', 'Unable to change the password.' => 'Passordet kuenne ikke endres.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Endre ansvarlige baseret på et eksternt brukernavn', 'Change the category based on an external label' => 'Endre kategorien basert på en ekstern etikett', 'Reference' => 'Referanse', - 'Reference: %s' => 'Referanse: %s', 'Label' => 'Etikett', 'Database' => 'Database', 'About' => 'Om', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frekevens i sekunder (60 sekunder som standard)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (0 for øt deaktivere denne funksjonen, 10 sekunder som standard)', 'Application URL' => 'Applikasjons URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Eksempel: http://example.kanboard.net/ (bruges til email notifikationer)', 'Token regenerated.' => 'Token regenerert.', 'Date format' => 'Datoformat', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Dette projektet er privat', 'Type here to create a new sub-task' => 'Skriv her for ø opprette en ny deloppgave', 'Add' => 'Legg til', - 'Estimated time: %s hours' => 'Estimert tid: %s timer', - 'Time spent: %s hours' => 'Tid brukt: %s timer', - 'Started on %B %e, %Y' => 'Startet %d.%m.%Y ', 'Start date' => 'Start dato', 'Time estimated' => 'Tid estimert', 'There is nothing assigned to you.' => 'Ingen er tildelt deg.', @@ -529,7 +493,6 @@ return array( // 'Do you really want to remove this swimlane: "%s"?' => '', // 'Inactive swimlanes' => '', 'Remove a swimlane' => 'Fjern en svømmebane', - 'Rename' => 'Endre navn', 'Show default swimlane' => 'Vis standard svømmebane', // 'Swimlane modification for the project "%s"' => '', 'Swimlane not found.' => 'Svømmebane ikke funnet', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Svømmebaner', 'Swimlane updated successfully.' => 'Svømmebane oppdatert', // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', // 'Unable to remove this swimlane.' => '', // 'Unable to update this swimlane.' => '', // 'Your swimlane have been created successfully.' => '', @@ -612,7 +574,6 @@ return array( 'This task' => 'Denne oppgaven', // '<1h' => '', // '%dh' => '', - // '%b %e' => '', 'Expand tasks' => 'Utvid oppgavevisning', 'Collapse tasks' => 'Komprimer oppgavevisning', 'Expand/collapse tasks' => 'Utvide/komprimere oppgavevisning', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Hurtigtaster', // 'Open board switcher' => '', // 'Application' => '', - 'since %B %e, %Y at %k:%M %p' => 'siden %B %e, %Y at %k:%M %p', 'Compact view' => 'Kompakt visning', 'Horizontal scrolling' => 'Bla horisontalt', 'Compact/wide view' => 'Kompakt/bred visning', 'No results match:' => 'Ingen resultater', 'Currency' => 'Valuta', - 'Files' => 'Filer', - 'Images' => 'Bilder', 'Private project' => 'Privat prosjekt', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', 'Assign a color when the task is moved to a specific column' => 'Endre til en valgt farge hvis en oppgave flyttes til en spesifikk kolonne', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - 'Swimlane: %s' => 'Svømmebane: %s', 'I want to receive notifications for:' => 'Jeg vil motta varslinger om:', 'All tasks' => 'Alle oppgaver', 'Only for tasks assigned to me' => 'Kun oppgaver som er tildelt meg', 'Only for tasks created by me' => 'Kun oppgaver som er opprettet av meg', 'Only for tasks created by me and assigned to me' => 'Kun oppgaver som er opprettet av meg og tildelt meg', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', 'Total for all columns' => 'Totalt for alle kolonner', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Ikke tildelt', 'View advanced search syntax' => 'Vis hjelp for avansert søk ', 'Overview' => 'Oversikt', - // '%b %e %Y' => '', 'Board/Calendar/List view' => 'Oversikt/kalender/listevisning', 'Switch to the board view' => 'Oversiktsvisning', 'Switch to the calendar view' => 'Kalendevisning', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', 'New remote user' => 'Ny eksternbruker', 'New local user' => 'Ny internbruker', 'Default task color' => 'Standard oppgavefarge', - 'Hide sidebar' => 'Skjul sidemeny', - 'Expand sidebar' => 'Vis sidemeny', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( 'open file' => 'Åpne fil', 'End date' => 'Sluttdato', 'Users overview' => 'Brukeroversikt', - 'Managers' => 'Ledere', 'Members' => 'Medlemmer', 'Shared project' => 'Delt prosjekt', 'Project managers' => 'Prosjektledere', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Sluttdato:', // 'There is no start date or end date for this project.' => '', 'Projects Gantt chart' => 'Gantt skjema for prosjekter', - 'Start date: %s' => 'Startdato: %s', - 'End date: %s' => 'Sluttdato: %s', 'Link type' => 'Relasjonstype', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', 'Milestone' => 'Milepæl', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', 'Documentation: %s' => 'Dokumentasjon: %s', 'Switch to the Gantt chart view' => 'Gantt skjema visning', 'Reset the search/filter box' => 'Nullstill søk/filter', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 4f38f256..67546e17 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1,14 +1,13 @@ <?php return array( - // 'number.decimals_separator' => '', - // 'number.thousands_separator' => '', + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Geen', 'edit' => 'bewerken', 'Edit' => 'Bewerken', 'remove' => 'verwijderen', 'Remove' => 'Verwijderen', - 'Update' => 'Update', 'Yes' => 'Ja', 'No' => 'Nee', 'cancel' => 'annuleren', @@ -20,15 +19,15 @@ return array( 'Red' => 'Rood', 'Orange' => 'Oranje', 'Grey' => 'Grijs', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', + 'Brown' => 'Bruin', + 'Deep Orange' => 'Dieporanje', + 'Dark Grey' => 'Donkergrijs', + 'Pink' => 'Roze', // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Cyan' => 'Cyaan', + 'Lime' => 'Limoen', + 'Light Green' => 'Lichtgroen', + 'Amber' => 'Amber', 'Save' => 'Opslaan', 'Login' => 'Inloggen', 'Official website:' => 'Officiële website :', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Acties', 'Inactive' => 'Inactief', 'Active' => 'Actief', - 'Add this column' => 'Deze kolom toevoegen', '%d tasks on the board' => '%d taken op het bord', '%d tasks in total' => '%d taken in totaal', 'Unable to update this board.' => 'Update van dit bord niet mogelijk.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Project verwijderen', 'Edit the board for "%s"' => 'Bord bewerken voor « %s »', 'All projects' => 'Alle projecten', - 'Change columns' => 'Kolommen veranderen', 'Add a new column' => 'Kolom toevoegen', 'Title' => 'Titel', 'Nobody assigned' => 'Niemand toegewezen', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Een taak openen', 'Do you really want to open this task: "%s"?' => 'Weet u zeker dat u deze taak wil openen : « %s » ?', 'Back to the board' => 'Terug naar het bord', - 'Created on %B %e, %Y at %k:%M %p' => 'Aangemaakt op %d/%m/%Y à %H:%M', 'There is nobody assigned' => 'Er is niemand toegewezen', 'Column on the board:' => 'Kolom op het bord : ', - 'Status is open' => 'Status is open', - 'Status is closed' => 'Status is gesloten', 'Close this task' => 'Deze taak sluiten', 'Open this task' => 'Deze taak openen', 'There is no description.' => 'Er is geen omschrijving.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'In behandeling', 'Done' => 'Afgewerkt', 'Application version:' => 'Applicatie versie :', - 'Completed on %B %e, %Y at %k:%M %p' => 'Voltooid op %d/%m/%Y à %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y op %H:%M', - 'Date created' => 'Datum aangemaakt', - 'Date completed' => 'Datum voltooid', 'Id' => 'Id', '%d closed tasks' => '%d gesloten taken', 'No task for this project' => 'Geen taken voor dit project', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Deze taak aanpassen', 'Due Date' => 'Vervaldag', 'Invalid date' => 'Ongeldige datum', - 'Must be done before %B %e, %Y' => 'Moet voltooid zijn voor %d/%m/%Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Geautomatiseerd acties', 'Your automatic action have been created successfully.' => 'Geautomatiseerde actie succesvol aangemaakt.', 'Unable to create your automatic action.' => 'Geautomatiseerde actie aanmaken niet gelukt.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Actie verwijderen niet gelukt', 'Action removed successfully.' => 'Actie succesvol verwijder.', 'Automatic actions for the project "%s"' => 'Automatiseer acties voor project « %s »', - 'Defined actions' => 'Gedefinieerde acties', 'Add an action' => 'Actie toevoegen', 'Event name' => 'Naam gebeurtenis', 'Action name' => 'Actie naam', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Als de geselecteerde gebeurtenis optreedt de volgende actie uitvoeren', 'Next step' => 'Volgende stap', 'Define action parameters' => 'Bepaal actie parameters', - 'Save this action' => 'Actie opslaan', 'Do you really want to remove this action: "%s"?' => 'Weet u zeker dat u de volgende actie wil verwijderen : « %s » ?', 'Remove an automatic action' => 'Automatische actie verwijderen', 'Assign the task to a specific user' => 'Taak toewijzen aan een gebruiker', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Wijs een kleur toe aan een gebruiker', 'Column title' => 'Kolom titel', 'Position' => 'Positie', - 'Move Up' => 'Omhoog verplaatsen', - 'Move Down' => 'Omlaag verplaatsen', 'Duplicate to another project' => 'Dupliceren in een ander project', 'Duplicate' => 'Dupliceren', 'link' => 'koppelen', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Commentaar succesvol verwijder.', 'Unable to remove this comment.' => 'Commentaar verwijderen niet gelukt.', 'Do you really want to remove this comment?' => 'Weet u zeker dat u dit commentaar wil verwijderen ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Alleen administrators of de aanmaker van het commentaar hebben toegang tot deze pagina.', 'Current password for the user "%s"' => 'Huidig wachtwoord voor gebruiker « %s »', 'The current password is required' => 'Huidig wachtwoord is verplicht', 'Wrong password' => 'Onjuist wachtwoord', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'Email', - 'Link my Google Account' => 'Link mijn Google Account', - 'Unlink my Google Account' => 'Link met Google Account verwijderen', - 'Login with my Google Account' => 'Inloggen met mijn Google Account', - 'Project not found.' => 'Project niet gevonden.', 'Task removed successfully.' => 'Taak succesvol verwijderd.', 'Unable to remove this task.' => 'Taak verwijderen niet gelukt.', 'Remove a task' => 'Taak verwijderen', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maximale grootte : ', 'Unable to upload the file.' => 'Uploaden van bestand niet gelukt.', 'Display another project' => 'Een ander project weergeven', - 'Login with my Github Account' => 'Login met mijn Github Account', - 'Link my Github Account' => 'Link met mijn Github', - 'Unlink my Github Account' => 'Link met mijn Github verwijderen', 'Created by %s' => 'Aangemaakt door %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Laatst gewijzigd op %d/%m/%Y à %H:%M', 'Tasks Export' => 'Taken exporteren', 'Tasks exportation for "%s"' => 'Taken exporteren voor « %s »', 'Start Date' => 'Startdatum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Ik wil notificaties ontvangen van de volgende projecten :', 'view the task on Kanboard' => 'taak bekijken op Kanboard', 'Public access' => 'Publieke toegang', - 'User management' => 'Gebruikers management', 'Active tasks' => 'Actieve taken', 'Disable public access' => 'Publieke toegang uitschakelen', 'Enable public access' => 'Publieke toegang inschakelen', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Wachtwoord aanpassen', 'Password modification' => 'Wachtwoord aanpassen', 'External authentications' => 'Externe authenticatie', - 'Google Account' => 'Google Account', - 'Github Account' => 'Github Account', 'Never connected.' => 'Nooit verbonden.', - 'No account linked.' => 'Geen account gelinkt.', - 'Account linked.' => 'Account gelinkt.', 'No external authentication enabled.' => 'Geen externe authenticatie aangezet.', 'Password modified successfully.' => 'Wachtwoord succesvol aangepast.', 'Unable to change the password.' => 'Aanpassen van wachtwoord niet gelukt.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Verander de toegewezene aan de hand van de externe gebruikersnaam', 'Change the category based on an external label' => 'Verander de categorie aan de hand van een extern label', 'Reference' => 'Referentie', - 'Reference: %s' => 'Referentie : %s', 'Label' => 'Label', 'Database' => 'Database', 'About' => 'Over', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequentie in seconden (stadaard 60)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequentie in seconden (0 om uit te schakelen, standaard 10)', 'Application URL' => 'Applicatie URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Voorbeeld: http://example.kanboard.net/ (gebruikt voor email notificaties)', 'Token regenerated.' => 'Token opnieuw gegenereerd.', 'Date format' => 'Datum formaat', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Dit project is privé', 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken', 'Add' => 'Toevoegen', - 'Estimated time: %s hours' => 'Geschatte tijd: %s hours', - 'Time spent: %s hours' => 'Tijd besteed : %s heures', - 'Started on %B %e, %Y' => 'Gestart op %d/%m/%Y', 'Start date' => 'Startdatum', 'Time estimated' => 'Geschatte tijd', 'There is nothing assigned to you.' => 'Er is niets aan u toegewezen.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Weet u zeker dat u deze swimlane wil verwijderen : « %s » ?', 'Inactive swimlanes' => 'Inactieve swinlanes', 'Remove a swimlane' => 'Verwijder swinlane', - 'Rename' => 'Hernoemen', 'Show default swimlane' => 'Standaard swimlane tonen', 'Swimlane modification for the project "%s"' => 'Swinlane aanpassing voor project « %s »', 'Swimlane not found.' => 'Swimlane niet gevonden.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane succesvol aangepast.', 'The default swimlane have been updated successfully.' => 'De standaard swimlane is succesvol aangepast.', - 'Unable to create your swimlane.' => 'Swimlane aanmaken niet gelukt.', 'Unable to remove this swimlane.' => 'Swimlane verwijderen niet gelukt.', 'Unable to update this swimlane.' => 'Swimlane aanpassen niet gelukt.', 'Your swimlane have been created successfully.' => 'Swimlane succesvol aangemaakt.', @@ -609,10 +571,9 @@ return array( 'is a milestone of' => 'is een milestone voor', 'fixes' => 'corrigeert', 'is fixed by' => 'word gecorrigeerd door', - 'This task' => 'Deze taal', + 'This task' => 'Deze taak', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Taken uitklappen', 'Collapse tasks' => 'Taken inklappen', 'Expand/collapse tasks' => 'Taken in/uiklappen', @@ -622,45 +583,42 @@ return array( 'Keyboard shortcuts' => 'Keyboard snelkoppelingen', 'Open board switcher' => 'Open bord switcher', 'Application' => 'Applicatie', - 'since %B %e, %Y at %k:%M %p' => 'sinds %d/%m/%Y à %H:%M', - // 'Compact view' => '', + 'Compact view' => 'Compacte weergave', // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', - // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', + 'Compact/wide view' => 'Compacte/breedbeeld-weergave', + 'No results match:' => 'Geen resultaten voor', + 'Currency' => 'Valuta', + 'Private project' => 'Privé project', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', // 'CHF - Swiss Francs' => '', // 'Custom Stylesheet' => '', // 'download' => '', - // 'EUR - Euro' => '', + 'EUR - Euro' => 'EUR - Euro', // 'GBP - British Pound' => '', // 'INR - Indian Rupee' => '', // 'JPY - Japanese Yen' => '', // 'NZD - New Zealand Dollar' => '', // 'RSD - Serbian dinar' => '', // 'USD - US Dollar' => '', - // 'Destination column' => '', + 'Destination column' => 'Doel kolom', // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', + 'Source column' => 'Bron kolom', // 'Transitions' => '', // 'Executer' => '', // 'Time spent in the column' => '', // 'Task transitions' => '', // 'Task transitions export' => '', // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - // 'Currency rates' => '', - // 'Rate' => '', + 'Currency rates' => 'Wisselkoersen', + 'Rate' => 'Koers', // 'Change reference currency' => '', // 'Add a new currency rate' => '', // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', - // 'Webhook URL' => '', + 'Webhook URL' => 'Webhook URL', // '%s remove the assignee of the task %s' => '', // 'Enable Gravatar images' => '', // 'Information' => '', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -689,12 +644,11 @@ return array( // 'Identifier' => '', // 'Disable two factor authentication' => '', // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', + 'Edit link' => 'Bewerk link', // 'Start to type task title...' => '', // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -708,10 +662,10 @@ return array( // 'Action date' => '', // 'Base date to calculate new due date: ' => '', // 'This task has created this child task: ' => '', - // 'Day(s)' => '', + 'Day(s)' => 'Dag(en)', // 'Existing due date' => '', // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', + 'Month(s)' => 'Maand(en)', // 'Recurrence' => '', // 'This task has been created by: ' => '', // 'Recurrent task has been generated:' => '', @@ -720,10 +674,10 @@ return array( // 'When task is closed' => '', // 'When task is moved from first column' => '', // 'When task is moved to last column' => '', - // 'Year(s)' => '', - // 'Calendar settings' => '', + 'Year(s)' => 'Jaar/Jaren', + 'Calendar settings' => 'Kalender instellingen', // 'Project calendar view' => '', - // 'Project settings' => '', + 'Project settings' => 'Project instellingen', // 'Show subtasks based on the time tracking' => '', // 'Show tasks based on the creation date' => '', // 'Show tasks based on the start date' => '', @@ -731,19 +685,19 @@ return array( // 'User calendar view' => '', // 'Automatically update the start date' => '', // 'iCal feed' => '', - // 'Preferences' => '', + 'Preferences' => 'Voorkeuren', // 'Security' => '', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', + 'Two factor authentication disabled' => 'Authenticatie in twee stappen uitgeschakeld', + 'Two factor authentication enabled' => 'Authenticatie in twee stappen ingeschakeld', + 'Unable to update this user.' => 'Niet mogelijk om deze gebruiker bij te werken', // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', + 'User that will receive the email' => 'Gebruiker die de mail ontvangt', + 'Email subject' => 'Email onderwerp', + 'Date' => 'Datum', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', + 'Reopen a task' => 'Heropen een taak', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -752,10 +706,10 @@ return array( // 'Notification' => '', // '%s moved the task #%d to the first swimlane' => '', // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', + 'Swimlane' => 'Swimlane', + 'Gravatar' => 'Gravatar', + '%s moved the task %s to the first swimlane' => '%s heeft de taak %s naar de eerste swimlane verplaatst', + '%s moved the task %s to the swimlane "%s"' => '%s heeft taak %s naar swimlane "%s" verplaatst', // 'This report contains all subtasks information for the given date range.' => '', // 'This report contains all tasks information for the given date range.' => '', // 'Project activities for %s' => '', @@ -763,7 +717,7 @@ return array( // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', + 'New title: %s' => 'Nieuw titel: %s', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', // 'There is no category now' => '', @@ -776,44 +730,37 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', + 'I want to receive notifications for:' => 'Ik wil notificaties ontvangen voor:', + 'All tasks' => 'Alle taken', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', - // '%%Y-%%m-%%d' => '', + '%%Y-%%m-%%d' => '%%d-%%m-%%Y', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', + '<15m' => '<15m', + '<30m' => '<30m', + 'Stop timer' => 'Stop timer', + 'Start timer' => 'Start timer', + 'Add project member' => 'Voeg projectlid toe', + 'Enable notifications' => 'Schakel notificaties in', + 'My activity stream' => 'Mijn activiteiten', + 'My calendar' => 'Mijn kalender', + 'Search tasks' => 'Zoek taken', + 'Back to the calendar' => 'Terug naar de kalender', + 'Filters' => 'Filters', + 'Reset filters' => 'Reset filters', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', // 'Tasks due tomorrow' => '', // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', + 'Closed tasks' => 'Gesloten taken', + 'Open tasks' => 'Open taken', // 'Not assigned' => '', // 'View advanced search syntax' => '', - // 'Overview' => '', - // '%b %e %Y' => '', + 'Overview' => 'Overzicht', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -822,9 +769,9 @@ return array( // 'There is no activity yet.' => '', // 'No tasks found.' => '', // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', + 'List' => 'Lijst', + 'Filter' => 'Filter', + 'Advanced search' => 'Uitgebreid zoeken', // 'Example of query: ' => '', // 'Search by project: ' => '', // 'Search by column: ' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -896,13 +835,12 @@ return array( // 'People who are project managers' => '', // 'People who are project members' => '', // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', + 'Show this column' => 'Toon deze kolom', + 'Hide this column' => 'Verberg deze kolom', + 'open file' => 'open bestand', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', + 'Members' => 'Leden', // 'Shared project' => '', // 'Project managers' => '', // 'Gantt chart for all projects' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', + 'Milestone' => 'Mijlpaal', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index ee0ceb47..747cd2c8 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Edytuj', 'remove' => 'usuń', 'Remove' => 'Usuń', - 'Update' => 'Aktualizuj', 'Yes' => 'Tak', 'No' => 'Nie', 'cancel' => 'anuluj', @@ -28,7 +27,7 @@ return array( 'Cyan' => 'Cyjan', 'Lime' => 'Limonkowy', 'Light Green' => 'Jasnozielony', - 'Amber' => 'Amber', + 'Amber' => 'Bursztynowy', 'Save' => 'Zapisz', 'Login' => 'Login', 'Official website:' => 'Oficjalna strona:', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Akcje', 'Inactive' => 'Nieaktywny', 'Active' => 'Aktywny', - 'Add this column' => 'Dodaj kolumnę', '%d tasks on the board' => '%d zadań na tablicy', '%d tasks in total' => '%d wszystkich zadań', 'Unable to update this board.' => 'Nie można zaktualizować tablicy.', @@ -72,9 +70,8 @@ return array( 'Remove project' => 'Usuń projekt', 'Edit the board for "%s"' => 'Edytuj tablicę dla "%s"', 'All projects' => 'Wszystkie projekty', - 'Change columns' => 'Zmień kolumny', 'Add a new column' => 'Dodaj nową kolumnę', - 'Title' => 'Tytuł', + 'Title' => 'Nazwa', 'Nobody assigned' => 'Nikt nie przypisany', 'Assigned to %s' => 'Przypisane do %s', 'Remove a column' => 'Usuń kolumnę', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Otwórz zadanie', 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', 'Back to the board' => 'Powrót do tablicy', - 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M', 'There is nobody assigned' => 'Nikt nie jest przypisany', 'Column on the board:' => 'Kolumna na tablicy:', - 'Status is open' => 'Status otwarty', - 'Status is closed' => 'Status zamknięty', 'Close this task' => 'Zamknij zadanie', 'Open this task' => 'Otwórz zadanie', 'There is no description.' => 'Brak opisu.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'W trakcie', 'Done' => 'Zakończone', 'Application version:' => 'Wersja aplikacji:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M', - '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', - 'Date created' => 'Data utworzenia', - 'Date completed' => 'Data zakończenia', 'Id' => 'Id', '%d closed tasks' => '%d zamkniętych zadań', 'No task for this project' => 'Brak zadań dla tego projektu', @@ -176,8 +166,8 @@ return array( 'Task count' => 'Liczba zadań', 'User' => 'Użytkownik', 'Comments' => 'Komentarze', - 'Write your text in Markdown' => 'Możesz użyć Markdown', - 'Leave a comment' => 'Zostaw komentarz', + 'Write your text in Markdown' => 'Zobacz jak formatować tekst z użyciem Markdown', + 'Leave a comment' => 'Wstaw komentarz', 'Comment is required' => 'Komentarz jest wymagany', 'Leave a description' => 'Dodaj opis', 'Comment added successfully.' => 'Komentarz dodany', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Edytuj zadanie', 'Due Date' => 'Termin', 'Invalid date' => 'Błędna data', - 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', - '%B %e, %Y' => '%e %B %Y', - '%b %e, %Y' => '%e %B %Y', 'Automatic actions' => 'Akcje automatyczne', 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Nie można usunąć akcji', 'Action removed successfully.' => 'Akcja usunięta', 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', - 'Defined actions' => 'Zdefiniowane akcje', 'Add an action' => 'Nowa akcja', 'Event name' => 'Nazwa zdarzenia', 'Action name' => 'Nazwa akcji', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję', 'Next step' => 'Następny krok', 'Define action parameters' => 'Zdefiniuj parametry akcji', - 'Save this action' => 'Zapisz akcję', 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?', 'Remove an automatic action' => 'Usuń akcję automatyczną', 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika', @@ -218,18 +203,15 @@ return array( 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', 'Column title' => 'Tytuł kolumny', 'Position' => 'Pozycja', - 'Move Up' => 'Przenieś wyżej', - 'Move Down' => 'Przenieś niżej', 'Duplicate to another project' => 'Skopiuj do innego projektu', 'Duplicate' => 'Utwórz kopię', 'link' => 'link', 'Comment updated successfully.' => 'Komentarz został zapisany.', - 'Unable to update your comment.' => 'Nie udało się zapisanie komentarza.', + 'Unable to update your comment.' => 'Nie udało się zapisać komentarza.', 'Remove a comment' => 'Usuń komentarz', 'Comment removed successfully.' => 'Komentarz został usunięty.', 'Unable to remove this comment.' => 'Nie udało się usunąć komentarza.', 'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?', - 'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostęp do tej strony.', 'Current password for the user "%s"' => 'Aktualne hasło dla użytkownika "%s"', 'The current password is required' => 'Wymanage jest aktualne hasło', 'Wrong password' => 'Błędne hasło', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'Email', - 'Link my Google Account' => 'Połącz z kontem Google', - 'Unlink my Google Account' => 'Rozłącz z kontem Google', - 'Login with my Google Account' => 'Zaloguj przy pomocy konta Google', - 'Project not found.' => 'Projek nieznaleziony.', 'Task removed successfully.' => 'Zadanie usunięto pomyślnie.', 'Unable to remove this task.' => 'Nie można usunąć tego zadania.', 'Remove a task' => 'Usuń zadanie', @@ -295,7 +273,7 @@ return array( 'Attach a document' => 'Dołącz plik', 'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?', 'Attachments' => 'Załączniki', - 'Edit the task' => 'Edytuj Zadanie', + 'Edit the task' => 'Edytuj zadanie', 'Edit the description' => 'Edytuj opis', 'Add a comment' => 'Dodaj komentarz', 'Edit a comment' => 'Edytuj komentarz', @@ -305,11 +283,11 @@ return array( 'Spent:' => 'Przeznaczony:', 'Do you really want to remove this sub-task?' => 'Czy na pewno chcesz usunąć to pod-zadanie?', 'Remaining:' => 'Pozostało:', - 'hours' => 'godzin', + 'hours' => 'godzin(y)', 'spent' => 'przeznaczono', 'estimated' => 'szacowany', - 'Sub-Tasks' => 'Pod-zadanie', - 'Add a sub-task' => 'Dodaj pod-zadanie', + 'Sub-Tasks' => 'Podzadania', + 'Add a sub-task' => 'Dodaj podzadanie', 'Original estimate' => 'Szacowanie początkowe', 'Create another sub-task' => 'Dodaj kolejne pod-zadanie', 'Time spent' => 'Przeznaczony czas', @@ -319,19 +297,15 @@ return array( 'Todo' => 'Do zrobienia', 'In progress' => 'W trakcie', 'Sub-task removed successfully.' => 'Pod-zadanie usunięte pomyślnie.', - 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.', + 'Unable to remove this sub-task.' => 'Nie można usunąć tego podzadania.', 'Sub-task updated successfully.' => 'Pod-zadanie zaktualizowane pomyślnie.', - 'Unable to update your sub-task.' => 'Nie można zaktualizować tego pod-zadania.', - 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.', + 'Unable to update your sub-task.' => 'Nie można zaktualizować tego podzadania.', + 'Unable to create your sub-task.' => 'Nie można utworzyć tego podzadania.', 'Sub-task added successfully.' => 'Pod-zadanie utworzone pomyślnie', 'Maximum size: ' => 'Maksymalny rozmiar: ', 'Unable to upload the file.' => 'Nie można wczytać pliku.', 'Display another project' => 'Wyświetl inny projekt', - 'Login with my Github Account' => 'Zaloguj przy użyciu konta Github', - 'Link my Github Account' => 'Podłącz konto Github', - 'Unlink my Github Account' => 'Odłącz konto Github', 'Created by %s' => 'Utworzone przez %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Ostatnio zmienione %e %B %Y o %k:%M', 'Tasks Export' => 'Eksport zadań', 'Tasks exportation for "%s"' => 'Eksport zadań dla "%s"', 'Start Date' => 'Data początkowa', @@ -349,7 +323,7 @@ return array( 'The task #%d have been opened.' => 'Zadania #%d zostały otwarte.', 'The task #%d have been closed.' => 'Zadania #%d zostały zamknięte.', 'Sub-task updated' => 'Pod-zadanie zaktualizowane', - 'Title:' => 'Tytuł:', + 'Title:' => 'Nazwa:', // 'Status:' => '', 'Assignee:' => 'Przypisano do:', 'Time tracking:' => 'Śledzenie czasu: ', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadomienia tylko dla poniższych projektów:', 'view the task on Kanboard' => 'Zobacz zadanie', 'Public access' => 'Dostęp publiczny', - 'User management' => 'Zarządzanie użytkownikami', 'Active tasks' => 'Aktywne zadania', 'Disable public access' => 'Zablokuj dostęp publiczny', 'Enable public access' => 'Odblokuj dostęp publiczny', @@ -385,7 +358,7 @@ return array( 'Remote' => 'Zdalne', 'Enabled' => 'Odblokowane', 'Disabled' => 'Zablokowane', - 'Username:' => 'Nazwa Użytkownika:', + 'Username:' => 'Nazwa Użytkownika (login):', 'Name:' => 'Imię i Nazwisko', 'Email:' => 'Email: ', 'Notifications:' => 'Powiadomienia: ', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Zmień hasło', 'Password modification' => 'Zmiana hasła', 'External authentications' => 'Autentykacja zewnętrzna', - 'Google Account' => 'Konto Google', - 'Github Account' => 'Konto Github', 'Never connected.' => 'Nigdy nie połączone.', - 'No account linked.' => 'Brak połączonych kont.', - 'Account linked.' => 'Konto połączone.', 'No external authentication enabled.' => 'Brak autentykacji zewnętrznych.', 'Password modified successfully.' => 'Hasło zmienione pomyślne.', 'Unable to change the password.' => 'Nie można zmienić hasła.', @@ -411,8 +380,8 @@ return array( '%s moved the task %s to the column "%s"' => '%s przeniósł zadanie %s do kolumny "%s"', '%s created the task %s' => '%s utworzył zadanie %s', '%s closed the task %s' => '%s zamknął zadanie %s', - '%s created a subtask for the task %s' => '%s utworzył pod-zadanie dla zadania %s', - '%s updated a subtask for the task %s' => '%s zaktualizował pod-zadanie dla zadania %s', + '%s created a subtask for the task %s' => '%s utworzył podzadanie dla zadania %s', + '%s updated a subtask for the task %s' => '%s zaktualizował podzadanie dla zadania %s', 'Assigned to %s with an estimate of %s/%sh' => 'Przypisano do %s z szacowanym czasem wykonania %s/%sh', 'Not assigned, estimate of %sh' => 'Nie przypisane, szacowany czas wykonania %sh', '%s updated a comment on the task %s' => '%s zaktualizował komentarz do zadania %s', @@ -421,8 +390,8 @@ return array( 'RSS feed' => 'Kanał RSS', '%s updated a comment on the task #%d' => '%s zaktualizował komentarz do zadania #%d', '%s commented on the task #%d' => '%s skomentował zadanie #%d', - '%s updated a subtask for the task #%d' => '%s zaktualizował pod-zadanie dla zadania #%d', - '%s created a subtask for the task #%d' => '%s utworzył pod-zadanie dla zadania #%d', + '%s updated a subtask for the task #%d' => '%s zaktualizował podzadanie dla zadania #%d', + '%s created a subtask for the task #%d' => '%s utworzył podzadanie dla zadania #%d', '%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', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrznej etykiety', // 'Reference' => '', - // 'Reference: %s' => '', 'Label' => 'Etykieta', 'Database' => 'Baza danych', 'About' => 'Informacje', @@ -459,23 +427,19 @@ return array( 'Frequency in second (60 seconds by default)' => 'Częstotliwość w sekundach (domyślnie 60 sekund)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Częstotliwość w sekundach (0 aby zablokować, domyślnie 10 sekund)', 'Application URL' => 'Adres URL aplikacji', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Przykład: http://example.kanboard.net/ (Używane przez powiadomienia email)', 'Token regenerated.' => 'Token wygenerowany ponownie.', 'Date format' => 'Format daty', 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"', 'New private project' => 'Nowy projekt prywatny', 'This project is private' => 'Ten projekt jest prywatny', - 'Type here to create a new sub-task' => 'Wpisz tutaj aby utworzyć pod-zadanie', + 'Type here to create a new sub-task' => 'Nazwa podzadania', 'Add' => 'Dodaj', - 'Estimated time: %s hours' => 'Szacowany czas: %s godzin', - 'Time spent: %s hours' => 'Przeznaczony czas: %s godzin', - 'Started on %B %e, %Y' => 'Rozpoczęto %e %B %Y', 'Start date' => 'Data rozpoczęcia', 'Time estimated' => 'Szacowany czas', 'There is nothing assigned to you.' => 'Nie ma przypisanych zadań', 'My tasks' => 'Moje zadania', - 'Activity stream' => 'Dziennik aktywności', - 'Dashboard' => 'Panel', + 'Activity stream' => 'Strumień aktywności', + 'Dashboard' => 'Dashboard', 'Confirmation' => 'Potwierdzenie', 'Allow everybody to access to this project' => 'Udostępnij ten projekt wszystkim', 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', @@ -493,8 +457,8 @@ return array( 'Reportings' => 'Raporty', 'Task repartition for "%s"' => 'Przydział zadań dla "%s"', 'Analytics' => 'Analizy', - 'Subtask' => 'Pod-zadanie', - 'My subtasks' => 'Moje pod-zadania', + 'Subtask' => 'Podzadanie', + 'My subtasks' => 'Moje podzadania', 'User repartition' => 'Przydział użytkownika', 'User repartition for "%s"' => 'Przydział użytkownika dla "%s"', 'Clone this project' => 'Sklonuj ten projekt', @@ -505,7 +469,7 @@ return array( 'The project id must be an integer' => 'ID projektu musi być liczbą całkowitą', 'The status must be an integer' => 'Status musi być liczbą całkowitą', 'The subtask id is required' => 'ID pod-zadanie jest wymagane', - 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą', + 'The subtask id must be an integer' => 'ID podzadania musi być liczbą całkowitą', 'The task id is required' => 'ID zadania jest wymagane', 'The task id must be an integer' => 'ID zadania musi być liczbą całkowitą', 'The user id must be an integer' => 'ID użytkownika musi być liczbą całkowitą', @@ -514,9 +478,9 @@ return array( 'Unable to create this task.' => 'Nie można tworzyć zadania.', 'Cumulative flow diagram' => 'Zbiorowy diagram przepływu', 'Cumulative flow diagram for "%s"' => 'Zbiorowy diagram przepływu dla "%s"', - 'Daily project summary' => 'Dzienne podsumowanie projektu', + 'Daily project summary' => 'Dzienne raport z projektu', 'Daily project summary export' => 'Eksport dziennego podsumowania projektu', - 'Daily project summary export for "%s"' => 'Eksport dziennego podsumowania projektu dla "%s"', + 'Daily project summary export for "%s"' => 'Wygeneruj dzienny raport dla projektu: "%s"', 'Exports' => 'Eksporty', 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień', 'Nothing to preview...' => 'Nic do podejrzenia...', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć proces: "%s"?', 'Inactive swimlanes' => 'Nieaktywne procesy', 'Remove a swimlane' => 'Usuń proces', - 'Rename' => 'Zmień nazwe', 'Show default swimlane' => 'Pokaż domyślny proces', 'Swimlane modification for the project "%s"' => 'Edycja procesów dla projektu "%s"', 'Swimlane not found.' => 'Nie znaleziono procesu.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Procesy', 'Swimlane updated successfully.' => 'Proces zaktualizowany pomyślnie.', 'The default swimlane have been updated successfully.' => 'Domyślny proces zaktualizowany pomyślnie.', - 'Unable to create your swimlane.' => 'Nie można utworzyć procesu.', 'Unable to remove this swimlane.' => 'Nie można usunąć procesu.', 'Unable to update this swimlane.' => 'Nie można zaktualizować procesu.', 'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.', @@ -546,11 +508,11 @@ return array( 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja z usługami firm trzecich', 'Subtask Id' => 'ID pod-zadania', - 'Subtasks' => 'Pod-zadania', - 'Subtasks Export' => 'Eksport pod-zadań', - 'Subtasks exportation for "%s"' => 'Eksporty pod-zadań dla "%s"', - 'Task Title' => 'Tytuł zadania', - 'Untitled' => 'Bez tytułu', + 'Subtasks' => 'Podzadania', + 'Subtasks Export' => 'Eksport podzadań', + 'Subtasks exportation for "%s"' => 'Wygeneruj raport podzadań dla projektu "%s"', + 'Task Title' => 'Nazwa zadania', + 'Untitled' => 'Bez nazwy', 'Application default' => 'Domyślne dla aplikacji', 'Language:' => 'Język:', 'Timezone:' => 'Strefa czasowa:', @@ -571,7 +533,7 @@ return array( 'Time Tracking' => 'Śledzenie czasu', 'You already have one subtask in progress' => 'Masz już zadanie o statusie "w trakcie"', 'Which parts of the project do you want to duplicate?' => 'Które elementy projektu chcesz zduplikować?', - // 'Disallow login form' => '', + 'Disallow login form' => 'Zablokuj możliwość logowania', 'Start' => 'Początek', 'End' => 'Koniec', 'Task age in days' => 'Wiek zadania w dniach', @@ -612,7 +574,6 @@ return array( 'This task' => 'To zadanie', // '<1h' => '', // '%dh' => '', - '%b %e' => '%e %b', 'Expand tasks' => 'Rozwiń zadania', 'Collapse tasks' => 'Zwiń zadania', 'Expand/collapse tasks' => 'Zwiń/Rozwiń zadania', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Skróty klawiszowe', 'Open board switcher' => 'Przełącz tablice', 'Application' => 'Aplikacja', - 'since %B %e, %Y at %k:%M %p' => 'od %e %B %Y o %k:%M', 'Compact view' => 'Widok kompaktowy', 'Horizontal scrolling' => 'Przewijanie poziome', 'Compact/wide view' => 'Pełny/Kompaktowy widok', 'No results match:' => 'Brak wyników:', 'Currency' => 'Waluta', - 'Files' => 'Pliki', - 'Images' => 'Obrazy', 'Private project' => 'Projekt prywatny', 'AUD - Australian Dollar' => 'AUD - Dolar australijski', 'CAD - Canadian Dollar' => 'CAD - Dolar kanadyjski', @@ -650,8 +608,8 @@ return array( 'Transitions' => 'Przeniesienia', 'Executer' => 'Wykonał', 'Time spent in the column' => 'Czas spędzony w tej kolumnie', - 'Task transitions' => 'Przeniesienia zadania', - 'Task transitions export' => 'Export przeniesień zadania', + 'Task transitions' => 'Przeniesienia zadań', + 'Task transitions export' => 'Wygeneruj raport z przeniesień zadań', 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ten raport zawiera wszystkie przeniesienia pomiędzy kolumnami wraz z datą oraz osobą odpowiedzialną', 'Currency rates' => 'Kursy walut', 'Rate' => 'Kurs', @@ -675,14 +633,11 @@ return array( 'Test your device' => 'Przetestuj urządzenie', 'Assign a color when the task is moved to a specific column' => 'Przypisz kolor gdy zadanie jest przeniesione do danej kolumny', '%s via Kanboard' => '%s poprzez Kanboard', - 'uploaded by: %s' => 'Dodane przez: %s', - 'uploaded on: %s' => 'Data dodania: %s', - 'size: %s' => 'Rozmiar: %s', 'Burndown chart for "%s"' => 'Wykres Burndown dla "%s"', 'Burndown chart' => 'Wykres Burndown', // 'This chart show the task complexity over the time (Work Remaining).' => '', 'Screenshot taken %s' => 'Zrzut ekranu zapisany %s', - 'Add a screenshot' => 'Dodaj zrzut ekranu', + 'Add a screenshot' => 'Dołącz zrzut ekranu', 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Zrób zrzut ekranu i wciśnij CTRL+V by dodać go tutaj.', 'Screenshot uploaded successfully.' => 'Zrzut ekranu dodany.', 'SEK - Swedish Krona' => 'SEK - Korona szwedzka', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Link do zadania nie może wskazywać na samego siebie', 'The exact same link already exists' => 'Taki link już istnieje', 'Recurrent task is scheduled to be generated' => 'Zaplanowano zadanie cykliczne', - 'Recurring information' => 'Informacje o zadaniu cyklicznym', 'Score' => 'Wynik', 'The identifier must be unique' => 'Identyfikator musi być unikatowy', 'This linked task id doesn\'t exists' => 'Id zadania nie istnieje', @@ -724,7 +678,7 @@ return array( 'Calendar settings' => 'Ustawienia kalendarza', 'Project calendar view' => 'Widok kalendarza projektu', 'Project settings' => 'Ustawienia Projektu', - 'Show subtasks based on the time tracking' => 'Pokaż pod-zadania w śledzeniu czasu', + 'Show subtasks based on the time tracking' => 'Pokaż podzadania w śledzeniu czasu', 'Show tasks based on the creation date' => 'Pokaż zadania względem daty utworzenia', 'Show tasks based on the start date' => 'Pokaż zadania względem daty rozpoczęcia', 'Subtasks time tracking' => 'Śledzenie czasu pod-zadań', @@ -736,10 +690,10 @@ return array( 'Two factor authentication disabled' => 'Uwierzytelnianie dwuetapowe wyłączone', 'Two factor authentication enabled' => 'Uwierzytelnianie dwuetapowe włączone', 'Unable to update this user.' => 'Nie można zaktualizować tego użytkownika', - 'There is no user management for private projects.' => 'Dla projektów prywatnych nie występuje zarządzanie użytkownikami.', + 'There is no user management for private projects.' => 'Projekty prywatne nie wspierają zarządzania użytkownikami. Projekt prywatny ma tylko jednego użytkownika.', // 'User that will receive the email' => '', // 'Email subject' => '', - // 'Date' => '', + 'Date' => 'Data', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', 'Send a task by email to someone' => 'Wyślij zadanie mailem do kogokolwiek', @@ -752,7 +706,7 @@ return array( 'Notification' => 'Powiadomienie', // '%s moved the task #%d to the first swimlane' => '', // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', + 'Swimlane' => 'Proces', // 'Gravatar' => '', // '%s moved the task %s to the first swimlane' => '', // '%s moved the task %s to the swimlane "%s"' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', 'I want to receive notifications for:' => 'Wysyłaj powiadomienia dla:', 'All tasks' => 'Wszystkich zadań', 'Only for tasks assigned to me' => 'Tylko zadań przypisanych do mnie', 'Only for tasks created by me' => 'Tylko zadań utworzonych przeze mnie', 'Only for tasks created by me and assigned to me' => 'Tylko zadań przypisanych lub utworzonych przeze mnie', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - 'New due date: %B %e, %Y' => 'Nowy termin: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Zmiana daty rozpoczęcia: %B %e, %Y', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -812,8 +760,7 @@ return array( 'Open tasks' => 'Otwarte zadania', 'Not assigned' => 'Nieprzypisane zadania', 'View advanced search syntax' => 'Pomoc dotycząca budowania filtrów', - 'Overview' => 'Przegląd', - // '%b %e %Y' => '', + 'Overview' => 'Podsumowanie', 'Board/Calendar/List view' => 'Widok: Tablica/Kalendarz/Lista', 'Switch to the board view' => 'Przełącz na tablicę', 'Switch to the calendar view' => 'Przełącz na kalendarz', @@ -846,28 +793,20 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', - // 'Time spent into each column' => '', + 'Time spent into each column' => 'Czas spędzony przez zadanie w każdej z kolumn', // 'The lead time is the duration between the task creation and the completion.' => '', // 'The cycle time is the duration between the start date and the completion.' => '', // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', 'Edit Authentication' => 'Edycja autoryzacji', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', 'New remote user' => 'Nowy użytkownik zdalny', 'New local user' => 'Nowy użytkownik lokalny', 'Default task color' => 'Domyślny kolor zadań', - 'Hide sidebar' => 'Ukryj menu boczne', - 'Expand sidebar' => 'Pokaż menu boczne', 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( 'open file' => 'otwórz plik', 'End date' => 'Data zakończenia', 'Users overview' => 'Przegląd użytkowników', - 'Managers' => 'Menedżerowie', 'Members' => 'Uczestnicy', 'Shared project' => 'Projekt udostępniony', 'Project managers' => 'Menedżerowie projektu', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Data zakończenia:', 'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu', 'Projects Gantt chart' => 'Wykres Gantta dla projektów', - 'Start date: %s' => 'Data rozpoczęcia: %s', - 'End date: %s' => 'Data zakończenia: %s', - 'Link type' => 'Typ adresu URL', + 'Link type' => 'Rodzaj link\'u', 'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL', 'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji', - // 'Login with my Gitlab Account' => '', 'Milestone' => 'Kamień milowy', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -935,9 +864,9 @@ return array( 'Version' => 'Wersja', 'Plugins' => 'Wtyczki', 'There is no plugin loaded.' => 'Nie wykryto żadnych wtyczek.', - 'Set maximum column height' => 'Ustaw maksymalną wysokość kolumn', - 'Remove maximum column height' => 'Usuń maksymalną wysokość kolumn', - 'My notifications' => 'Moje powiadomienia', + 'Set maximum column height' => 'Rozwiń kolumny', + 'Remove maximum column height' => 'Zwiń kolumny', + 'My notifications' => 'Powiadomienia', 'Custom filters' => 'Dostosuj filtry', // 'Your custom filter have been created successfully.' => '', // 'Unable to create your custom filter.' => '', @@ -948,10 +877,10 @@ return array( // 'Unable to update custom filter.' => '', // 'Web' => '', 'New attachment on task #%d: %s' => 'Nowy załącznik do zadania #%d: %s', - 'New comment on task #%d' => 'Nowy załącznik #%d', - 'Comment updated on task #%d' => 'Comment updated on task #%d', - 'New subtask on task #%d' => 'Nowe pod-zadanie dla zadania #%d', - 'Subtask updated on task #%d' => 'Aktualizacja pod-zadania w zadaniu #%d', + 'New comment on task #%d' => 'Nowy komentarz do zadania #%d', + 'Comment updated on task #%d' => 'Aktualizacja komentarza do zadania #%d', + 'New subtask on task #%d' => 'Nowe podzadanie dla zadania #%d', + 'Subtask updated on task #%d' => 'Aktualizacja podzadania w zadaniu #%d', 'New task #%d: %s' => 'Nowe zadanie #%d: %s', 'Task updated #%d' => 'Aktualizacja zadania #%d', 'Task #%d closed' => 'Zamknięto zadanie #%d', @@ -994,7 +923,7 @@ return array( // 'Append' => '', // 'Replace' => '', // 'Import' => '', - // 'change sorting' => '', + 'change sorting' => 'odwróć sortowanie', 'Tasks Importation' => 'Import zadań', 'Delimiter' => 'Separator pola', 'Enclosure' => 'Separator tekstu', @@ -1044,18 +973,18 @@ return array( 'View group members' => 'Wyświetl wszystkich członków grupy', 'There is no user available.' => 'Żaden użytkownik nie jest dostępny.', 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy napewno chcesz usunąć użytkownika "%s" z grupy "%s"?', - 'There is no group.' => 'Brak grup.', + 'There is no group.' => 'Nie utworzono jeszcze żadnej grupy.', 'External Id' => 'Zewnętrzny Id', 'Add group member' => 'Dodaj członka grupy', 'Do you really want to remove this group: "%s"?' => 'Czy napewno chcesz usunąć grupę "%s"?', 'There is no user in this group.' => 'Wybrana grupa nie posiada członków.', 'Remove this user' => 'Usuń użytkownika', 'Permissions' => 'Prawa dostępu', - 'Allowed Users' => 'Użytkownicy z dostępem', + 'Allowed Users' => 'Użytkownicy, którzy mają dostęp', 'No user have been allowed specifically.' => 'Żaden użytkownik nie ma przyznanego dostępu.', 'Role' => 'Rola', 'Enter user name...' => 'Wprowadź nazwę użytkownika...', - 'Allowed Groups' => 'Dostępne grupy', + 'Allowed Groups' => 'Grupy, które mają dostęp', 'No group have been allowed specifically.' => 'Żadna grupa nie ma przyznanego dostępu.', 'Group' => 'Grupa', 'Group Name' => 'Nazwa grupy', @@ -1063,11 +992,11 @@ return array( 'Role:' => 'Rola:', 'Project members' => 'Uczestnicy projektu', // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', + '%s mentioned you in the task #%d' => '%s wspomiał o Tobie w zadaniu #%d', + '%s mentioned you in a comment on the task #%d' => '%s wspomiał o Tobie w komentarzu do zadania #%d', + 'You were mentioned in the task #%d' => 'Wspomiano o Tobie w zadaniu #%d', + 'You were mentioned in a comment on the task #%d' => 'Wspomiano o Tobie w komentarzu do zadania #%d', + 'Mentioned' => 'Wspomiano', // 'Compare Estimated Time vs Actual Time' => '', // 'Estimated hours: ' => '', // 'Actual hours: ' => '', @@ -1083,39 +1012,143 @@ return array( // 'Two-Factor Provider: ' => '', // 'Disable two-factor authentication' => '', // 'Enable two-factor authentication' => '', - // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', - // 'The password has never been reinitialized.' => '', + 'There is no integration registered at the moment.' => 'W chwili obecnej funkcjonalność ta została wyłączona.', + 'Password Reset for Kanboard' => 'Resetuj hasło do Kanboarda', + 'Forgot password?' => 'Nie pamiętasz hasła?', + 'Enable "Forget Password"' => 'Włącz możliwość odzyskiwania zapomianego hasła przez użytkowników', + 'Password Reset' => 'Resetuj hasło', + 'New password' => 'Nowe hasło', + 'Change Password' => 'Zmień hasło', + 'To reset your password click on this link:' => 'Kliknij w poniższy link, aby zresetować hasło:', + 'Last Password Reset' => 'Ostatnie odzyskiwanie hasła', + 'The password has never been reinitialized.' => 'Hasło nigdy nie było odzyskiwane.', // 'Creation' => '', // 'Expiration' => '', - // 'Password reset history' => '', + 'Password reset history' => 'Historia hasła', // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', // 'Do you really want to close all tasks of this column?' => '', // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', + 'Close all tasks of this column' => 'Zamknij wszystkie zadania w tej kolumnie', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Wtyczki obsługujące dodatkowe powiadomienia nie zostały zainstalowane. Dalej jednak możesz korzystać z standardowych powiadomień (sprawdź w ustawieniach Twojego profilu).', + 'My dashboard' => 'Mój dashboard', + 'My profile' => 'Mój profil', // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Identyfikator projektu jest opcjonalny i musi być alfanumeryczny, przykład: MYPROJECT.', + 'Project owner' => 'Właściciel projektu', + 'Those dates are useful for the project Gantt chart.' => 'Daty te są przydatne dla wykresu Gantta.', + 'Private projects do not have users and groups management.' => 'Projekty prywatne nie wspierają obsługi użytkowników i grup.', // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + 'Priority' => 'Priorytet', + 'Task priority' => 'Priorytety zadań', + 'General' => 'Ogólne', + 'Dates' => 'Czas życia projektu', + 'Default priority' => 'Domyślny priorytet', + 'Lowest priority' => 'Najniższy priorytet', + 'Highest priority' => 'Najwyższy priorytet', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Jeżeli dla najniższego i najwyższego priorytetu ustawisz 0, to priorytety dla tablicy zostaną wyłączone.', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + 'Auto' => 'Automatyczny', + 'Related' => 'Powiązanie', + 'Attachment' => 'Załącznik', + // 'Title not found' => '', + 'Web Link' => 'Link URL', + 'External links' => 'Linki zewnętrzne', + 'Add external link' => 'Dodaj link zewnętrzny', + 'Type' => 'Typ', + 'Dependency' => 'Zależność', + 'Add internal link' => 'Dodaj link do innego zadania', + 'Add a new external link' => 'Dodaj nowy link zewnętrzny', + 'Edit external link' => 'Edytuj link zewnętrzny', + 'External link' => 'Link zewnętrzny', + 'Copy and paste your link here...' => 'Skopiuj i wklej link tutaj ...', + // 'URL' => '', + 'There is no external link for the moment.' => 'Brak linków zewnętrznych.', + 'Internal links' => 'Linki do innych zadań', + 'There is no internal link for the moment.' => 'Brak powiązań do innych zadań.', + // 'Assign to me' => '', + 'Me' => 'JA', + 'Do not duplicate anything' => 'Nie kopiuj żadnego projektu', + 'Projects management' => 'Zarządzanie projektami', + 'Users management' => 'Zarządzanie użytkownikami', + 'Groups management' => 'Zarządzanie grupami', + 'Create from another project' => 'Utwórz na podstawie innego projektu', + 'There is no subtask at the moment.' => 'Brak podzadań.', + 'open' => 'otwarty', + 'closed' => 'zamknięty', + 'Priority:' => 'Priorytet:', + // 'Reference:' => '', + 'Complexity:' => 'Złożoność:', + 'Swimlane:' => 'Proces:', + 'Column:' => 'Kolumna:', + 'Position:' => 'Pozycja:', + 'Creator:' => 'Utworzył:', + 'Time estimated:' => 'Szacowany czas:', + '%s hours' => '%s godzin', + 'Time spent:' => 'Wykorzystany czas:', + 'Created:' => 'Utworzone:', + 'Modified:' => 'Zmodyfikowane:', + 'Completed:' => 'Ukończone:', + 'Started:' => 'Rozpoczęte:', + 'Moved:' => 'Przesunięcie:', + 'Task #%d' => 'Zadanie #%d', + 'Sub-tasks' => 'Podzadania', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + 'Drag and drop your files here' => 'Przeciągnij i upuść pliki tutaj', + 'choose files' => 'wybierz pliki', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + 'Download' => 'Pobierz', + 'Uploaded: %s' => 'Data załączenia: %s', + 'Size: %s' => 'Rozmiar: %s', + 'Uploaded by %s' => 'Załadowany przez %s', + 'Filename' => 'Nazwa pliku', + 'Size' => 'Rozmiar', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + 'Upload a file' => 'Prześlij plik', + 'There is no attachment at the moment.' => 'Brak załączników.', + 'View file' => 'Wyświetl plik', + 'Last activity' => 'Ostatnia aktywność', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 09e87048..62063891 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Editar', 'remove' => 'remover', 'Remove' => 'Remover', - 'Update' => 'Atualizar', 'Yes' => 'Sim', 'No' => 'Não', 'cancel' => 'cancelar', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Ações', 'Inactive' => 'Inativo', 'Active' => 'Ativo', - 'Add this column' => 'Adicionar esta coluna', '%d tasks on the board' => '%d tarefas no board', '%d tasks in total' => '%d tarefas no total', 'Unable to update this board.' => 'Não foi possível atualizar este board.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Remover projeto', 'Edit the board for "%s"' => 'Editar o board para "%s"', 'All projects' => 'Todos os projetos', - 'Change columns' => 'Modificar colunas', 'Add a new column' => 'Adicionar uma nova coluna', 'Title' => 'Título', 'Nobody assigned' => 'Ninguém designado', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Abrir uma tarefa', 'Do you really want to open this task: "%s"?' => 'Você realmente deseja abrir esta tarefa: "%s"?', 'Back to the board' => 'Voltar ao board', - 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', 'There is nobody assigned' => 'Não há ninguém designado', 'Column on the board:' => 'Coluna no board:', - 'Status is open' => 'Status está aberto', - 'Status is closed' => 'Status está finalizado', 'Close this task' => 'Finalizar esta tarefa', 'Open this task' => 'Abrir esta tarefa', 'There is no description.' => 'Não há descrição.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Em andamento', 'Done' => 'Finalizado', 'Application version:' => 'Versão da aplicação:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M', - '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', - 'Date created' => 'Data de criação', - 'Date completed' => 'Data da finalização', 'Id' => 'Id', '%d closed tasks' => '%d tarefas finalizadas', 'No task for this project' => 'Não há tarefa para este projeto', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', 'Invalid date' => 'Data inválida', - 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d %B %Y', 'Automatic actions' => 'Ações automáticas', 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', 'Unable to create your automatic action.' => 'Não é possível criar sua ação automática.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Não é possível remover esta ação.', 'Action removed successfully.' => 'Ação removida com sucesso.', 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', - 'Defined actions' => 'Ações definidas', 'Add an action' => 'Adicionar Ação', 'Event name' => 'Nome do evento', 'Action name' => 'Nome da ação', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a ação correspondente.', 'Next step' => 'Próximo passo', 'Define action parameters' => 'Definir parêmetros da ação', - 'Save this action' => 'Salvar esta ação', 'Do you really want to remove this action: "%s"?' => 'Você realmente deseja remover esta ação: "%s"?', 'Remove an automatic action' => 'Remover uma ação automática', 'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', 'Column title' => 'Título da coluna', 'Position' => 'Posição', - 'Move Up' => 'Mover para cima', - 'Move Down' => 'Mover para baixo', 'Duplicate to another project' => 'Duplicar para outro projeto', 'Duplicate' => 'Duplicar', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Comentário removido com sucesso.', 'Unable to remove this comment.' => 'Não é possível remover este comentário.', 'Do you really want to remove this comment?' => 'Você realmente deseja remover este comentário?', - 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.', 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"', 'The current password is required' => 'A senha atual é obrigatória', 'Wrong password' => 'Senha incorreta', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Autenticação externa falhou', 'Your external account is linked to your profile successfully.' => 'Sua conta externa está agora ligada ao seu perfil.', 'Email' => 'E-mail', - 'Link my Google Account' => 'Vincular minha Conta do Google', - 'Unlink my Google Account' => 'Desvincular minha Conta do Google', - 'Login with my Google Account' => 'Entrar com minha Conta do Google', - 'Project not found.' => 'Projeto não encontrado.', 'Task removed successfully.' => 'Tarefa removida com sucesso.', 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.', 'Remove a task' => 'Remover uma tarefa', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Tamanho máximo: ', 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', 'Display another project' => 'Exibir outro projeto', - 'Login with my Github Account' => 'Entrar com minha Conta do Github', - 'Link my Github Account' => 'Associar à minha Conta do Github', - 'Unlink my Github Account' => 'Desassociar a minha Conta do Github', 'Created by %s' => 'Criado por %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', 'Tasks Export' => 'Exportar Tarefas', 'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"', 'Start Date' => 'Data inicial', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projetos:', 'view the task on Kanboard' => 'ver a tarefa no Kanboard', 'Public access' => 'Acesso público', - 'User management' => 'Gerenciamento de usuários', 'Active tasks' => 'Tarefas ativas', 'Disable public access' => 'Desabilitar o acesso público', 'Enable public access' => 'Habilitar o acesso público', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Alterar senha', 'Password modification' => 'Alteração de senha', 'External authentications' => 'Autenticação externa', - 'Google Account' => 'Conta do Google', - 'Github Account' => 'Conta do Github', 'Never connected.' => 'Nunca conectado.', - 'No account linked.' => 'Nenhuma conta associada.', - 'Account linked.' => 'Conta associada.', 'No external authentication enabled.' => 'Nenhuma autenticação externa habilitada.', 'Password modified successfully.' => 'Senha alterada com sucesso.', 'Unable to change the password.' => 'Não foi possível alterar a senha.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo', 'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo', 'Reference' => 'Referência', - 'Reference: %s' => 'Referência: %s', 'Label' => 'Rótulo', 'Database' => 'Banco de dados', 'About' => 'Sobre', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por padrão)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desativar este recurso, 10 segundos por padrão)', 'Application URL' => 'URL da Aplicação', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)', 'Token regenerated.' => 'Novo token gerado.', 'Date format' => 'Formato de data', 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Este projeto é privado', 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova subtarefa', 'Add' => 'Adicionar', - 'Estimated time: %s hours' => 'Tempo estimado: %s horas', - 'Time spent: %s hours' => 'Tempo gasto: %s horas', - 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y', 'Start date' => 'Data de início', 'Time estimated' => 'Tempo estimado', 'There is nothing assigned to you.' => 'Não há nada designado à você.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover esta swimlane: "%s"?', 'Inactive swimlanes' => 'Desativar swimlanes', 'Remove a swimlane' => 'Remover uma swimlane', - 'Rename' => 'Renomear', 'Show default swimlane' => 'Exibir swimlane padrão', 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projeto "%s"', 'Swimlane not found.' => 'Swimlane não encontrada.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane atualizada com sucesso.', 'The default swimlane have been updated successfully.' => 'A swimlane padrão foi atualizada com sucesso.', - 'Unable to create your swimlane.' => 'Não foi possível criar a sua swimlane.', 'Unable to remove this swimlane.' => 'Não foi possível remover esta swimlane.', 'Unable to update this swimlane.' => 'Não foi possível atualizar esta swimlane.', 'Your swimlane have been created successfully.' => 'Sua swimlane foi criada com sucesso.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Esta tarefa', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Expandir tarefas', 'Collapse tasks' => 'Contrair tarefas', 'Expand/collapse tasks' => 'Expandir/Contrair tarefas', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Atalhos de teclado', 'Open board switcher' => 'Abrir o comutador de painel', 'Application' => 'Aplicação', - 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M', 'Compact view' => 'Vista reduzida', 'Horizontal scrolling' => 'Rolagem horizontal', 'Compact/wide view' => 'Alternar entre a vista compacta e ampliada', 'No results match:' => 'Nenhum resultado:', 'Currency' => 'Moeda', - 'Files' => 'Arquivos', - 'Images' => 'Imagens', 'Private project' => 'Projeto privado', 'AUD - Australian Dollar' => 'AUD - Dólar australiano', 'CAD - Canadian Dollar' => 'CAD - Dólar canadense', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Teste o seu dispositivo', 'Assign a color when the task is moved to a specific column' => 'Atribuir uma cor quando a tarefa é movida em uma coluna específica', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'carregado por: %s', - 'uploaded on: %s' => 'carregado em: %s', - 'size: %s' => 'tamanho: %s', 'Burndown chart for "%s"' => 'Gráfico de Burndown para "%s"', 'Burndown chart' => 'Gráfico de Burndown', 'This chart show the task complexity over the time (Work Remaining).' => 'Este gráfico mostra a complexidade da tarefa ao longo do tempo (Trabalho Restante).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Uma tarefa não pode ser vinculada a si própria', 'The exact same link already exists' => 'Um link idêntico já existe', 'Recurrent task is scheduled to be generated' => 'A tarefa recorrente está programada para ser criada', - 'Recurring information' => 'Informação sobre a recorrência', 'Score' => 'Complexidade', 'The identifier must be unique' => 'O identificador deve ser único', 'This linked task id doesn\'t exists' => 'O identificador da tarefa associada não existe', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'O tempo despendido foi mudado: %sh', 'Time estimated changed: %sh' => 'O tempo estimado foi mudado/ %sh', 'The field "%s" have been updated' => 'O campo "%s" foi atualizada', - 'The description have been modified' => 'A descrição foi modificada', + 'The description has been modified' => 'A descrição foi modificada', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Você realmente deseja finalizar a tarefa "%s" e todas as suas subtarefas?', - 'Swimlane: %s' => 'Swimlane: %s', 'I want to receive notifications for:' => 'Eu quero receber as notificações para:', 'All tasks' => 'Todas as tarefas', 'Only for tasks assigned to me' => 'Somente as tarefas atribuídas a mim', 'Only for tasks created by me' => 'Apenas as tarefas que eu criei', 'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Nova data limite: %d/%m/%Y', - 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y', - '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', 'Total for all columns' => 'Total para todas as colunas', 'You need at least 2 days of data to show the chart.' => 'Você precisa de pelo menos 2 dias de dados para visualizar o gráfico.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Não designada', 'View advanced search syntax' => 'Ver a sintaxe para pesquisa avançada', 'Overview' => 'Visão global', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Vista Painel/Calendário/Lista', 'Switch to the board view' => 'Mudar para o modo Painel', 'Switch to the calendar view' => 'Mudar par o modo Calendário', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Este gráfico mostra o tempo médio do Lead and cycle time das últimas %d tarefas.', 'Average time into each column' => 'Tempo médio de cada coluna', 'Lead and cycle time' => 'Lead and cycle time', - 'Google Authentication' => 'Autenticação Google', - 'Help on Google authentication' => 'Ajuda com a autenticação Google', - 'Github Authentication' => 'Autenticação Github', - 'Help on Github authentication' => 'Ajuda com a autenticação Github', 'Lead time: ' => 'Lead time: ', 'Cycle time: ' => 'Cycle time: ', 'Time spent into each column' => 'Tempo gasto em cada coluna', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Se a tarefa não está fechada, a hora atual é usada no lugar da data de conclusão.', 'Set automatically the start date' => 'Definir automaticamente a data de início', 'Edit Authentication' => 'Modificar a autenticação', - 'Google Id' => 'Google ID', - 'Github Id' => 'Github Id', 'Remote user' => 'Usuário remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Os usuários remotos não conservam as suas senhas no banco de dados Kanboard, exemplos: contas LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se você marcar "Interdir o formulário de autenticação", os identificadores entrados no formulário de login serão ignorado.', 'New remote user' => 'Criar um usuário remoto', 'New local user' => 'Criar um usuário local', 'Default task color' => 'Cor padrão para as tarefas', - 'Hide sidebar' => 'Esconder a barra lateral', - 'Expand sidebar' => 'Expandir a barra lateral', 'This feature does not work with all browsers.' => 'Esta funcionalidade não é compatível com todos os navegadores.', 'There is no destination project available.' => 'Não há nenhum projeto de destino disponível.', 'Trigger automatically subtask time tracking' => 'Ativar automaticamente o monitoramento do tempo para as subtarefas', @@ -901,7 +840,6 @@ return array( 'open file' => 'abrir um arquivo', 'End date' => 'Data de término', 'Users overview' => 'Visão geral dos usuários', - 'Managers' => 'Gerentes', 'Members' => 'Membros', 'Shared project' => 'Projeto compartilhado', 'Project managers' => 'Gerentes de projeto', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Data de término:', 'There is no start date or end date for this project.' => 'Não há data de início ou data de término para este projeto.', 'Projects Gantt chart' => 'Gráfico de Gantt dos projetos', - 'Start date: %s' => 'Data de início: %s', - 'End date: %s' => 'Data de término: %s', 'Link type' => 'Tipo de link', 'Change task color when using a specific task link' => 'Mudar a cor da tarefa quando um link específico é utilizado', 'Task link creation or modification' => 'Criação ou modificação de um link em uma tarefa', - 'Login with my Gitlab Account' => 'Login com a minha conta Gitlab', 'Milestone' => 'Milestone', - 'Gitlab Authentication' => 'Autenticação Gitlab', - 'Help on Gitlab authentication' => 'Ajuda com a autenticação Gitlab', - 'Gitlab Id' => 'Gitlab Id', - 'Gitlab Account' => 'Conta Gitlab', - 'Link my Gitlab Account' => 'Vincular minha conta Gitlab', - 'Unlink my Gitlab Account' => 'Desvincular minha conta Gitlab', 'Documentation: %s' => 'Documentação: %s', 'Switch to the Gantt chart view' => 'Mudar para a vista gráfico de Gantt', 'Reset the search/filter box' => 'Reiniciar o campo de pesquisa', @@ -971,7 +900,7 @@ return array( 'Add a new filter' => 'Adicionar filtro', 'Share with all project members' => 'Compartilhar com todos os membros do projeto', 'Shared' => 'Compartilhado', - 'Owner' => 'Proprietário', + 'Owner' => 'Líder', 'Unread notifications' => 'Notificações não lidas', 'My filters' => 'Meus filtros', 'Notification methods:' => 'Métodos de notificação:', @@ -1010,112 +939,216 @@ return array( 'Duplicates are not imported' => 'Registros duplicados não são importados', 'Usernames must be lowercase and unique' => 'Nomes de usuário devem ser únicos e em letras minúsculas', 'Passwords will be encrypted if present' => 'Senhas serão encriptadas, se presentes', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', + '%s attached a new file to the task %s' => '%s anexou um novo arquivo a tarefa %s', + 'Assign automatically a category based on a link' => 'Atribuir automaticamente uma categoria baseada num link', + 'BAM - Konvertible Mark' => 'BAM - Mark conversível', + 'Assignee Username' => 'Usuário designado', + 'Assignee Name' => 'Nome do designado', + 'Groups' => 'Grupos', + 'Members of %s' => 'Membros do %s', + 'New group' => 'Novo grupo', + 'Group created successfully.' => 'Grupo criado com sucesso.', + 'Unable to create your group.' => 'Não foi possível de criar o seu grupo.', + 'Edit group' => 'Editar o grupo', + 'Group updated successfully.' => 'Grupo atualizado com sucesso.', + 'Unable to update your group.' => 'Não foi possível atualizar o seu grupo.', + 'Add group member to "%s"' => 'Adicionar um membro ao grupo "%s"', + 'Group member added successfully.' => 'Membro adicionado com sucesso ao o grupo.', + 'Unable to add group member.' => 'Não foi possivel adicionar esse membro ao grupo.', + 'Remove user from group "%s"' => 'Remover usuário do grupo "%s"', + 'User removed successfully from this group.' => 'Usuário removido com sucesso deste grupo.', + 'Unable to remove this user from the group.' => 'Não foi possivel remover este Usuário do grupo.', + 'Remove group' => 'Remover o grupo', + 'Group removed successfully.' => 'Grupo removido com sucesso.', + 'Unable to remove this group.' => 'Não foi possível remover este grupo.', + 'Project Permissions' => 'Permissões do projeto', + 'Manager' => 'Gerente', + 'Project Manager' => 'Gerente de projeto', + 'Project Member' => 'Membro de projeto', // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', + 'Your account is locked for %d minutes' => 'A sua conta está bloqueada por %d minutos', + 'Invalid captcha' => 'Captcha inválido', + 'The name must be unique' => 'O nome deve ser único', + 'View all groups' => 'Ver todos os grupos', + 'View group members' => 'Ver os membros do grupo', + 'There is no user available.' => 'Não há nenhum usuário disponível', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Você realmente deseja remover o usuário "%s" do grupo "%s"?', + 'There is no group.' => 'Não há nenhum grupo.', + 'External Id' => 'Id externo', + 'Add group member' => 'Adicionar um membro ao grupo', + 'Do you really want to remove this group: "%s"?' => 'Você realmente deseja excluir este grupo: "%s"?', + 'There is no user in this group.' => 'Não há usuários neste grupo.', + 'Remove this user' => 'Remover este usuário', + 'Permissions' => 'Permissões', + 'Allowed Users' => 'Usuários autorizados', + 'No user have been allowed specifically.' => 'Nenhum usuário foi especificamente autorizado.', + 'Role' => 'Função', + 'Enter user name...' => 'Digite o nome do usuário...', + 'Allowed Groups' => 'Grupos autorizados', + 'No group have been allowed specifically.' => 'Nenhum grupo foi especificamente autorizado.', + 'Group' => 'Grupo', + 'Group Name' => 'Nome do grupo', + 'Enter group name...' => 'Digite o nome do grupo', + 'Role:' => 'Função:', 'Project members' => 'Membros de projeto', - // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', - // 'Time-based One-time Password Algorithm' => '', - // 'Two-Factor Provider: ' => '', - // 'Disable two-factor authentication' => '', - // 'Enable two-factor authentication' => '', - // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', - // 'The password has never been reinitialized.' => '', - // 'Creation' => '', - // 'Expiration' => '', - // 'Password reset history' => '', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + 'Compare hours for "%s"' => 'Comparar as horas para "%s"', + '%s mentioned you in the task #%d' => '%s mencionou você na tarefa #%d', + '%s mentioned you in a comment on the task #%d' => '%s mencionou você num comentário da tarefa #%d', + 'You were mentioned in the task #%d' => 'Você foi mencionado na tarefa #%d', + 'You were mentioned in a comment on the task #%d' => 'Você foi mencionado num comentário da tarefa #%d', + 'Mentioned' => 'Mencionado', + 'Compare Estimated Time vs Actual Time' => 'Comparar o tempo estimado e o tempo atual', + 'Estimated hours: ' => 'Horas estimadas: ', + 'Actual hours: ' => 'Horas reais: ', + 'Hours Spent' => 'Horas gastas', + 'Hours Estimated' => 'Horas estimadas', + 'Estimated Time' => 'Tempo estimado', + 'Actual Time' => 'Tempo real', + 'Estimated vs actual time' => 'Tempo estimado vs tempo real', + 'RUB - Russian Ruble' => 'RUB - Rublo russo', + 'Assign the task to the person who does the action when the column is changed' => 'Atribuir a tarefa a pessoa que faz a ação quando a coluna é alterada', + 'Close a task in a specific column' => 'Fechar uma tarefa em uma coluna específica', + 'Time-based One-time Password Algorithm' => 'Senha de uso único baseado no tempo', + 'Two-Factor Provider: ' => 'Provedor de autenticação a dois fatores: ', + 'Disable two-factor authentication' => 'Desativar a autenticação a dois fatores', + 'Enable two-factor authentication' => 'Ativar a autenticação a dois fatores', + 'There is no integration registered at the moment.' => 'Não há nenhuma integração registrada por enquanto.', + 'Password Reset for Kanboard' => 'Redefinir a senha para Kanboard', + 'Forgot password?' => 'Esqueceu a senha?', + 'Enable "Forget Password"' => 'Activar a funcionalidade "Esqueceu a senha"', + 'Password Reset' => 'Redefinir a senha', + 'New password' => 'Nova senha', + 'Change Password' => 'Alterar a senha', + 'To reset your password click on this link:' => 'Para redefinir a sua senha, clique neste link:', + 'Last Password Reset' => 'Última redefinição de senha', + 'The password has never been reinitialized.' => 'A senha nunca foi redifinida.', + 'Creation' => 'Criação', + 'Expiration' => 'Expiração', + 'Password reset history' => 'Histórico da redefinição da senha', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas as tarefas da coluna "%s" e da Swimlane "%s" foram fechadas com sucesso.', + 'Do you really want to close all tasks of this column?' => 'Você realmente deseja fechar todas as tarefas desta coluna?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na Swimlane "%s" serão fechadas.', + 'Close all tasks of this column' => 'Fechar todas as tarefas desta coluna', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Nenhuma extensão tem registado um método de notificação de projecto. Você ainda pode definir notificações individuais em seu perfil de usuário.', + 'My dashboard' => 'Meu Painel', + 'My profile' => 'Meu perfil', + 'Project owner: ' => 'Líder do projeto: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'O identificador do projeto é opcional e deve ser alfanumérico, por exemplo MEUPROJETO.', + 'Project owner' => 'Líder do projeto', + 'Those dates are useful for the project Gantt chart.' => 'Estas datas são úteis para o gráfico de Gantt dos projetos.', + 'Private projects do not have users and groups management.' => 'Projetos privados não têm gestão de usuários e grupos.', + 'There is no project member.' => 'Não há nenhum membro do projeto.', + 'Priority' => 'Prioridade', + 'Task priority' => 'Prioridade das tarefas', + 'General' => 'Geral', + 'Dates' => 'Datas', + 'Default priority' => 'Prioridade padrão', + 'Lowest priority' => 'Prioridade baixa', + 'Highest priority' => 'Prioridade alta', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Se você colocar zero para a prioridade alta e baixa, essa funcionalidade será desativada.', + 'Close a task when there is no activity' => 'Fechar uma tarefa sem atividade', + 'Duration in days' => 'Duração em dias', + 'Send email when there is no activity on a task' => 'Enviar um e-mail quando não há nenhuma atividade em uma tarefa', + 'List of external links' => 'Lista dos links externos', + 'Unable to fetch link information.' => 'Não foi possível obter informações sobre o link.', + 'Daily background job for tasks' => 'Tarefa agendada diariamente para as tarefas', + 'Auto' => 'Auto', + 'Related' => 'Ligado', + 'Attachment' => 'Anexo', + 'Title not found' => 'Título não encontrado', + 'Web Link' => 'Link web', + 'External links' => 'Links externos', + 'Add external link' => 'Adicionar um link externo', + 'Type' => 'Tipo', + 'Dependency' => 'Dependência', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 19c2ebf7..bdbea711 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Editar', 'remove' => 'remover', 'Remove' => 'Remover', - 'Update' => 'Actualizar', 'Yes' => 'Sim', 'No' => 'Não', 'cancel' => 'cancelar', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Acções', 'Inactive' => 'Inactivo', 'Active' => 'Activo', - 'Add this column' => 'Adicionar esta coluna', '%d tasks on the board' => '%d tarefas no quadro', '%d tasks in total' => '%d tarefas no total', 'Unable to update this board.' => 'Não foi possível actualizar este quadro.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Remover projecto', 'Edit the board for "%s"' => 'Editar o quadro para "%s"', 'All projects' => 'Todos os projectos', - 'Change columns' => 'Modificar colunas', 'Add a new column' => 'Adicionar uma nova coluna', 'Title' => 'Título', 'Nobody assigned' => 'Ninguém assignado', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Abrir uma tarefa', 'Do you really want to open this task: "%s"?' => 'Tem a certeza que quer abrir esta tarefa: "%s"?', 'Back to the board' => 'Voltar ao quadro', - 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', 'There is nobody assigned' => 'Não há ninguém assignado', 'Column on the board:' => 'Coluna no quadro:', - 'Status is open' => 'Estado é aberto', - 'Status is closed' => 'Estado é finalizado', 'Close this task' => 'Finalizar esta tarefa', 'Open this task' => 'Abrir esta tarefa', 'There is no description.' => 'Não há descrição.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Em andamento', 'Done' => 'Finalizado', 'Application version:' => 'Versão da aplicação:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M', - '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', - 'Date created' => 'Data de criação', - 'Date completed' => 'Data da finalização', 'Id' => 'Id', '%d closed tasks' => '%d tarefas finalizadas', 'No task for this project' => 'Não há tarefa para este projecto', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', 'Invalid date' => 'Data inválida', - 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d %B %Y', 'Automatic actions' => 'Acções automáticas', 'Your automatic action have been created successfully.' => 'A sua acção automática foi criada com sucesso.', 'Unable to create your automatic action.' => 'Não é possível criar a sua acção automática.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Não é possível remover esta acção.', 'Action removed successfully.' => 'Acção removida com sucesso.', 'Automatic actions for the project "%s"' => 'Acções automáticas para o projecto "%s"', - 'Defined actions' => 'Acções definidas', 'Add an action' => 'Adicionar Acção', 'Event name' => 'Nome do evento', 'Action name' => 'Nome da acção', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a acção correspondente.', 'Next step' => 'Próximo passo', 'Define action parameters' => 'Definir parêmetros da acção', - 'Save this action' => 'Guardar esta acção', 'Do you really want to remove this action: "%s"?' => 'Tem a certeza que quer remover esta acção: "%s"?', 'Remove an automatic action' => 'Remover uma acção automática', 'Assign the task to a specific user' => 'Designar a tarefa para um utilizador específico', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Designar uma cor para um utilizador específico', 'Column title' => 'Título da coluna', 'Position' => 'Posição', - 'Move Up' => 'Mover para cima', - 'Move Down' => 'Mover para baixo', 'Duplicate to another project' => 'Duplicar para outro projecto', 'Duplicate' => 'Duplicar', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Comentário removido com sucesso.', 'Unable to remove this comment.' => 'Não é possível remover este comentário.', 'Do you really want to remove this comment?' => 'Tem a certeza que quer remover este comentário?', - 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.', 'Current password for the user "%s"' => 'Senha atual para o utilizador "%s"', 'The current password is required' => 'A senha atual é obrigatória', 'Wrong password' => 'Senha incorreta', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Autenticação externa falhou', 'Your external account is linked to your profile successfully.' => 'A sua conta externa foi vinculada com sucesso ao seu perfil', 'Email' => 'E-mail', - 'Link my Google Account' => 'Vincular a minha Conta do Google', - 'Unlink my Google Account' => 'Desvincular a minha Conta do Google', - 'Login with my Google Account' => 'Entrar com a minha Conta do Google', - 'Project not found.' => 'Projecto não encontrado.', 'Task removed successfully.' => 'Tarefa removida com sucesso.', 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.', 'Remove a task' => 'Remover uma tarefa', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Tamanho máximo: ', 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', 'Display another project' => 'Mostrar outro projecto', - 'Login with my Github Account' => 'Entrar com a minha Conta do Github', - 'Link my Github Account' => 'Associar à minha Conta do Github', - 'Unlink my Github Account' => 'Desassociar a minha Conta do Github', 'Created by %s' => 'Criado por %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', 'Tasks Export' => 'Exportar Tarefas', 'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"', 'Start Date' => 'Data inicial', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projectos:', 'view the task on Kanboard' => 'ver a tarefa no Kanboard', 'Public access' => 'Acesso público', - 'User management' => 'Gestão de utilizadores', 'Active tasks' => 'Tarefas activas', 'Disable public access' => 'Desactivar o acesso público', 'Enable public access' => 'Activar o acesso público', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Alterar senha', 'Password modification' => 'Alteração de senha', 'External authentications' => 'Autenticação externa', - 'Google Account' => 'Conta do Google', - 'Github Account' => 'Conta do Github', 'Never connected.' => 'Nunca conectado.', - 'No account linked.' => 'Nenhuma conta associada.', - 'Account linked.' => 'Conta associada.', 'No external authentication enabled.' => 'Nenhuma autenticação externa activa.', 'Password modified successfully.' => 'Senha alterada com sucesso.', 'Unable to change the password.' => 'Não foi possível alterar a senha.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Alterar assignação com base num utilizador externo', 'Change the category based on an external label' => 'Alterar categoria com base num rótulo externo', 'Reference' => 'Referência', - 'Reference: %s' => 'Referência: %s', 'Label' => 'Rótulo', 'Database' => 'Base de dados', 'About' => 'Sobre', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por defeito)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desactivar este recurso, 10 segundos por defeito)', 'Application URL' => 'URL da Aplicação', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)', 'Token regenerated.' => 'Token ', 'Date format' => 'Formato da data', 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Este projecto é privado', 'Type here to create a new sub-task' => 'Escreva aqui para criar uma nova subtarefa', 'Add' => 'Adicionar', - 'Estimated time: %s hours' => 'Tempo estimado: %s horas', - 'Time spent: %s hours' => 'Tempo gasto: %s horas', - 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y', 'Start date' => 'Data de início', 'Time estimated' => 'Tempo estimado', 'There is nothing assigned to you.' => 'Não há nada assignado a si.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Tem a certeza que quer remover este swimlane: "%s"?', 'Inactive swimlanes' => 'Desactivar swimlanes', 'Remove a swimlane' => 'Remover um swimlane', - 'Rename' => 'Renomear', 'Show default swimlane' => 'Mostrar swimlane padrão', 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projecto "%s"', 'Swimlane not found.' => 'Swimlane não encontrado.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane atualizado com sucesso.', 'The default swimlane have been updated successfully.' => 'O swimlane padrão foi atualizado com sucesso.', - 'Unable to create your swimlane.' => 'Não foi possível criar o seu swimlane.', 'Unable to remove this swimlane.' => 'Não foi possível remover este swimlane.', 'Unable to update this swimlane.' => 'Não foi possível atualizar este swimlane.', 'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Esta tarefa', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%e %b', 'Expand tasks' => 'Expandir tarefas', 'Collapse tasks' => 'Contrair tarefas', 'Expand/collapse tasks' => 'Expandir/Contrair tarefas', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Atalhos de teclado', 'Open board switcher' => 'Abrir o comutador de painel', 'Application' => 'Aplicação', - 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M', 'Compact view' => 'Vista reduzida', 'Horizontal scrolling' => 'Deslocamento horizontal', 'Compact/wide view' => 'Alternar entre a vista compacta e ampliada', 'No results match:' => 'Nenhum resultado:', 'Currency' => 'Moeda', - 'Files' => 'Arquivos', - 'Images' => 'Imagens', 'Private project' => 'Projecto privado', 'AUD - Australian Dollar' => 'AUD - Dólar australiano', 'CAD - Canadian Dollar' => 'CAD - Dólar canadense', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Teste o seu dispositivo', 'Assign a color when the task is moved to a specific column' => 'Atribuir uma cor quando a tarefa é movida em uma coluna específica', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'carregado por: %s', - 'uploaded on: %s' => 'carregado em: %s', - 'size: %s' => 'tamanho: %s', 'Burndown chart for "%s"' => 'Gráfico de Burndown para "%s"', 'Burndown chart' => 'Gráfico de Burndown', 'This chart show the task complexity over the time (Work Remaining).' => 'Este gráfico mostra a complexidade da tarefa ao longo do tempo (Trabalho Restante).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Uma tarefa não pode ser ligada a si própria', 'The exact same link already exists' => 'Um link idêntico jà existe', 'Recurrent task is scheduled to be generated' => 'A tarefa recorrente está programada para ser criada', - 'Recurring information' => 'Informação sobre a recorrência', 'Score' => 'Complexidade', 'The identifier must be unique' => 'O identificador deve ser único', 'This linked task id doesn\'t exists' => 'O identificador da tarefa associada não existe', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'O tempo despendido foi mudado: %sh', 'Time estimated changed: %sh' => 'O tempo estimado foi mudado/ %sh', 'The field "%s" have been updated' => 'O campo "%s" foi actualizada', - 'The description have been modified' => 'A descrição foi modificada', + 'The description has been modified' => 'A descrição foi modificada', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Tem a certeza que quer fechar a tarefa "%s" e todas as suas sub-tarefas?', - 'Swimlane: %s' => 'Swimlane: %s', 'I want to receive notifications for:' => 'Eu quero receber as notificações para:', 'All tasks' => 'Todas as tarefas', 'Only for tasks assigned to me' => 'Somente as tarefas atribuídas a mim', 'Only for tasks created by me' => 'Apenas as tarefas que eu criei', 'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Nova data de vencimento: %d/%m/%Y', - 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y', - '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', 'Total for all columns' => 'Total para todas as colunas', 'You need at least 2 days of data to show the chart.' => 'Precisa de pelo menos 2 dias de dados para visualizar o gráfico.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Não assignada', 'View advanced search syntax' => 'Ver sintaxe avançada de pesquisa', 'Overview' => 'Visão global', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Vista Painel/Calendário/Lista', 'Switch to the board view' => 'Mudar para o modo Painel', 'Switch to the calendar view' => 'Mudar para o modo Calendário', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Este gráfico mostra o tempo médio de espera e ciclo para as últimas %d tarefas realizadas.', 'Average time into each column' => 'Tempo médio em cada coluna', 'Lead and cycle time' => 'Tempo de Espera e Ciclo', - 'Google Authentication' => 'Autenticação Google', - 'Help on Google authentication' => 'Ajuda com autenticação Google', - 'Github Authentication' => 'Autenticação Github', - 'Help on Github authentication' => 'Ajuda com autenticação Github', 'Lead time: ' => 'Tempo de Espera: ', 'Cycle time: ' => 'Tempo de Ciclo: ', 'Time spent into each column' => 'Tempo gasto em cada coluna', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Se a tarefa não estiver fechada o hora actual será usada em vez da hora de conclusão', 'Set automatically the start date' => 'Definir data de inicio automáticamente', 'Edit Authentication' => 'Editar Autenticação', - 'Google Id' => 'Id Google', - 'Github Id' => 'Id Github', 'Remote user' => 'Utilizador remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Utilizadores remotos não guardam a password na base de dados do Kanboard, por exemplo: LDAP, contas do Google e Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se activar a opção "Desactivar login", as credenciais digitadas no login serão ignoradas.', 'New remote user' => 'Novo utilizador remoto', 'New local user' => 'Novo utilizador local', 'Default task color' => 'Cor de tarefa por defeito', - 'Hide sidebar' => 'Esconder barra lateral', - 'Expand sidebar' => 'Expandir barra lateral', 'This feature does not work with all browsers.' => 'Esta funcionalidade não funciona em todos os browsers', 'There is no destination project available.' => 'Não há projecto de destino disponivel', 'Trigger automatically subtask time tracking' => 'Activar automáticamente subtarefa de controlo de tempo', @@ -901,7 +840,6 @@ return array( 'open file' => 'abrir ficheiro', 'End date' => 'Data de fim', 'Users overview' => 'Visão geral de Utilizadores', - 'Managers' => 'Gestores', 'Members' => 'Membros', 'Shared project' => 'Projecto partilhado', 'Project managers' => 'Gestores do projecto', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Data de fim:', 'There is no start date or end date for this project.' => 'Não existe data de inicio ou fim para este projecto.', 'Projects Gantt chart' => 'Gráfico de Gantt dos projectos', - 'Start date: %s' => 'Data de inicio: %s', - 'End date: %s' => 'Data de fim: %s', 'Link type' => 'Tipo de ligação', 'Change task color when using a specific task link' => 'Alterar cor da tarefa quando se usar um tipo especifico de ligação de tarefa', 'Task link creation or modification' => 'Criação ou modificação de ligação de tarefa', - 'Login with my Gitlab Account' => 'Login com a minha Conta Gitlab', 'Milestone' => 'Objectivo', - 'Gitlab Authentication' => 'Autenticação Gitlab', - 'Help on Gitlab authentication' => 'Ajuda com autenticação Gitlab', - 'Gitlab Id' => 'Id Gitlab', - 'Gitlab Account' => 'Conta Gitlab', - 'Link my Gitlab Account' => 'Connectar a minha Conta Gitlab', - 'Unlink my Gitlab Account' => 'Desconectar a minha Conta Gitlab', 'Documentation: %s' => 'Documentação: %s', 'Switch to the Gantt chart view' => 'Mudar para vista de gráfico de Gantt', 'Reset the search/filter box' => 'Repor caixa de procura/filtro', @@ -1100,22 +1029,126 @@ return array( 'Do you really want to close all tasks of this column?' => 'Tem a certeza que quer fechar todas as tarefas nesta coluna?', '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na swimlane "%s" serão fechadas.', 'Close all tasks of this column' => 'Fechar todas as tarefas nesta coluna', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Nenhum plugin tem registado um método de notificação do projecto. Pode continuar a configurar notificações individuais no seu perfil de utilizador.', + 'My dashboard' => 'Meu painel', + 'My profile' => 'Meu perfil', + 'Project owner: ' => 'Dono do projecto: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'O identificador do projecto é opcional e tem de ser alfa-numerico, exemplo: MEUPROJECTO.', + 'Project owner' => 'Dono do projecto', + 'Those dates are useful for the project Gantt chart.' => 'Estas datas são uteis para o gráfico de Grantt do projecto.', + 'Private projects do not have users and groups management.' => 'Projectos privados não têm gestão de utilizadores nem de grupos.', + 'There is no project member.' => 'Não existe membro do projecto.', + 'Priority' => 'Prioridade', + 'Task priority' => 'Prioridade da Tarefa', + 'General' => 'Geral', + 'Dates' => 'Datas', + 'Default priority' => 'Prioridade por defeito', + 'Lowest priority' => 'Prioridade mais baixa', + 'Highest priority' => 'Prioridade mais alta', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Se colocar zero na prioridade baixa ou alta, essa funcionalidade será desactivada.', + 'Close a task when there is no activity' => 'Fechar tarefa quando não há actividade', + 'Duration in days' => 'Duração em dias', + 'Send email when there is no activity on a task' => 'Enviar email quando não há actividade numa tarefa', + 'List of external links' => 'Lista de ligações externas', + 'Unable to fetch link information.' => 'Impossivel obter informação da ligação.', + 'Daily background job for tasks' => 'Trabalho diário em segundo plano para tarefas', + 'Auto' => 'Auto', + 'Related' => 'Relacionado', + 'Attachment' => 'Anexo', + 'Title not found' => 'Titulo não encontrado', + 'Web Link' => 'Ligação Web', + 'External links' => 'Ligações externas', + 'Add external link' => 'Adicionar ligação externa', + 'Type' => 'Tipo', + 'Dependency' => 'Dependencia', + 'Add internal link' => 'Adicionar ligação interna', + 'Add a new external link' => 'Adicionar nova ligação externa', + 'Edit external link' => 'Editar ligação externa', + 'External link' => 'Ligação externa', + 'Copy and paste your link here...' => 'Copie e cole a sua ligação aqui...', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'De momento não existe nenhuma ligação externa.', + 'Internal links' => 'Ligações internas', + 'There is no internal link for the moment.' => 'De momento não existe nenhuma ligação interna.', + 'Assign to me' => 'Assignar a mim', + 'Me' => 'Eu', + 'Do not duplicate anything' => 'Não duplicar nada', + 'Projects management' => 'Gestão de projectos', + 'Users management' => 'Gestão de utilizadores', + 'Groups management' => 'Gestão de grupos', + 'Create from another project' => 'Criar apartir de outro projecto', + 'There is no subtask at the moment.' => 'De momento não existe sub-tarefa.', + 'open' => 'aberto', + 'closed' => 'fechado', + 'Priority:' => 'Prioridade:', + 'Reference:' => 'Referência:', + 'Complexity:' => 'Complexidade:', + 'Swimlane:' => 'Swimlane:', + 'Column:' => 'Coluna:', + 'Position:' => 'Posição:', + 'Creator:' => 'Criador:', + 'Time estimated:' => 'Tempo estimado:', + '%s hours' => '%s horas', + 'Time spent:' => 'Tempo gasto:', + 'Created:' => 'Criado:', + 'Modified:' => 'Modificado:', + 'Completed:' => 'Completado:', + 'Started:' => 'Iniciado:', + 'Moved:' => 'Movido:', + 'Task #%d' => 'Tarefa #%d', + 'Sub-tasks' => 'Sub-tarefa', + 'Date and time format' => 'Formato tempo e data', + 'Time format' => 'Formato tempo', + 'Start date: ' => 'Data inicio: ', + 'End date: ' => 'Data final: ', + 'New due date: ' => 'Nova data estimada: ', + 'Start date changed: ' => 'Data inicio alterada: ', + 'Disable private projects' => 'Desactivar projectos privados', + 'Do you really want to remove this custom filter: "%s"?' => 'Tem a certeza que quer remover este filtro personalizado: "%s"?', + 'Remove a custom filter' => 'Remover o filtro personalizado', + 'User activated successfully.' => 'Utilizador activado com sucesso.', + 'Unable to enable this user.' => 'Não foi possivel activar este utilizador.', + 'User disabled successfully.' => 'Utilizador desactivado com sucesso.', + 'Unable to disable this user.' => 'Não foi possivel desactivar este utilizador.', + 'All files have been uploaded successfully.' => 'Todos os ficheiros foram enviados com sucesso.', + 'View uploaded files' => 'Ver ficheiros enviados', + 'The maximum allowed file size is %sB.' => 'O tamanho máximo permitido é %sB.', + 'Choose files again' => 'Escolher ficheiros novamente', + 'Drag and drop your files here' => 'Arraste e deixe os ficheiros para aqui', + 'choose files' => 'escolher ficheiros', + 'View profile' => 'Ver perfil', + 'Two Factor' => 'Dois Factores', + 'Disable user' => 'Desactivar utilizador', + 'Do you really want to disable this user: "%s"?' => 'Tem a certeza que quer desactivar este utilizador: "%s"?', + 'Enable user' => 'Activar utilizador', + 'Do you really want to enable this user: "%s"?' => 'Tem a certeza que quer activar este utilizador: "%s"?', + 'Download' => 'Transferir', + 'Uploaded: %s' => 'Enviado: %s', + 'Size: %s' => 'Tamanho: %s', + 'Uploaded by %s' => 'Enviado por %s', + 'Filename' => 'Nome do ficheiro', + 'Size' => 'Tamanho', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index c52e622d..780d54b3 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Изменить', 'remove' => 'удалить', 'Remove' => 'Удалить', - 'Update' => 'Обновить', 'Yes' => 'Да', 'No' => 'Нет', 'cancel' => 'Отменить', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Действия', 'Inactive' => 'Неактивен', 'Active' => 'Активен', - 'Add this column' => 'Добавить колонку', '%d tasks on the board' => '%d задач на доске', '%d tasks in total' => 'всего %d задач', 'Unable to update this board.' => 'Не удалось обновить эту доску.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Удалить проект', 'Edit the board for "%s"' => 'Изменить доску для "%s"', 'All projects' => 'Все проекты', - 'Change columns' => 'Изменить колонки', 'Add a new column' => 'Добавить новую колонку', 'Title' => 'Название', 'Nobody assigned' => 'Никто не назначен', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Открыть задачу', 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу: "%s" ?', 'Back to the board' => 'Вернуться на доску', - 'Created on %B %e, %Y at %k:%M %p' => 'Создано %B /%e /%Y в %k:%M %p', 'There is nobody assigned' => 'Никто не назначен', 'Column on the board:' => 'Колонка на доске: ', - 'Status is open' => 'Статус - открыт', - 'Status is closed' => 'Статус - закрыт', 'Close this task' => 'Закрыть задачу', 'Open this task' => 'Открыть задачу', 'There is no description.' => 'Нет описания.', @@ -120,7 +114,7 @@ return array( 'The user id is required' => 'Необходим ID пользователя', 'Passwords don\'t match' => 'Пароли не совпадают', 'The confirmation is required' => 'Необходимо подтверждение', - 'The project is required' => 'Необъодимо указать проект', + 'The project is required' => 'Необходимо указать проект', 'The id is required' => 'Необходим ID', 'The project id is required' => 'Необходим ID проекта', 'The project name is required' => 'Необходимо имя проекта', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'В процессе', 'Done' => 'Выполнено', 'Application version:' => 'Версия приложения:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M', - 'Date created' => 'Дата создания', - 'Date completed' => 'Дата завершения', 'Id' => 'ID', '%d closed tasks' => '%d завершенных задач', 'No task for this project' => 'Нет задач для этого проекта', @@ -177,7 +167,7 @@ return array( 'User' => 'Пользователь', 'Comments' => 'Комментарии', 'Write your text in Markdown' => 'Справка по синтаксису Markdown', - 'Leave a comment' => 'Оставить комментарий 2', + 'Leave a comment' => 'Оставить комментарий', 'Comment is required' => 'Нужен комментарий', 'Leave a description' => 'Напишите описание', 'Comment added successfully.' => 'Комментарий успешно добавлен.', @@ -185,17 +175,13 @@ return array( 'Edit this task' => 'Изменить задачу', 'Due Date' => 'Сделать до', 'Invalid date' => 'Неверная дата', - 'Must be done before %B %e, %Y' => 'Должно быть сделано до %B %e %Y', - '%B %e, %Y' => '%B, %e, %Y', - '%b %e, %Y' => '%b %e, %Y', 'Automatic actions' => 'Автоматические действия', - 'Your automatic action have been created successfully.' => 'Автоматика успешно настроена.', + 'Your automatic action have been created successfully.' => 'Автоматизированное действие успешно настроено.', 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', 'Remove an action' => 'Удалить действие', 'Unable to remove this action.' => 'Не удалось удалить действие', 'Action removed successfully.' => 'Действие удалено.', 'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %s »', - 'Defined actions' => 'Заданные действия', 'Add an action' => 'Добавить действие', 'Event name' => 'Имя события', 'Action name' => 'Имя действия', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Когда случится ВЫБРАННОЕ событие выполняется СООТВЕТСТВУЮЩЕЕ действие.', 'Next step' => 'Следующий шаг', 'Define action parameters' => 'Задать параметры действия', - 'Save this action' => 'Сохранить это действие', 'Do you really want to remove this action: "%s"?' => 'Вы точно хотите удалить это действие: « %s » ?', 'Remove an automatic action' => 'Удалить автоматическое действие', 'Assign the task to a specific user' => 'Назначить задачу определенному пользователю', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю', 'Column title' => 'Название колонки', 'Position' => 'Расположение', - 'Move Up' => 'Сдвинуть вверх', - 'Move Down' => 'Сдвинуть вниз', 'Duplicate to another project' => 'Клонировать в другой проект', 'Duplicate' => 'Клонировать', 'link' => 'ссылка', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Комментарий удален.', 'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.', 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий?', - 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор и автор комментария имеют доступ к этой странице.', 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »', 'The current password is required' => 'Требуется текущий пароль', 'Wrong password' => 'Неверный пароль', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Внешняя авторизация не удалась', 'Your external account is linked to your profile successfully.' => 'Ваш внешний аккаунт успешно подключен к профилю.', 'Email' => 'E-mail', - 'Link my Google Account' => 'Привязать мой профиль к Google', - 'Unlink my Google Account' => 'Отвязать мой профиль от Google', - 'Login with my Google Account' => 'Аутентификация через Google', - 'Project not found.' => 'Проект не найден.', 'Task removed successfully.' => 'Задача удалена.', 'Unable to remove this task.' => 'Не удалось удалить эту задачу.', 'Remove a task' => 'Удалить задачу', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Максимальный размер: ', 'Unable to upload the file.' => 'Не удалось загрузить файл.', 'Display another project' => 'Показать другой проект', - 'Login with my Github Account' => 'Аутентификация через Github', - 'Link my Github Account' => 'Привязать мой профиль к Github', - 'Unlink my Github Account' => 'Отвязать мой профиль от Github', 'Created by %s' => 'Создано %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M', 'Tasks Export' => 'Экспорт задач', 'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »', 'Start Date' => 'Дата начала', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам:', 'view the task on Kanboard' => 'посмотреть задачу на Kanboard', 'Public access' => 'Общий доступ', - 'User management' => 'Управление пользователями', 'Active tasks' => 'Активные задачи', 'Disable public access' => 'Отключить общий доступ', 'Enable public access' => 'Включить общий доступ', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Сменить пароль', 'Password modification' => 'Изменение пароля', 'External authentications' => 'Внешняя аутентификация', - 'Google Account' => 'Профиль Google', - 'Github Account' => 'Профиль Github', 'Never connected.' => 'Ранее не соединялось.', - 'No account linked.' => 'Нет связанных профилей.', - 'Account linked.' => 'Профиль связан.', 'No external authentication enabled.' => 'Нет активной внешней аутентификации.', 'Password modified successfully.' => 'Пароль изменен.', 'Unable to change the password.' => 'Не удалось сменить пароль.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', 'Reference' => 'Ссылка', - 'Reference: %s' => 'Ссылка: %s', 'Label' => 'Ярлык', 'Database' => 'База данных', 'About' => 'Информация', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Частота в секундах (60 секунд по умолчанию)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Частота в секундах (0 для выключения, 10 секунд по умолчанию)', 'Application URL' => 'URL приложения', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Пример: http://example.kanboard.net (используется в email уведомлениях)', 'Token regenerated.' => 'Токен пересоздан', 'Date format' => 'Формат даты', 'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Это проект с ограниченным доступом', 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу', 'Add' => 'Добавить', - 'Estimated time: %s hours' => 'Запланировано: %s часов', - 'Time spent: %s hours' => 'Потрачено времени: %s часов', - 'Started on %B %e, %Y' => 'Начато %B %e, %Y', 'Start date' => 'Дата начала', 'Time estimated' => 'Запланировано', 'There is nothing assigned to you.' => 'Вам ничего не назначено', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Вы действительно хотите удалить дорожку "%s"?', 'Inactive swimlanes' => 'Неактивные дорожки', 'Remove a swimlane' => 'Удалить дорожку', - 'Rename' => 'Переименовать', 'Show default swimlane' => 'Показать стандартную дорожку', 'Swimlane modification for the project "%s"' => 'Редактирование дорожки для проекта "%s"', 'Swimlane not found.' => 'Дорожка не найдена.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Дорожки', 'Swimlane updated successfully.' => 'Дорожка успешно обновлена.', 'The default swimlane have been updated successfully.' => 'Стандартная swimlane был успешно обновлен.', - 'Unable to create your swimlane.' => 'Невозможно создать дорожку.', 'Unable to remove this swimlane.' => 'Невозможно удалить дорожку.', 'Unable to update this swimlane.' => 'Невозможно обновить дорожку.', 'Your swimlane have been created successfully.' => 'Ваша дорожка была успешно создан.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Эта задача', '<1h' => '<1ч', '%dh' => '%dh', - '%b %e' => '%b %e', 'Expand tasks' => 'Развернуть задачи', 'Collapse tasks' => 'Свернуть задачи', 'Expand/collapse tasks' => 'Развернуть/свернуть задачи', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Горячие клавиши', 'Open board switcher' => 'Открыть переключатель доски', 'Application' => 'Приложение', - 'since %B %e, %Y at %k:%M %p' => 'с %B %e, %Y - %k:%M %p', 'Compact view' => 'Компактный вид', 'Horizontal scrolling' => 'Широкий вид', 'Compact/wide view' => 'Компактный/широкий вид', 'No results match:' => 'Отсутствуют результаты:', 'Currency' => 'Валюта', - 'Files' => 'Файлы', - 'Images' => 'Изображения', 'Private project' => 'Приватный проект', 'AUD - Australian Dollar' => 'AUD - Австралийский доллар', 'CAD - Canadian Dollar' => 'CAD - Канадский доллар', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Проверьте свое устройство', 'Assign a color when the task is moved to a specific column' => 'Назначить цвет, когда задача перемещается в определенную колонку', '%s via Kanboard' => '%s через Канборд', - 'uploaded by: %s' => 'загружено: %s', - 'uploaded on: %s' => 'загружено в: %s', - 'size: %s' => 'размер: %s', 'Burndown chart for "%s"' => 'Диаграмма сгорания для « %s »', 'Burndown chart' => 'Диаграмма сгорания', 'This chart show the task complexity over the time (Work Remaining).' => 'Эта диаграмма показывают сложность задачи по времени (оставшейся работы).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Задача не может быть связана с собой же', 'The exact same link already exists' => 'Такая ссылка уже существует', 'Recurrent task is scheduled to be generated' => 'Периодическая задача запланирована к созданию', - 'Recurring information' => 'Информация о периодичности', 'Score' => 'Оценка', 'The identifier must be unique' => 'Идентификатор должен быть уникальным', 'This linked task id doesn\'t exists' => 'Этот ID звязанной задачи не существует', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Изменение количества затраченного времени: %sh', 'Time estimated changed: %sh' => 'Ожидаемый срок изменен: %sh', 'The field "%s" have been updated' => 'Поле "%s" ,было изменено', - 'The description have been modified' => 'Описание было изменено', + 'The description has been modified' => 'Описание было изменено', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Вы действительно хотите закрыть задачу "%s", а также все подзадачи?', - 'Swimlane: %s' => 'Дорожка: %s', 'I want to receive notifications for:' => 'Я хочу получать уведомления для:', 'All tasks' => 'Все задачи', 'Only for tasks assigned to me' => 'Только для задач, назначенных на меня', 'Only for tasks created by me' => 'Только для задач, созданных мной', 'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мной', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', - 'New due date: %B %e, %Y' => 'Новая дата завершения: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Изменить дату начала: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%Y-%%m-%%d' => '%%Y-%%m-%%d', 'Total for all columns' => 'Суммарно для всех колонок', 'You need at least 2 days of data to show the chart.' => 'Для отображения диаграммы нужно по крайней мере 2 дня.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Не назначенные', 'View advanced search syntax' => 'Просмотр расширенного синтаксиса поиска', 'Overview' => 'Обзор', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Просмотр Доска/Календарь/Список', 'Switch to the board view' => 'Переключиться в режим доски', 'Switch to the calendar view' => 'Переключиться в режим календаря', @@ -839,17 +786,13 @@ return array( 'Average time spent' => 'Затрачено времени в среднем', 'This chart show the average time spent into each column for the last %d tasks.' => 'Эта диаграмма показывает среднее время, проведенное задачами в каждой колонке за последний %d.', 'Average Lead and Cycle time' => 'Среднее время выполнения и цикла', - 'Average lead time: ' => 'Среднее время выполнения', - 'Average cycle time: ' => 'Среднее время цикла', + 'Average lead time: ' => 'Среднее время выполнения: ', + 'Average cycle time: ' => 'Среднее время цикла: ', 'Cycle Time' => 'Время цикла', 'Lead Time' => 'Время выполнения', 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Эта диаграма показывает среднее время выполнения и цикла задачь в последние %d.', 'Average time into each column' => 'Среднее время в каждом столбце', 'Lead and cycle time' => 'Время выполнения и цикла', - 'Google Authentication' => 'Авторизация Google', - 'Help on Google authentication' => 'Помощь в авторизации Google', - 'Github Authentication' => 'Авторизация Github', - 'Help on Github authentication' => 'Помощь в авторизации Github', 'Lead time: ' => 'Время выполнения:', 'Cycle time: ' => 'Время цикла:', 'Time spent into each column' => 'Время, проведенное в каждой колонке', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Если задача не закрыта, то текущая дата будет указана в дате завершения задачи.', 'Set automatically the start date' => 'Установить автоматическую дату начала', 'Edit Authentication' => 'Редактировать авторизацию', - 'Google Id' => 'Google Id', - 'Github Id' => 'Github Id', 'Remote user' => 'Удаленный пользователь', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Учетные данные для входа через LDAP, Google и Github не будут сохранены в Kanboard.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Если вы установите флажок "Запретить форму входа", учетные данные, введенные в форму входа будет игнорироваться.', 'New remote user' => 'Новый удаленный пользователь', 'New local user' => 'Новый локальный пользователь', 'Default task color' => 'Стандартные цвета задач', - 'Hide sidebar' => 'Свернуть сайдбар', - 'Expand sidebar' => 'Показать сайдбар', 'This feature does not work with all browsers.' => 'Эта функция доступна не во всех браузерах.', 'There is no destination project available.' => 'Нет доступного для назначения проекта.', 'Trigger automatically subtask time tracking' => 'Триггер автоматического отслеживания времени подзадач', @@ -876,14 +815,14 @@ return array( 'Current column: %s' => 'Текущая колонка: %s', 'Current category: %s' => 'Текущая категория: %s', 'no category' => 'без категории', - 'Current assignee: %s' => 'Current assignee: %s', + 'Current assignee: %s' => 'Текущее назначенное лицо: %s', 'not assigned' => 'не назначен', 'Author:' => 'Автор:', 'contributors' => 'соавторы', 'License:' => 'Лицензия:', 'License' => 'Лицензия', 'Enter the text below' => 'Введите текст ниже', - 'Gantt chart for %s' => 'Диаграмма Гантта для %s', + 'Gantt chart for %s' => 'Диаграмма Ганта для %s', 'Sort by position' => 'Сортировать по позиции', 'Sort by date' => 'Сортировать по дате', 'Add task' => 'Добавить задачу', @@ -892,7 +831,7 @@ return array( 'There is no start date or due date for this task.' => 'Для этой задачи нет даты начала или завершения.', 'Moving or resizing a task will change the start and due date of the task.' => 'Изменение или перемещение задачи повлечет изменение даты начала завершения задачи.', 'There is no task in your project.' => 'В Вашем проекте задач нет.', - 'Gantt chart' => 'Диаграмма Гантта', + 'Gantt chart' => 'Диаграмма Ганта', 'People who are project managers' => 'Люди, которые менеджеры проекта', 'People who are project members' => 'Люди, которые участники проекта', 'NOK - Norwegian Krone' => 'НК - Норвежская крона', @@ -901,36 +840,26 @@ return array( 'open file' => 'открыть файл', 'End date' => 'Дата завершения', 'Users overview' => 'Обзор пользователей', - 'Managers' => 'Менеджеры', 'Members' => 'Участники', 'Shared project' => 'Общие/публичные проекты', 'Project managers' => 'Менеджер проекта', - 'Gantt chart for all projects' => 'Диаграмма Гантта для всех проектов', + 'Gantt chart for all projects' => 'Диаграмма Ганта для всех проектов', 'Projects list' => 'Список проектов', - 'Gantt chart for this project' => 'Диаграмма Гантта для этого проекта', + 'Gantt chart for this project' => 'Диаграмма Ганта для этого проекта', 'Project board' => 'Доска проекта', 'End date:' => 'Дата завершения:', 'There is no start date or end date for this project.' => 'В проекте не указаны дата начала или завершения.', - 'Projects Gantt chart' => 'Диаграмма Гантта проектов', - 'Start date: %s' => 'Дата начала: %s', - 'End date: %s' => 'Дата завершения: %s', + 'Projects Gantt chart' => 'Диаграмма Ганта проектов', 'Link type' => 'Тип ссылки', 'Change task color when using a specific task link' => 'Изменение цвета задач при использовании ссылки на определенные задачи', 'Task link creation or modification' => 'Ссылка на создание или модификацию задачи', - 'Login with my Gitlab Account' => 'Авторизоваться через аккаунт Gitlab', 'Milestone' => 'Веха', - 'Gitlab Authentication' => 'Авторизация через Gitlab', - 'Help on Gitlab authentication' => 'Помощь а авторизации через Gitlab', - 'Gitlab Id' => 'Gitlab Id', - 'Gitlab Account' => 'Аккаунт Gitlab', - 'Link my Gitlab Account' => 'Привязать аккаунт Gitlab', - 'Unlink my Gitlab Account' => 'Отвязать аккаунт Gitlab', 'Documentation: %s' => 'Документация: %s', - 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Гантта', + 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Ганта', 'Reset the search/filter box' => 'Сбросить поиск/фильтр', 'Documentation' => 'Документация', 'Table of contents' => 'Содержание', - 'Gantt' => 'Гантт', + 'Gantt' => 'Гант', 'Author' => 'Автор', 'Version' => 'Версия', 'Plugins' => 'Плагины', @@ -1040,8 +969,8 @@ return array( 'Your account is locked for %d minutes' => 'Ваш аккаунт заблокирован на %d минут', 'Invalid captcha' => 'Неверный код подтверждения', 'The name must be unique' => 'Имя должно быть уникальным', - 'View all groups' => 'Просмотр всех группы', - 'View group members' => 'Просмотр всех группы участников группы', + 'View all groups' => 'Просмотр всех групп', + 'View group members' => 'Просмотр участников группы', 'There is no user available.' => 'Нет доступных пользователей.', 'Do you really want to remove the user "%s" from the group "%s"?' => 'Вы действительно хотите удалить пользователя "%s" из группы "%s"?', 'There is no group.' => 'Нет созданных групп.', @@ -1079,7 +1008,7 @@ return array( 'RUB - Russian Ruble' => 'Руб - Российский рубль', 'Assign the task to the person who does the action when the column is changed' => 'Назначить задачу пользователю, который произвел изменение в колонке', 'Close a task in a specific column' => 'Закрыть задачу в выбранной колонке', - 'Time-based One-time Password Algorithm' => 'Времязависиммый, одноразовый алгоритм пароля', + 'Time-based One-time Password Algorithm' => 'Зависимый от времени, одноразовый алгоритм пароля', 'Two-Factor Provider: ' => 'Провайдер двух-факторной авторизации: ', 'Disable two-factor authentication' => 'Отключить двух-факторную авторизацию', 'Enable two-factor authentication' => 'Включить двух-факторную авторизацию', @@ -1115,7 +1044,111 @@ return array( 'Dates' => 'Даты', 'Default priority' => 'Приоритет по-умолчанию', 'Lowest priority' => 'Наименьший приоритет', - 'Highest priority' => 'Нивысший приоритет', + 'Highest priority' => 'Наивысший приоритет', 'If you put zero to the low and high priority, this feature will be disabled.' => 'Если Вы введете 0 для наименьшего и наивысшего приоритета, этот функционал будет отключен.', - 'Priority: %d' => 'Приоритет: %d', + 'Close a task when there is no activity' => 'Закрывать задачу, когда нет активности', + 'Duration in days' => 'Длительность в днях', + 'Send email when there is no activity on a task' => 'Отправлять email, когда активность по задаче отсутствует', + 'List of external links' => 'Список внешних ссылок', + 'Unable to fetch link information.' => 'Не удалось получить информацию о ссылке', + 'Daily background job for tasks' => 'Ежедневные фоновые работы для задач', + 'Auto' => 'Авто', + 'Related' => 'Связано', + 'Attachment' => 'Вложение', + 'Title not found' => 'Заголовок не найден', + 'Web Link' => 'Web-ссылка', + 'External links' => 'Внешние ссылки', + 'Add external link' => 'Добавить внешнюю ссылку', + 'Type' => 'Тип', + 'Dependency' => 'Зависимость', + 'Add internal link' => 'Добавить внутреннюю ссылку', + 'Add a new external link' => 'Добавить новую внешнюю ссылку', + 'Edit external link' => 'Изменить внешнюю ссылку', + 'External link' => 'Внешняя ссылка', + 'Copy and paste your link here...' => 'Скопируйте и вставьте вашу ссылку здесь', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'На данный момент внешние ссылки отсутствуют', + 'Internal links' => 'Внутренние ссылки', + 'There is no internal link for the moment.' => 'На данные момент внутреннии ссылки отсутствуют', + 'Assign to me' => 'Связать со мной', + 'Me' => 'Мне', + 'Do not duplicate anything' => 'Не дублировать ничего', + 'Projects management' => 'Управление проектами', + 'Users management' => 'Управление пользователями', + 'Groups management' => 'Управление группами', + 'Create from another project' => 'Создать из другого проекта', + 'There is no subtask at the moment.' => 'На данный момент подзадачи отсутствуют', + 'open' => 'открыто', + 'closed' => 'закрыто', + 'Priority:' => 'Приоритет:', + 'Reference:' => 'Ссылка:', + 'Complexity:' => 'Сложность:', + 'Swimlane:' => 'Дорожка:', + 'Column:' => 'Столбец:', + 'Position:' => 'Позиция:', + 'Creator:' => 'Создатель:', + 'Time estimated:' => 'Оценочное время:', + '%s hours' => '%s часов', + 'Time spent:' => 'Времени потрачено:', + 'Created:' => 'Создана:', + 'Modified:' => 'Изменена:', + 'Completed:' => 'Завершена:', + 'Started:' => 'Начата:', + 'Moved:' => 'Перемещена:', + 'Task #%d' => 'Задача #%d', + 'Sub-tasks' => 'Подзадачи', + 'Date and time format' => 'Формат даты и времени', + 'Time format' => 'Формат времени', + 'Start date: ' => 'Дата начала:', + 'End date: ' => 'Дата окончания:', + 'New due date: ' => 'Новая дата исполнения:', + 'Start date changed: ' => 'Дата начала изменена:', + 'Disable private projects' => 'Выключить приватные проекты', + 'Do you really want to remove this custom filter: "%s"?' => 'Вы точно ходите удалить этот пользовательский фильтр: "%s"?', + 'Remove a custom filter' => 'Удалить пользовательский фильтр', + 'User activated successfully.' => 'Пользователь успешно активирован.', + 'Unable to enable this user.' => 'Не удалось включить этого пользователя.', + 'User disabled successfully.' => 'Пользователь был успешно выключен.', + 'Unable to disable this user.' => 'Не удалось выключить пользователя.', + 'All files have been uploaded successfully.' => 'Все файлы были успешно загружены.', + 'View uploaded files' => 'Просмотр загруженных файлов', + 'The maximum allowed file size is %sB.' => 'Максимально допустимый размер файла: %sB.', + 'Choose files again' => 'Выбрать файлы повторно', + 'Drag and drop your files here' => 'Переместите ваши файлы сюда', + 'choose files' => 'выбор файлов', + 'View profile' => 'Просмотр профиля', + 'Two Factor' => 'Двухфакторный', + 'Disable user' => 'Выключить пользователя', + 'Do you really want to disable this user: "%s"?' => 'Вы точно хотите выключить этого пользователя: "%s"?', + 'Enable user' => 'Включить пользователя', + 'Do you really want to enable this user: "%s"?' => 'Вы точно хотите включить этого пользователя: "%s"?', + 'Download' => 'Загрузка', + 'Uploaded: %s' => 'Загружено: %s', + 'Size: %s' => 'Размер: %s', + 'Uploaded by %s' => 'Загружено пользователем: %s', + 'Filename' => 'Имя файла', + 'Size' => 'Размер', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 2b3553c2..45098222 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Izmeni', 'remove' => 'ukloni', 'Remove' => 'Ukloni', - 'Update' => 'Ažuriraj', 'Yes' => 'Da', 'No' => 'Ne', 'cancel' => 'odustani', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Akcje', 'Inactive' => 'Neaktivan', 'Active' => 'Aktivan', - 'Add this column' => 'Dodaj kolonu', '%d tasks on the board' => '%d zadataka na tabli', '%d tasks in total' => '%d zadataka ukupno', 'Unable to update this board.' => 'Nemogu da ažuriram ovu tablu.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Ukloni projekat', 'Edit the board for "%s"' => 'Izmeni tablu za "%s"', 'All projects' => 'Svi projekti', - 'Change columns' => 'Zameni kolonu', 'Add a new column' => 'Dodaj novu kolonu', 'Title' => 'Naslov', 'Nobody assigned' => 'Niko nije dodeljen', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Otvori zadatak', 'Do you really want to open this task: "%s"?' => 'Da li zaista želiš da otvoriš zadatak: "%s"?', 'Back to the board' => 'Nazad na tablu', - 'Created on %B %e, %Y at %k:%M %p' => 'Kreiran %e %B %Y o %k:%M', 'There is nobody assigned' => 'Niko nije dodeljen!', 'Column on the board:' => 'Kolona na tabli:', - 'Status is open' => 'Status otvoren', - 'Status is closed' => 'Status zatvoren', 'Close this task' => 'Zatvori ovaj zadatak', 'Open this task' => 'Otvori ovaj zadatak', 'There is no description.' => 'Bez opisa.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'U radu', 'Done' => 'Gotovo', 'Application version:' => 'Verzija aplikacije:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Završeno u %e %B %Y o %k:%M', - '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', - 'Date created' => 'Kreiran dana', - 'Date completed' => 'Završen dana', 'Id' => 'Id', '%d closed tasks' => '%d zatvorenih zadataka', 'No task for this project' => 'Nema dodeljenih zadataka ovom projektu', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Izmeni ovaj zadatak', 'Due Date' => 'Termin', 'Invalid date' => 'Loš datum', - 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', - '%B %e, %Y' => '%e %B %Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'Automatske akcije', 'Your automatic action have been created successfully.' => 'Uspešno kreirana automatska akcija', 'Unable to create your automatic action.' => 'Nemoguće kreiranje automatske akcije', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Nije moguće obrisati akciju', 'Action removed successfully.' => 'Akcija obrisana', 'Automatic actions for the project "%s"' => 'Akcje za automatizaciju projekta "%s"', - 'Defined actions' => 'Definisane akcje', 'Add an action' => 'dodaj akcju', 'Event name' => 'Naziv događaja', 'Action name' => 'Naziv akcije', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Kad se događaj desi izvrši odgovarajuću akciju', 'Next step' => 'Sledeći korak', 'Define action parameters' => 'Definiši parametre akcije', - 'Save this action' => 'Snimi akciju', 'Do you really want to remove this action: "%s"?' => 'Da li da obrišem akciju "%s"?', 'Remove an automatic action' => 'Obriši automatsku akciju', 'Assign the task to a specific user' => 'Dodeli zadatak određenom korisniku', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Dodeli boju korisniku', 'Column title' => 'Naslov kolone', 'Position' => 'Pozicija', - 'Move Up' => 'Podigni', - 'Move Down' => 'Spusti', 'Duplicate to another project' => 'Kopiraj u drugi projekat', 'Duplicate' => 'Napravi kopiju', 'link' => 'link', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Komentar je uspešno obrisan.', 'Unable to remove this comment.' => 'Neuspešno brisanje komentara.', 'Do you really want to remove this comment?' => 'Da li da obrišem ovaj komentar?', - 'Only administrators or the creator of the comment can access to this page.' => 'Samo administrator i kreator komentara mogu ga obrisati.', 'Current password for the user "%s"' => 'Trenutna lozinka za korisnika "%s"', 'The current password is required' => 'Trenutna lozinka je obavezna', 'Wrong password' => 'Pogrešna lozinka', @@ -260,10 +242,6 @@ return array( // 'External authentication failed' => '', // 'Your external account is linked to your profile successfully.' => '', 'Email' => 'E-mail', - 'Link my Google Account' => 'Poveži sa Google nalogom', - 'Unlink my Google Account' => 'Ukini vezu sa Google nalogom', - 'Login with my Google Account' => 'Prijavi se preko Google naloga', - 'Project not found.' => 'Projekat nije pronađen.', 'Task removed successfully.' => 'Zadatak uspešno uklonjen.', 'Unable to remove this task.' => 'Nemoguće uklanjanje zadatka.', 'Remove a task' => 'Ukloni zadatak', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimalna veličina: ', 'Unable to upload the file.' => 'Nije moguće snimiti fajl.', 'Display another project' => 'Prikaži drugi projekat', - 'Login with my Github Account' => 'Zaloguj przy użyciu konta Github', - 'Link my Github Account' => 'Podłącz konto Github', - 'Unlink my Github Account' => 'Odłącz konto Github', 'Created by %s' => 'Kreirao %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslednja izmena %e %B %Y o %k:%M', 'Tasks Export' => 'Izvoz zadataka', 'Tasks exportation for "%s"' => 'Izvoz zadataka za "%s"', 'Start Date' => 'Početni datum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Želim obaveštenja samo za ovaj projekat:', 'view the task on Kanboard' => 'Pregledaj zadatke', 'Public access' => 'Javni pristup', - 'User management' => 'Uređivanje korisnika', 'Active tasks' => 'Aktivni zadaci', 'Disable public access' => 'Zabrani javni pristup', 'Enable public access' => 'Dozvoli javni pristup', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Izmeni lozinku', 'Password modification' => 'Izmena lozinke', 'External authentications' => 'Spoljne akcije', - 'Google Account' => 'Google nalog', - 'Github Account' => 'Github nalog', 'Never connected.' => 'Bez konekcija.', - 'No account linked.' => 'Bez povezanih naloga.', - 'Account linked.' => 'Nalog povezan.', 'No external authentication enabled.' => 'Bez omogućenih spoljnih autentikacija.', 'Password modified successfully.' => 'Uspešna izmena lozinke.', 'Unable to change the password.' => 'Nije moguće izmeniti lozinku.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', // 'Reference' => '', - // 'Reference: %s' => '', 'Label' => 'Etikieta', 'Database' => 'Baza', 'About' => 'Informacje', @@ -459,7 +427,6 @@ return array( // 'Frequency in second (60 seconds by default)' => '', // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', 'Application URL' => 'Adres URL aplikacji', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Primer: http://example.kanboard.net/ (koristi se u obaveštenjima putem mail-a)', 'Token regenerated.' => 'Token wygenerowany ponownie.', 'Date format' => 'Format daty', 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Ovaj projekat je privatan', 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka', 'Add' => 'Dodaj', - 'Estimated time: %s hours' => 'Procenjeno vreme: %s godzin', - 'Time spent: %s hours' => 'Utrošeno vreme: %s godzin', - 'Started on %B %e, %Y' => 'Započeto dana %e %B %Y', 'Start date' => 'Datum početka', 'Time estimated' => 'Procenjeno vreme', 'There is nothing assigned to you.' => 'Ništa vam nije dodeljeno', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Da li da uklonim razdelnik: "%s"?', 'Inactive swimlanes' => 'Neaktivni razdelniki', 'Remove a swimlane' => 'Ukloni razdelnik', - 'Rename' => 'Preimenuj', 'Show default swimlane' => 'Prikaži osnovni razdelnik', 'Swimlane modification for the project "%s"' => 'Izmena razdelnika za projekat "%s"', 'Swimlane not found.' => 'Razdelnik nije pronađen.', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Razdelnici', 'Swimlane updated successfully.' => 'Razdelnik zaktualizowany pomyślnie.', // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', // 'Unable to remove this swimlane.' => '', // 'Unable to update this swimlane.' => '', 'Your swimlane have been created successfully.' => 'Razdelnik je uspešno kreiran.', @@ -612,7 +574,6 @@ return array( // 'This task' => '', // '<1h' => '', // '%dh' => '', - // '%b %e' => '', // 'Expand tasks' => '', // 'Collapse tasks' => '', // 'Expand/collapse tasks' => '', @@ -622,14 +583,11 @@ return array( // 'Keyboard shortcuts' => '', // 'Open board switcher' => '', // 'Application' => '', - // 'since %B %e, %Y at %k:%M %p' => '', // 'Compact view' => '', // 'Horizontal scrolling' => '', // 'Compact/wide view' => '', // 'No results match:' => '', // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', // 'Private project' => '', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( // 'Test your device' => '', // 'Assign a color when the task is moved to a specific column' => '', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -694,7 +649,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -776,19 +730,13 @@ return array( // 'Time spent changed: %sh' => '', // 'Time estimated changed: %sh' => '', // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', + // 'The description has been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', // 'I want to receive notifications for:' => '', // 'All tasks' => '', // 'Only for tasks assigned to me' => '', // 'Only for tasks created by me' => '', // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -813,7 +761,6 @@ return array( // 'Not assigned' => '', // 'View advanced search syntax' => '', // 'Overview' => '', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -858,16 +801,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', // 'This feature does not work with all browsers.' => '', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 1c01e94d..39cd25be 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Redigera', 'remove' => 'ta bort', 'Remove' => 'Ta bort', - 'Update' => 'Uppdatera', 'Yes' => 'Ja', 'No' => 'Nej', 'cancel' => 'avbryt', @@ -60,7 +59,6 @@ return array( 'Actions' => 'Åtgärder', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', - 'Add this column' => 'Lägg till kolumnen', '%d tasks on the board' => '%d uppgifter på tavlan', '%d tasks in total' => '%d uppgifter totalt', 'Unable to update this board.' => 'Kunde inte uppdatera tavlan', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Ta bort projekt', 'Edit the board for "%s"' => 'Ändra tavlan för "%s"', 'All projects' => 'Alla projekt', - 'Change columns' => 'Ändra kolumner', 'Add a new column' => 'Lägg till ny kolumn', 'Title' => 'Titel', 'Nobody assigned' => 'Ingen tilldelad', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Öppna en uppgift', 'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?', 'Back to the board' => 'Tillbaka till tavlan', - 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %Y-%m-%d kl %H:%M', 'There is nobody assigned' => 'Det finns ingen tilldelad', 'Column on the board:' => 'Kolumn på tavlan:', - 'Status is open' => 'Statusen är öppen', - 'Status is closed' => 'Statusen är stängd', 'Close this task' => 'Stäng uppgiften', 'Open this task' => 'Öppna uppgiften', 'There is no description.' => 'Det finns ingen beskrivning.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'Pågående', 'Done' => 'Slutfört', 'Application version:' => 'Version:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %Y-%m-%d kl %H:%M', - '%B %e, %Y at %k:%M %p' => '%Y-%m-%d kl %H:%M', - 'Date created' => 'Skapat datum', - 'Date completed' => 'Slutfört datum', 'Id' => 'ID', '%d closed tasks' => '%d stängda uppgifter', 'No task for this project' => 'Inga uppgifter i detta projekt', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Ändra denna uppgift', 'Due Date' => 'Måldatum', 'Invalid date' => 'Ej tillåtet datum', - 'Must be done before %B %e, %Y' => 'Måste vara klart innan %Y-%m-%d', - '%B %e, %Y' => '%Y-%m-%d', - '%b %e, %Y' => '%Y-%m-%d', 'Automatic actions' => 'Automatiska åtgärder', 'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.', 'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Kunde inte ta bort denna åtgärd.', 'Action removed successfully.' => 'Åtgärden har tagits bort.', 'Automatic actions for the project "%s"' => 'Automatiska åtgärder för projektet "%s"', - 'Defined actions' => 'Definierade åtgärder', 'Add an action' => 'Lägg till en åtgärd', 'Event name' => 'Händelsenamn', 'Action name' => 'Åtgärdsnamn', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'När händelsen inträffar, kör inställd åtgärd.', 'Next step' => 'Nästa steg', 'Define action parameters' => 'Definiera upp händelseparametrar', - 'Save this action' => 'Spara denna åtgärd', 'Do you really want to remove this action: "%s"?' => 'Vill du verkligen ta bort denna åtgärd: "%s"?', 'Remove an automatic action' => 'Ta bort en automatiskt åtgärd', 'Assign the task to a specific user' => 'Tilldela uppgiften till en specifik användare', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Tilldela en färg till en specifik användare', 'Column title' => 'Kolumnens titel', 'Position' => 'Position', - 'Move Up' => 'Flytta upp', - 'Move Down' => 'Flytta ned', 'Duplicate to another project' => 'Kopiera till ett annat projekt', 'Duplicate' => 'Kopiera uppgiften', 'link' => 'länk', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Kommentaren har tagits bort.', 'Unable to remove this comment.' => 'Kunde inte ta bort denna kommentar.', 'Do you really want to remove this comment?' => 'Är du säker på att du vill ta bort denna kommentar?', - 'Only administrators or the creator of the comment can access to this page.' => 'Bara administratörer eller skaparen av kommentaren har tillgång till denna sida.', 'Current password for the user "%s"' => 'Nuvarande lösenord för användaren %s"', 'The current password is required' => 'Det nuvarande lösenordet måste anges', 'Wrong password' => 'Fel lösenord', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Extern autentisering misslyckades', 'Your external account is linked to your profile successfully.' => 'Ditt externa konto länkades till din profil.', 'Email' => 'Epost', - 'Link my Google Account' => 'Länka till mitt Google-konto', - 'Unlink my Google Account' => 'Ta bort länken till mitt Google-konto', - 'Login with my Google Account' => 'Logga in med mitt Google-konto', - 'Project not found.' => 'Projektet kunde inte hittas', 'Task removed successfully.' => 'Uppgiften har tagits bort', 'Unable to remove this task.' => 'Kunde inte ta bort denna uppgift', 'Remove a task' => 'Ta bort en uppgift', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maxstorlek: ', 'Unable to upload the file.' => 'Kunde inte ladda upp filen.', 'Display another project' => 'Visa ett annat projekt', - 'Login with my Github Account' => 'Logga in med mitt Github-konto', - 'Link my Github Account' => 'Anslut mitt Github-konto', - 'Unlink my Github Account' => 'Koppla ifrån mitt Github-konto', 'Created by %s' => 'Skapad av %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %Y-%m-%d kl %H:%M', 'Tasks Export' => 'Exportera uppgifter', 'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"', 'Start Date' => 'Startdatum', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:', 'view the task on Kanboard' => 'Visa uppgiften på Kanboard', 'Public access' => 'Publik åtkomst', - 'User management' => 'Hantera användare', 'Active tasks' => 'Aktiva uppgifter', 'Disable public access' => 'Inaktivera publik åtkomst', 'Enable public access' => 'Aktivera publik åtkomst', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Byt lösenord', 'Password modification' => 'Ändra lösenord', 'External authentications' => 'Extern autentisering', - 'Google Account' => 'Googlekonto', - 'Github Account' => 'Githubkonto', 'Never connected.' => 'Inte ansluten.', - 'No account linked.' => 'Inget konto länkat.', - 'Account linked.' => 'Konto länkat.', 'No external authentication enabled.' => 'Ingen extern autentisering aktiverad.', 'Password modified successfully.' => 'Lösenordet har ändrats.', 'Unable to change the password.' => 'Kunde inte byta lösenord.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Ändra tilldelning baserat på ett externt användarnamn', 'Change the category based on an external label' => 'Ändra kategori baserat på en extern etikett', 'Reference' => 'Referens', - 'Reference: %s' => 'Referens: %s', 'Label' => 'Etikett', 'Database' => 'Databas', 'About' => 'Om', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Frekvens i sekunder (60 sekunder är standard)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (ange 0 för att inaktivera, 10 sekunder är standard)', 'Application URL' => 'Applikations-URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exempel: http://example.kanboard.net/ (används för e-postnotiser)', 'Token regenerated.' => 'Token nyskapad.', 'Date format' => 'Datumformat', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-format är alltid tillåtet, exempel: "%s" och "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Det här projektet är privat', 'Type here to create a new sub-task' => 'Skriv här för att skapa en ny deluppgift', 'Add' => 'Lägg till', - 'Estimated time: %s hours' => 'Uppskattad tid: %s timmar', - 'Time spent: %s hours' => 'Nedlaggd tid: %s timmar', - 'Started on %B %e, %Y' => 'Startad %Y-%m-%d', 'Start date' => 'Startdatum', 'Time estimated' => 'Uppskattad tid', 'There is nothing assigned to you.' => 'Du har inget tilldelat till dig.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Vill du verkligen ta bort denna swimlane: "%s"?', 'Inactive swimlanes' => 'Inaktiv swimlane', 'Remove a swimlane' => 'Ta bort en swimlane', - 'Rename' => 'Byt namn', 'Show default swimlane' => 'Visa standard swimlane', 'Swimlane modification for the project "%s"' => 'Ändra swimlane för projektet "%s"', 'Swimlane not found.' => 'Swimlane kunde inte hittas', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane uppdaterad', 'The default swimlane have been updated successfully.' => 'Standardswimlane har uppdaterats', - 'Unable to create your swimlane.' => 'Kunde inte skapa din swimlane', 'Unable to remove this swimlane.' => 'Kunde inte ta bort swimlane', 'Unable to update this swimlane.' => 'Kunde inte uppdatera swimlane', 'Your swimlane have been created successfully.' => 'Din swimlane har skapats', @@ -612,7 +574,6 @@ return array( 'This task' => 'Denna uppgift', '<1h' => '<1h', '%dh' => '%dh', - '%b %e' => '%b %e', 'Expand tasks' => 'Expandera uppgifter', 'Collapse tasks' => 'Minimera uppgifter', 'Expand/collapse tasks' => 'Expandera/minimera uppgifter', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Tangentbordsgenvägar', 'Open board switcher' => 'Växling av öppen tavla', 'Application' => 'Applikation', - 'since %B %e, %Y at %k:%M %p' => 'sedan %B %e, %Y at %k:%M %p', 'Compact view' => 'Kompakt vy', 'Horizontal scrolling' => 'Horisontell scroll', 'Compact/wide view' => 'Kompakt/bred vy', 'No results match:' => 'Inga matchande resultat', 'Currency' => 'Valuta', - 'Files' => 'Filer', - 'Images' => 'Bilder', 'Private project' => 'Privat projekt', 'AUD - Australian Dollar' => 'AUD - Australiska dollar', 'CAD - Canadian Dollar' => 'CAD - Kanadensiska dollar', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Testa din enhet', 'Assign a color when the task is moved to a specific column' => 'Tilldela en färg när uppgiften flyttas till en specifik kolumn', '%s via Kanboard' => '%s via Kanboard', - 'uploaded by: %s' => 'uppladdad av: %s', - 'uploaded on: %s' => 'uppladdad på: %s', - 'size: %s' => 'storlek: %s', 'Burndown chart for "%s"' => 'Burndown diagram för "%s"', 'Burndown chart' => 'Burndown diagram', 'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'En uppgift kan inte länkas till sig själv', 'The exact same link already exists' => 'Länken existerar redan', 'Recurrent task is scheduled to be generated' => 'Återkommande uppgift är schemalagd att genereras', - 'Recurring information' => 'Återkommande information', 'Score' => 'Poäng', 'The identifier must be unique' => 'Identifieraren måste vara unik', 'This linked task id doesn\'t exists' => 'Denna länkade uppgifts id existerar inte', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Spenderad tid har ändrats: %sh', 'Time estimated changed: %sh' => 'Tidsuppskattning ändrad: %sh', 'The field "%s" have been updated' => 'Fältet "%s" har uppdaterats', - 'The description have been modified' => 'Beskrivningen har modifierats', + 'The description has been modified' => 'Beskrivningen har modifierats', 'Do you really want to close the task "%s" as well as all subtasks?' => 'Vill du verkligen stänga uppgiften "%s" och alla deluppgifter?', - 'Swimlane: %s' => 'Swimlane: %s', 'I want to receive notifications for:' => 'Jag vill få notiser för:', 'All tasks' => 'Alla uppgifter', 'Only for tasks assigned to me' => 'Bara för uppgifter tilldelade mig', 'Only for tasks created by me' => 'Bara för uppgifter skapade av mig', 'Only for tasks created by me and assigned to me' => 'Bara för uppgifter skapade av mig och tilldelade till mig', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', - 'New due date: %B %e, %Y' => 'Nytt förfallodatum: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Startdatum ändrat: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%Y-%%m-%%d' => '%%Y-%%m-%%d', 'Total for all columns' => 'Totalt för alla kolumner', 'You need at least 2 days of data to show the chart.' => 'Du behöver minst två dagars data för att visa diagrammet.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Inte tilldelad', 'View advanced search syntax' => 'Visa avancerad söksyntax', 'Overview' => 'Översikts', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Tavla/Kalender/Listvy', 'Switch to the board view' => 'Växla till tavelvy', 'Switch to the calendar view' => 'Växla till kalendervy', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Diagramet visar medel av led och cykeltid för de senaste %d uppgifterna över tiden.', 'Average time into each column' => 'Medeltidsåtgång i varje kolumn', 'Lead and cycle time' => 'Led och cykeltid', - 'Google Authentication' => 'Google autentisering', - 'Help on Google authentication' => 'Hjälp för Google autentisering', - 'Github Authentication' => 'Github autentisering', - 'Help on Github authentication' => 'Hjälp för Github autentisering', 'Lead time: ' => 'Ledtid', 'Cycle time: ' => 'Cykeltid', 'Time spent into each column' => 'Tidsåtgång per kolumn', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Om uppgiften inte är stängd används nuvarande tid istället för slutförandedatum.', 'Set automatically the start date' => 'Sätt startdatum automatiskt', 'Edit Authentication' => 'Ändra autentisering', - 'Google Id' => 'Google Id', - 'Github Id' => 'Github Id', 'Remote user' => 'Extern användare', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Externa användares lösenord lagras inte i Kanboard-databasen, exempel: LDAP, Google och Github-konton.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Om du aktiverar boxen "Tillåt inte loginformulär" kommer inloggningsuppgifter i formuläret att ignoreras.', 'New remote user' => 'Ny extern användare', 'New local user' => 'Ny lokal användare', 'Default task color' => 'Standardfärg för uppgifter', - 'Hide sidebar' => 'Göm sidokolumn', - 'Expand sidebar' => 'Expandera sidokolumn', 'This feature does not work with all browsers.' => 'Denna funktion fungerar inte i alla webbläsare.', 'There is no destination project available.' => 'Det finns inget destinationsprojekt tillgängligt.', 'Trigger automatically subtask time tracking' => 'Aktivera automatisk tidsbevakning av deluppgifter', @@ -901,7 +840,6 @@ return array( // 'open file' => '', // 'End date' => '', // 'Users overview' => '', - // 'Managers' => '', // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', @@ -912,19 +850,10 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index e0de5844..36dd641c 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'แก้ไข', 'remove' => 'ลบ', 'Remove' => 'ลบ', - 'Update' => 'ปรับปรุง', 'Yes' => 'ใช่', 'No' => 'ไม่', 'cancel' => 'ยกเลิก', @@ -20,15 +19,15 @@ return array( 'Red' => 'สีแดง', 'Orange' => 'สีส้ม', 'Grey' => 'สีเทา', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'สีน้ำตาล', + 'Deep Orange' => 'สีส้มเข้ม', + 'Dark Grey' => 'สีเทาเข้ม', + 'Pink' => 'สีชมพู', + 'Teal' => 'สีเขียวหัวเป็ด', + 'Cyan' => 'สีฟ้า', + 'Lime' => 'สีมะนาว', + 'Light Green' => 'สีเขียวสว่าง', + 'Amber' => 'สีเหลืองอำพัน', 'Save' => 'บันทึก', 'Login' => 'เข้าสู่ระบบ', 'Official website:' => 'เวบไซต์อย่างเป็นทางการ:', @@ -60,19 +59,17 @@ return array( 'Actions' => 'การกระทำ', 'Inactive' => 'ไม่เปิดใช้งาน', 'Active' => 'เปิดใช้งาน', - 'Add this column' => 'เพิ่มคอลัมน์', '%d tasks on the board' => '%d งานบนบอร์ด', '%d tasks in total' => '%d งานทั้งหมด', 'Unable to update this board.' => 'ไม่สามารถปรับปรุงบอร์ดได้.', 'Edit board' => 'แก้ไขบอร์ด', - 'Disable' => 'ปิด', - 'Enable' => 'เปิด', + 'Disable' => 'ปิดการทำงาน', + 'Enable' => 'เปิดการทำงาน', 'New project' => 'โปรเจคใหม่', 'Do you really want to remove this project: "%s"?' => 'คุณต้องการเอาโปรเจค « %s » ออกใช่หรือไม่?', 'Remove project' => 'ลบโปรเจค', 'Edit the board for "%s"' => 'แก้ไขบอร์ดสำหรับ « %s »', 'All projects' => 'โปรเจคทั้งหมด', - 'Change columns' => 'เปลี่ยนคอลัมน์', 'Add a new column' => 'เพิ่มคอลัมน์ใหม่', 'Title' => 'หัวเรื่อง', 'Nobody assigned' => 'ไม่กำหนดใคร', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'เปิดงาน', 'Do you really want to open this task: "%s"?' => 'คุณต้องการเปิดงาน: « %s » ใช่หรือไม่?', 'Back to the board' => 'กลับไปที่บอร์ด', - 'Created on %B %e, %Y at %k:%M %p' => 'สร้างวันที่ %d/%m/%Y เวลา %H:%M', 'There is nobody assigned' => 'ไม่มีใครถูกกำหนด', 'Column on the board:' => 'คอลัมน์บนบอร์ด:', - 'Status is open' => 'สถานะเปิด', - 'Status is closed' => 'สถานะปิด', 'Close this task' => 'ปิดงานนี้', 'Open this task' => 'เปิดงานนี้', 'There is no description.' => 'ไม่มีคำอธิบาย', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'กำลังทำ', 'Done' => 'เสร็จ', 'Application version:' => 'แอพเวอร์ชัน:', - 'Completed on %B %e, %Y at %k:%M %p' => 'เรียบร้อยวันที่ %d/%m/%Y เวลา %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y เวลา %H:%M', - 'Date created' => 'สร้างวันที่', - 'Date completed' => 'เรียบร้อยวันที่', 'Id' => 'ไอดี', '%d closed tasks' => '%d งานที่ปิด', 'No task for this project' => 'ไม่มีงานสำหรับโปรเจคนี้', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'แก้ไขงาน', 'Due Date' => 'วันที่ครบกำหนด', 'Invalid date' => 'วันที่ผิด', - 'Must be done before %B %e, %Y' => 'ต้องทำให้เสร็จก่อน %d/%m/%Y', - '%B %e, %Y' => '%d/%m/%Y', - // '%b %e, %Y' => '', 'Automatic actions' => 'การกระทำอัตโนมัติ', 'Your automatic action have been created successfully.' => 'การกระทำอัตโนมัติสร้างเรียบร้อยแล้ว', 'Unable to create your automatic action.' => 'ไม่สามารถสร้างการกระทำอัตโนมัติได้', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'ไม่สามารถลบการกระทำ', 'Action removed successfully.' => 'ลบการกระทำเรียบร้อยแล้ว', 'Automatic actions for the project "%s"' => 'การกระทำอัตโนมัติสำหรับโปรเจค « %s »', - 'Defined actions' => 'กำหนดการกระทำ', 'Add an action' => 'เพิ่มการกระทำ', 'Event name' => 'ชื่อเหตุกาณ์', 'Action name' => 'ชื่อการกระทำ', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'เหตุการ์ที่เลือกจะเกิดขึ้นเมื่อมีการกระทำที่สอดคล้องกัน', 'Next step' => 'ขั้นตอนต่อไป', 'Define action parameters' => 'กำหนดพารามิเตอร์ของการกระทำ', - 'Save this action' => 'บันทึกการกระทำนี้', 'Do you really want to remove this action: "%s"?' => 'คุณต้องการลบการกระทำ « %s » ใช่หรือไม่?', 'Remove an automatic action' => 'ลบการกระทำอัตโนมัติ', 'Assign the task to a specific user' => 'กำหนดงานให้ผู้ใช้แบบเจาะจง', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'กำหนดสีให้ผู้ใช้แบบเจาะจง', 'Column title' => 'หัวเรื่องคอลัมน์', 'Position' => 'ตำแหน่ง', - 'Move Up' => 'ย้ายขึ้น', - 'Move Down' => 'ย้ายลง', 'Duplicate to another project' => 'ทำซ้ำในโปรเจคอื่น', 'Duplicate' => 'ทำซ้ำ', 'link' => 'ลิงค์', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'ลบความคิดเห็นเรียบร้อยแล้ว', 'Unable to remove this comment.' => 'ไม่สามารถลบความคิดเห็นได้', 'Do you really want to remove this comment?' => 'คุณต้องการลบความคิดเห็น', - 'Only administrators or the creator of the comment can access to this page.' => 'เฉพาะผู้ดูแลระบบหรือผู้สร้างความคิดเห็นเข้าถึงหน้านี้', 'Current password for the user "%s"' => 'รหัสผ่านปัจจุบันของผู้ใช้ « %s »', 'The current password is required' => 'ต้องการรหัสผ่านปัจจุบัน', 'Wrong password' => 'รหัสผ่านผิด', @@ -255,39 +237,35 @@ return array( '%d comments' => '%d ความคิดเห็น', '%d comment' => '%d ความคิดเห็น', 'Email address invalid' => 'อีเมลผิด', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', + 'Your external account is not linked anymore to your profile.' => 'บัญชีภายนอกของคุณไม่ได้เชื่อมโยงอีกต่อไปในโปรไฟล์ของคุณ', + 'Unable to unlink your external account.' => 'ไม่สามารถยกเลิกการเชื่อมโยงบัญชีภายนอกของคุณ', + 'External authentication failed' => 'การตรวจสอบภายนอกล้มเหลว', + 'Your external account is linked to your profile successfully.' => 'บัญชีภายนอกของคุณลิงค์กับโปรไฟล์ของคุณเรียบร้อย', 'Email' => 'อีเมล', - 'Link my Google Account' => 'เชื่อมต่อกับกูเกิลแอคเคาท์', - 'Unlink my Google Account' => 'ไม่เชื่อมต่อกับกูเกิลแอคเคาท์', - 'Login with my Google Account' => 'เข้าใช้ด้วยกูเกิลแอคเคาท์', - 'Project not found.' => 'หาโปรเจคไม่พบ', 'Task removed successfully.' => 'ลบงานเรียบร้อยแล้ว', 'Unable to remove this task.' => 'ไม่สามารถลบงานนี้', 'Remove a task' => 'ลบงาาน', 'Do you really want to remove this task: "%s"?' => 'คุณต้องการลบงาน "%s" ออกใช่หรือไม่?', - 'Assign automatically a color based on a category' => 'กำหนดสีอัตโนมัติขึ้นอยู่กับกลุ่ม', - 'Assign automatically a category based on a color' => 'กำหนดกลุ่มอัตโนมัติขึ้นอยู่กับสี', + 'Assign automatically a color based on a category' => 'กำหนดสีอัตโนมัติขึ้นอยู่กับหมวด', + 'Assign automatically a category based on a color' => 'กำหนดหมวดอัตโนมัติขึ้นอยู่กับสี', 'Task creation or modification' => 'สร้างหรือแก้ไขงาน', - 'Category' => 'กลุ่ม', - 'Category:' => 'กลุ่ม:', - 'Categories' => 'กลุ่ม', - 'Category not found.' => 'ไม่พบกลุ่ม.', - 'Your category have been created successfully.' => 'สร้างกลุ่มเรียบร้อยแล้ว', - 'Unable to create your category.' => 'ไม่สามารถสร้างกลุ่มได้', - 'Your category have been updated successfully.' => 'ปรับปรุงกลุ่มเรียบร้อยแล้ว', - 'Unable to update your category.' => 'ไม่สามารถปรับปรุงกลุ่มได้', - 'Remove a category' => 'ลบกลุ่ม', - 'Category removed successfully.' => 'ลบกลุ่มเรียบร้อยแล้ว', - 'Unable to remove this category.' => 'ไม่สามารถลบกลุ่มได้', - 'Category modification for the project "%s"' => 'แก้ไขกลุ่มสำหรับโปรเจค "%s"', - 'Category Name' => 'ชื่อกลุ่ม', - 'Add a new category' => 'เพิ่มกลุ่มใหม่', - 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบกลุ่ม "%s" ใช่หรือไม่?', - 'All categories' => 'กลุ่มทั้งหมด', - 'No category' => 'ไม่มีกลุ่ม', + 'Category' => 'หมวด', + 'Category:' => 'หมวด:', + 'Categories' => 'หมวด', + 'Category not found.' => 'ไม่พบหมวด', + 'Your category have been created successfully.' => 'สร้างหมวดเรียบร้อยแล้ว', + 'Unable to create your category.' => 'ไม่สามารถสร้างหมวดได้', + 'Your category have been updated successfully.' => 'ปรับปรุงหมวดเรียบร้อยแล้ว', + 'Unable to update your category.' => 'ไม่สามารถปรับปรุงหมวดได้', + 'Remove a category' => 'ลบหมวด', + 'Category removed successfully.' => 'ลบหมวดเรียบร้อยแล้ว', + 'Unable to remove this category.' => 'ไม่สามารถลบหมวดได้', + 'Category modification for the project "%s"' => 'แก้ไขหมวดสำหรับโปรเจค "%s"', + 'Category Name' => 'ชื่อหมวด', + 'Add a new category' => 'เพิ่มหมวดใหม่', + 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบหมวด "%s" ใช่หรือไม่?', + 'All categories' => 'หมวดทั้งหมด', + 'No category' => 'ไม่มีหมวด', 'The name is required' => 'ต้องการชื่อ', 'Remove a file' => 'ลบไฟล์', 'Unable to remove this file.' => 'ไม่สามารถลบไฟล์ได้', @@ -310,7 +288,7 @@ return array( 'estimated' => 'ประมาณ', 'Sub-Tasks' => 'งานย่อย', 'Add a sub-task' => 'เพิ่มงานย่อย', - // 'Original estimate' => '', + 'Original estimate' => 'ประมาณการเดิม', 'Create another sub-task' => 'สร้างงานย่อยอื่น', 'Time spent' => 'ใช้เวลา', 'Edit a sub-task' => 'แก้ไขงานย่อย', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'ขนาดสูงสุด:', 'Unable to upload the file.' => 'ไม่สามารถอัพโหลดไฟล์ได้', 'Display another project' => 'แสดงโปรเจคอื่น', - 'Login with my Github Account' => 'เข้าใช้ด้วยกิทฮับแอคเคาท์', - 'Link my Github Account' => 'เชื่อมกับกิทฮับแอคเคาท์', - 'Unlink my Github Account' => 'ยกเลิกการเชื่อมกับกิทอับแอคเคาท์', 'Created by %s' => 'สร้างโดย %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'แก้ไขล่าสุดวันที่ %B %e, %Y เวลา %k:%M %p', 'Tasks Export' => 'ส่งออกงาน', 'Tasks exportation for "%s"' => 'ส่งออกงานสำหรับ "%s"', 'Start Date' => 'เริ่มวันที่', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับโปรเจค:', 'view the task on Kanboard' => 'แสดงงานบน Kanboard', 'Public access' => 'การเข้าถึงสาธารณะ', - 'User management' => 'การจัดการผู้ใช้', 'Active tasks' => 'งานที่กำลังใช้งาน', 'Disable public access' => 'ปิดการเข้าถึงสาธารณะ', 'Enable public access' => 'เปิดการเข้าถึงสาธารณะ', @@ -390,21 +363,17 @@ return array( 'Email:' => 'อีเมล:', 'Notifications:' => 'แจ้งเตือน:', 'Notifications' => 'การแจ้งเตือน', - 'Account type:' => 'ชนิดบัญชี:', + 'Account type:' => 'ประเภทบัญชี:', 'Edit profile' => 'แก้ไขประวัติ', 'Change password' => 'เปลี่ยนรหัสผ่าน', 'Password modification' => 'แก้ไขรหัสผ่าน', 'External authentications' => 'การยืนยันภายนอก', - 'Google Account' => 'กูเกิลแอคเคาท์', - 'Github Account' => 'กิทฮับแอคเคาท์', 'Never connected.' => 'ไม่เชื่อมต่อ', - 'No account linked.' => 'แอคเคาท์ไม่มีการเชื่อม', - 'Account linked.' => 'แอคเคาท์เชื่อมต่อแล้ว', 'No external authentication enabled.' => 'ไม่เปิดการใช้งานการยืนยันภายนอก', 'Password modified successfully.' => 'แก้ไขรหัสผ่านเรียบร้อยแล้ว', 'Unable to change the password.' => 'ไม่สามารถเปลี่ยนรหัสผ่านได้', - 'Change category for the task "%s"' => 'เปลี่ยนกลุ่มสำหรับงาน "%s"', - 'Change category' => 'เปลี่ยนกลุ่ม', + 'Change category for the task "%s"' => 'เปลี่ยนหมวดสำหรับงาน "%s"', + 'Change category' => 'เปลี่ยนหมวด', '%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s', '%s opened the task %s' => '%s เปิดงานแล้ว %s', '%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"', @@ -433,15 +402,14 @@ return array( '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 changed the assignee of the task %s to %s' => '', + '%s change 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' => 'เลือกเหตุการณ์', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', + 'Create a task from an external provider' => 'สร้างงานจากบริการภายนอก', + 'Change the assignee based on an external username' => 'เปลี่ยนผู้รับผิดชอบขึ้นอยู่กับชื่อผู้ใช้ภายนอก', + 'Change the category based on an external label' => 'เปลี่ยนหมวดขึ้นอยู่กับป้ายชื่อภายนอก', + 'Reference' => 'อ้างถึง', 'Label' => 'ป้ายชื่อ', 'Database' => 'ฐานข้อมูล', 'About' => 'เกี่ยวกับ', @@ -459,17 +427,13 @@ return array( 'Frequency in second (60 seconds by default)' => 'ความถี่ (ค่าเริ่มต้นทุก 60 วินาที) ', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'ความถี่ (0 ไม่ใช้คุณลักษณะนี้, ค่าเริ่มต้นทุก 10 วินาที)', // 'Application URL' => '', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'ตัวอย่าง: http://example.kanboard.net/ (ถูกใช้ในการแจ้งเตือนทางอีเมล์)', // 'Token regenerated.' => '', 'Date format' => 'รูปแบบวันที่', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ยอมรับรูปแบบ ISO ตัวอย่าง: "%s" และ "%s"', 'New private project' => 'เพิ่มโปรเจคส่วนตัวใหม่', 'This project is private' => 'โปรเจคนี้เป็นโปรเจคส่วนตัว', - // 'Type here to create a new sub-task' => '', + 'Type here to create a new sub-task' => 'พิมพ์ที่นี้เพื่อสร้างงานย่อยใหม่', 'Add' => 'เพิ่ม', - 'Estimated time: %s hours' => 'เวลาเฉลี่ย: %s ชั่วโมง', - 'Time spent: %s hours' => 'ใช้เวลาไป %s ชม.', - 'Started on %B %e, %Y' => 'เริ่ม %B %e, %Y', 'Start date' => 'เริ่มวันที่', 'Time estimated' => 'เวลาโดยประมาณ', 'There is nothing assigned to you.' => 'ไม่มีอะไรกำหนดให้คุณ', @@ -481,7 +445,7 @@ return array( 'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้', // 'Webhooks' => '', // 'API' => '', - // 'Create a comment from an external provider' => '', + 'Create a comment from an external provider' => 'สร้างความคิดเห็นจากบริการภายนอก', 'Project management' => 'การจัดการโปรเจค', 'My projects' => 'โปรเจคของฉัน', 'Columns' => 'คอลัมน์', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'คุณต้องการลบสวิมเลนนี้ : "%s"?', 'Inactive swimlanes' => 'สวิมเลนไม่ทำงาน', 'Remove a swimlane' => 'ลบสวิมเลน', - 'Rename' => 'เปลี่ยนชื่อ', 'Show default swimlane' => 'แสดงสวิมเลนเริ่มต้น', 'Swimlane modification for the project "%s"' => 'แก้ไขสวิมเลนสำหรับโปรเจค "%s"', 'Swimlane not found.' => 'หาสวิมเลนไม่พบ', @@ -537,12 +500,11 @@ return array( 'Swimlanes' => 'สวิมเลน', 'Swimlane updated successfully.' => 'ปรับปรุงสวิมเลนเรียบร้อยแล้ว', 'The default swimlane have been updated successfully.' => 'สวิมเลนเริ่มต้นปรับปรุงเรียบร้อยแล้ว', - 'Unable to create your swimlane.' => 'ไม่สามารถสร้างสวิมเลนของคุณได้', 'Unable to remove this swimlane.' => 'ไม่สามารถลบสวิมเลนนี้', 'Unable to update this swimlane.' => 'ไม่สามารถปรับปรุงสวิมเลนนี้', 'Your swimlane have been created successfully.' => 'สวิมเลนของคุณถูกสร้างเรียบร้อยแล้ว', 'Example: "Bug, Feature Request, Improvement"' => 'ตัวอย่าง: "Bug, Feature Request, Improvement"', - 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นกลุ่มสำหรับโปรเจคใหม่ (Comma-separated)', + 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นหมวดสำหรับโปรเจคใหม่ (Comma-separated)', 'Integrations' => 'การใช้ร่วมกัน', 'Integration with third-party services' => 'การใช้งานร่วมกับบริการ third-party', 'Subtask Id' => 'รหัสงานย่อย', @@ -557,7 +519,7 @@ return array( 'All columns' => 'คอลัมน์ทั้งหมด', 'Calendar' => 'ปฏิทิน', 'Next' => 'ต่อไป', - // '#%d' => '', + '#%d' => '#%d', 'All swimlanes' => 'สวิมเลนทั้งหมด', 'All colors' => 'สีทั้งหมด', 'Moved to column %s' => 'เคลื่อนไปคอลัมน์ %s', @@ -570,17 +532,17 @@ return array( 'There is nothing to show.' => 'ไม่มีที่ต้องแสดง', 'Time Tracking' => 'ติดตามเวลา', 'You already have one subtask in progress' => 'คุณมีหนึ่งงานย่อยที่กำลังทำงาน', - // 'Which parts of the project do you want to duplicate?' => '', - // 'Disallow login form' => '', + 'Which parts of the project do you want to duplicate?' => 'เลือกส่วนของโปรเจคที่คุณต้องการทำซ้ำ?', + 'Disallow login form' => 'ไม่อนุญาตให้ใช้แบบฟอร์มการเข้าสู่ระบบ', 'Start' => 'เริ่ม', 'End' => 'จบ', 'Task age in days' => 'อายุงาน', 'Days in this column' => 'วันในคอลัมน์นี้', - // '%dd' => '', + '%dd' => '%d วัน', 'Add a link' => 'เพิ่มลิงค์', 'Add a new link' => 'เพิ่มลิงค์ใหม่', 'Do you really want to remove this link: "%s"?' => 'คุณต้องการลบลิงค์นี้: "%s"?', - // 'Do you really want to remove this link with task #%d?' => '', + 'Do you really want to remove this link with task #%d?' => 'คุณต้องการลบลิงค์นี้ของงาน #%d?', 'Field required' => 'ต้องใส่', 'Link added successfully.' => 'เพิ่มลิงค์เรียบร้อยแล้ว', 'Link updated successfully.' => 'ปรับปรุงลิงค์เรียบร้อยแล้ว', @@ -591,7 +553,7 @@ return array( 'Link settings' => 'ตั้งค่าลิงค์', 'Opposite label' => 'ป้ายชื่อตรงข้าม', 'Remove a link' => 'ลบลิงค์', - 'Task\'s links' => 'ลิงค', + 'Task\'s links' => 'ลิงค์', 'The labels must be different' => 'ป้ายชื่อต้องต่างกัน', 'There is no link.' => 'ไม่มีลิงค์', 'This label must be unique' => 'ป้ายชื่อต้องไม่ซ้ำกัน', @@ -610,9 +572,8 @@ return array( 'fixes' => 'เจาะจง', 'is fixed by' => 'ถูกเจาะจงด้วย', 'This task' => 'งานนี้', - // '<1h' => '', - // '%dh' => '', - // '%b %e' => '', + '<1h' => '<1 ชม.', + '%dh' => '%d ชม.', 'Expand tasks' => 'ขยายงาน', 'Collapse tasks' => 'ย่องาน', 'Expand/collapse tasks' => 'ขยาย/ย่อ งาน', @@ -622,27 +583,24 @@ return array( 'Keyboard shortcuts' => 'คีย์ลัด', 'Open board switcher' => 'เปิดการสลับบอร์ด', 'Application' => 'แอพพลิเคชัน', - 'since %B %e, %Y at %k:%M %p' => 'เริ่ม %B %e, %Y เวลา %k:%M %p', 'Compact view' => 'มุมมองพอดี', 'Horizontal scrolling' => 'เลื่อนตามแนวนอน', 'Compact/wide view' => 'พอดี/กว้าง มุมมอง', 'No results match:' => 'ไม่มีผลลัพท์ที่ตรง', 'Currency' => 'สกุลเงิน', - 'Files' => 'ไฟล์', - 'Images' => 'รูปภาพ', 'Private project' => 'โปรเจคส่วนตัว', - // 'AUD - Australian Dollar' => '', - // 'CAD - Canadian Dollar' => '', - // 'CHF - Swiss Francs' => '', - // 'Custom Stylesheet' => '', + 'AUD - Australian Dollar' => 'AUD - ดอลลาร์ออสเตรเลีย', + 'CAD - Canadian Dollar' => 'CAD - ดอลลาร์แคนาดา', + 'CHF - Swiss Francs' => 'CHF - ฟรังก์สวิส', + 'Custom Stylesheet' => 'สไตล์ที่กำหนดเอง', 'download' => 'ดาวน์โหลด', - // 'EUR - Euro' => '', - // 'GBP - British Pound' => '', - // 'INR - Indian Rupee' => '', - // 'JPY - Japanese Yen' => '', - // 'NZD - New Zealand Dollar' => '', - // 'RSD - Serbian dinar' => '', - // 'USD - US Dollar' => '', + 'EUR - Euro' => 'EUR - ยูโร', + 'GBP - British Pound' => 'GBP - ปอนด์อังกฤษ', + 'INR - Indian Rupee' => 'INR - รูปี', + 'JPY - Japanese Yen' => 'JPY - เยน', + 'NZD - New Zealand Dollar' => 'NZD - ดอลลาร์นิวซีแลนด์', + 'RSD - Serbian dinar' => 'RSD - ดีนาร์เซอร์เบีย', + 'USD - US Dollar' => 'USD - ดอลลาร์สหรัฐ', 'Destination column' => 'คอลัมน์เป้าหมาย', 'Move the task to another column when assigned to a user' => 'ย้ายงานไปคอลัมน์อื่นเมื่อกำหนดบุคคลรับผิดชอบ', 'Move the task to another column when assignee is cleared' => 'ย้ายงานไปคอลัมน์อื่นเมื่อไม่กำหนดบุคคลรับผิดชอบ', @@ -652,16 +610,16 @@ return array( 'Time spent in the column' => 'เวลาที่ใช้ในคอลัมน์', 'Task transitions' => 'การเปลี่ยนคอลัมน์งาน', 'Task transitions export' => 'ส่งออกการเปลี่ยนคอลัมน์งาน', - // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - 'Currency rates' => 'อัตราแลกเปลี่ยน', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'รายงานนี้มีการเคลื่อนไหวคอลัมน์ทั้งหมดของงานแต่ละงานมีวันที่ผู้ใช้และเวลาที่ใช้สำหรับแต่ละการเปลี่ยนแปลง', + 'Currency rates' => 'อัตราค่าเงิน', 'Rate' => 'อัตรา', - // 'Change reference currency' => '', - 'Add a new currency rate' => 'เพิ่มอัตราแลกเปลี่ยนเงินตราใหม่', - // 'Reference currency' => '', - // 'The currency rate have been added successfully.' => '', - // 'Unable to add this currency rate.' => '', + 'Change reference currency' => 'เปลี่ยนการอ้างถึงค่าเงิน', + 'Add a new currency rate' => 'เพิ่มอัตราค่าเงินใหม่', + 'Reference currency' => 'อ้างถึงค่าเงิน', + 'The currency rate have been added successfully.' => 'เพิ่มอัตราค่าเงินเรียบร้อย', + 'Unable to add this currency rate.' => 'ไม่สามารถเพิ่มค่าเงินนี้', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', + '%s remove the assignee of the task %s' => '%s เอาผู้รับผิดชอบออกจากงาน %s', 'Enable Gravatar images' => 'สามารถใช้งานภาพ Gravatar', 'Information' => 'ข้อมูลสารสนเทศ', // 'Check two factor authentication code' => '', @@ -675,47 +633,43 @@ return array( 'Test your device' => 'ทดสอบอุปกรณ์ของคุณ', 'Assign a color when the task is moved to a specific column' => 'กำหนดสีเมื่องานถูกย้ายไปคอลัมน์ที่กำหนดไว้', // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - 'size: %s' => 'ขนาด: %s', 'Burndown chart for "%s"' => 'แผนภูมิงานกับเวลา "%s"', 'Burndown chart' => 'แผนภูมิงานกับเวลา', - // 'This chart show the task complexity over the time (Work Remaining).' => '', - // 'Screenshot taken %s' => '', + 'This chart show the task complexity over the time (Work Remaining).' => 'แผนภูมิแสดงความซับซ้อนของงานตามเวลา (งานที่เหลือ)', + 'Screenshot taken %s' => 'จับภาพหน้าจอ %s', 'Add a screenshot' => 'เพิ่ม screenshot', - // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'จับภาพหน้าจอ (screenshot) และกด CTRL+V หรือ ⌘+V เพื่อวางที่นี้', 'Screenshot uploaded successfully.' => 'อัพโหลด screenshot เรียบร้อยแล้ว', - // 'SEK - Swedish Krona' => '', - // 'Identifier' => '', + 'SEK - Swedish Krona' => 'SEK - โครนสวีเดน', + 'Identifier' => 'ตัวบ่งชี้', // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'คุณต้องการยกเลิก the two factor authentication สำหรับผู้ใช้นีั: "%s"?', 'Edit link' => 'แก้ไขลิงค์', 'Start to type task title...' => 'พิมพ์ชื่องาน', 'A task cannot be linked to itself' => 'งานไม่สามารถลิงค์ตัวเอง', // 'The exact same link already exists' => '', 'Recurrent task is scheduled to be generated' => 'งานแบบวนลูปถูกสร้างตามที่กำหนดไว้', - 'Recurring information' => 'รายละเอียดการวนลูป', 'Score' => 'คะแนน', - // 'The identifier must be unique' => '', + 'The identifier must be unique' => 'ตัวบ่งชี้ต้องไม่ซ้ำ', // 'This linked task id doesn\'t exists' => '', 'This value must be alphanumeric' => 'ค่านี้ต้องเป็นตัวอักษร', 'Edit recurrence' => 'แก้ไขการวนลูป', 'Generate recurrent task' => 'สร้างงานที่เป็นวนลูป', 'Trigger to generate recurrent task' => 'จะสร้างงานแบบวนลูป', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', + 'Factor to calculate new due date' => 'ปัจจัยการคำนวณวันครบกำหนดใหม่', + 'Timeframe to calculate new due date' => 'ระยะเวลาการคำนวณวันครบกำหนดใหม่', + 'Base date to calculate new due date' => 'ฐานวันที่การคำนวณวันครบกำหนดใหม่', + 'Action date' => 'วันที่ทำ', + 'Base date to calculate new due date: ' => 'ฐานวันที่การคำนวณวันครบกำหนดใหม่: ', 'This task has created this child task: ' => 'งานนี้สร้างงานลูกคือ', 'Day(s)' => 'วัน', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', + 'Existing due date' => 'วันครบกำหนดที่มีอยู่', + 'Factor to calculate new due date: ' => 'ปัจจัยการคำนวณวันครบกำหนดใหม่: ', 'Month(s)' => 'เดือน', 'Recurrence' => 'วนลูป', 'This task has been created by: ' => 'งานนี้ถูกสร้างโดย', 'Recurrent task has been generated:' => 'งานแบบวนลูปถูกสร้าง', - // 'Timeframe to calculate new due date: ' => '', + 'Timeframe to calculate new due date: ' => 'ระยะเวลาการคำนวณวันครบกำหนดใหม่: ', 'Trigger to generate recurrent task: ' => 'จะสร้างงานแบบวนลูป', 'When task is closed' => 'เมื่อปิดงาน', 'When task is moved from first column' => 'เมื่องานถูกย้ายจากคอลัมน์แรก', @@ -737,385 +691,464 @@ return array( // 'Two factor authentication enabled' => '', 'Unable to update this user.' => 'ไม่สามารถปรับปรุงผู้ใช้นี้', 'There is no user management for private projects.' => 'ไม่มีการจัดการผู้ใช้สำหรับโปรเจคส่วนตัว', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', - // 'Notification' => '', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', + 'User that will receive the email' => 'ผู้ใช้จะได้รับอีเมล์', + 'Email subject' => 'หัวเรื่องอีเมล์', + 'Date' => 'วันที่', + 'Add a comment log when moving the task between columns' => 'เพิ่มล็อกความคิดเห็นเมื่อย้ายงานระหว่างคอลัมน์', + 'Move the task to another column when the category is changed' => 'ย้ายงานไปคอลัมน์อื่นเมื่อหมวดถูกเปลี่ยน', + 'Send a task by email to someone' => 'ส่งงานโดยถึงบางคน', + 'Reopen a task' => 'เปิดงานอีกครั้ง', + 'Column change' => 'เปลี่ยนคอลัมน์', + 'Position change' => 'เปลี่ยนตำแหน่ง', + 'Swimlane change' => 'เปลี่ยนสวิมเลน', + 'Assignee change' => 'เปลี่ยนการผู้รับผิดชอบ', + '[%s] Overdue tasks' => '[%s] งานที่เกินกำหนด', + 'Notification' => 'แจ้งเตือน', + '%s moved the task #%d to the first swimlane' => '%s ย้ายงาน #%d ไปสวินเลนแรก', + '%s moved the task #%d to the swimlane "%s"' => '%s ย้ายงาน #%d ไปสวินเลน "%s"', + 'Swimlane' => 'สวิมเลน', + 'Gravatar' => 'รูปแทนตัว', + '%s moved the task %s to the first swimlane' => '%s ย้ายงาน %s ไปสวินเลนแรก', + '%s moved the task %s to the swimlane "%s"' => '%s ย้ายงาน %s ไปสวินเลนไปสวินเลน "%s"', + 'This report contains all subtasks information for the given date range.' => 'รายงานนี้มีข้อมูลงานย่อยทั้งหมดในช่วงวันที่กำหนด', + 'This report contains all tasks information for the given date range.' => 'รายงานนี้มีข้อมูลงานทั้งหมดสำหรับช่วงวันที่ที่กำหนด', + 'Project activities for %s' => 'กิจกรรมโปรเจคสำหรับ %s', + 'view the board on Kanboard' => 'แสดงบอร์ดบนคังบอร์ด', + 'The task have been moved to the first swimlane' => 'งานถูกย้านไปสวิมเลนแรก', + 'The task have been moved to another swimlane:' => 'งานถูกย้านไปสวิมเลนอื่น:', + 'Overdue tasks for the project "%s"' => 'งานที่เกินกำหนดสำหรับโปรเจค "%s"', + 'New title: %s' => 'ชื่อเรื่องใหม่: %s', + 'The task is not assigned anymore' => 'ไม่กำหนดผู้รับผิดชอบ', + 'New assignee: %s' => 'ผู้รับผิดชอบใหม่: %s', + 'There is no category now' => 'ปัจจุบันไม่มีหมวด', + 'New category: %s' => 'หมวดใหม่: %s', + 'New color: %s' => 'สีใหม่: %s', + 'New complexity: %d' => 'ความซับซ้อนใหม่: %d', + 'The due date have been removed' => 'วันครบกำหนดถูกลบ', + 'There is no description anymore' => 'ไม่มีคำอธิบาย', + 'Recurrence settings have been modified' => 'แก้ไขการตั้งค่าการวนลูป', + 'Time spent changed: %sh' => 'เวลาที่ใช้ในการเปลี่ยน: %s ชม.', + 'Time estimated changed: %sh' => 'เวลาโดยประมาณในการเปลี่ยน: %s ชม.', + 'The field "%s" have been updated' => 'ฟิลด์ "%s" ถูกปรับปรุง', + 'The description has been modified' => 'คำอธิบายถูกแก้ไข', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'คุณต้องการปิดงาน "%s" เช่นเดียวกับงานย่อยทั้งหมด?', + 'I want to receive notifications for:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับ:', + 'All tasks' => 'ทุกงาน', + 'Only for tasks assigned to me' => 'เฉพาะงานที่ฉันรับผิดชอบ', + 'Only for tasks created by me' => 'เฉพาะงานที่ฉันสร้าง', + 'Only for tasks created by me and assigned to me' => 'เฉพาะงานที่ฉันสร้างและฉันรับผิดชอบ', // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', - // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - // 'Cycle Time' => '', - // 'Lead Time' => '', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', - // 'Lead time: ' => '', - // 'Cycle time: ' => '', - // 'Time spent into each column' => '', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', + 'Total for all columns' => 'จำนวนคอลัมน์ทั้งหมด', + 'You need at least 2 days of data to show the chart.' => 'คุณต้องการอย่างน้อย 2 วันในการแสดงแผนภูมิ', + '<15m' => '<15นาที', + '<30m' => '<30นาที', + 'Stop timer' => 'หยุดจับเวลา', + 'Start timer' => 'เริ่มจับเวลา', + 'Add project member' => 'เพิ่มสมาชิกโปรเจค', + 'Enable notifications' => 'เปิดการแจ้งเตือน', + 'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน', + 'My calendar' => 'ปฎิทินของฉัน', + 'Search tasks' => 'ค้นหางาน', + 'Back to the calendar' => 'กลับไปที่ปฎิทิน', + 'Filters' => 'ตัวกรอง', + 'Reset filters' => 'ล้างตัวกรอง', + 'My tasks due tomorrow' => 'งานถึงกำหนดของฉันวันพรุ่งนี้', + 'Tasks due today' => 'งานถึงกำหนดวันนี้', + 'Tasks due tomorrow' => 'งานถึงกำหนดพรุ่งนี้', + 'Tasks due yesterday' => 'งานถึงกำหนดเมื่อวาน', + 'Closed tasks' => 'งานปิด', + 'Open tasks' => 'งานเปิด', + 'Not assigned' => 'ไม่กำหนดใคร', + 'View advanced search syntax' => 'แสดงรูปแบบการค้นหาขั้นสูง', + 'Overview' => 'ภาพรวม', + 'Board/Calendar/List view' => 'มุมมอง บอร์ด/ปฎิทิน/ลิสต์', + 'Switch to the board view' => 'เปลี่ยนเป็นมุมมองบอร์ด', + 'Switch to the calendar view' => 'เปลี่ยนเป็นมุมมองปฎิทิน', + 'Switch to the list view' => 'เปลี่ยนเป็นมุมมองลิสต์', + 'Go to the search/filter box' => 'ไปที่กล่องค้นหา/ตัวกรอง', + 'There is no activity yet.' => 'ตอนนี้ไม่มีกิจกรรม', + 'No tasks found.' => 'ไม่พบงาน', + 'Keyboard shortcut: "%s"' => 'คีย์ลัด: %s', + 'List' => 'ลิสต์', + 'Filter' => 'ตัวกรอง', + 'Advanced search' => 'ค้นหาขั้นสูง', + 'Example of query: ' => 'ตัวอย่างคิวรี: ', + 'Search by project: ' => 'ค้นหาตามโปรเจค: ', + 'Search by column: ' => 'ค้นหาตามคอลัมน์: ', + 'Search by assignee: ' => 'ค้นหาตามผู้รับผิดชอบ: ', + 'Search by color: ' => 'ค้นหาตามสี: ', + 'Search by category: ' => 'ค้นหาตามหมวด: ', + 'Search by description: ' => 'ค้นหาตามคำอธิบาย: ', + 'Search by due date: ' => 'ค้นหาตามวันครบกำหนด: ', + 'Lead and Cycle time for "%s"' => 'เวลานำและรอบเวลาสำหรับ "%s"', + 'Average time spent into each column for "%s"' => 'ค่าเฉลี่ยเวลาที่ใช้แต่ละคอลัมน์สำหรับ "%s"', + 'Average time spent into each column' => 'ค่าเฉลี่ยเวลาที่ใช้แต่ละคอลัมน์', + 'Average time spent' => 'ค่าเฉลี่ยเวลาที่ใช้', + 'This chart show the average time spent into each column for the last %d tasks.' => 'แผนภูมิแสดงค่าเฉลี่ยเวลาที่ใช้แต่ละคอลัมน์สำหรับ %d งานล่าสุด', + 'Average Lead and Cycle time' => 'ค่าเฉลี่ยเวลานำและรอบเวลา', + 'Average lead time: ' => 'ค่าเฉลี่ยเวลานำ: ', + 'Average cycle time: ' => 'ค่าเฉลี่ยรอบเวลา: ', + 'Cycle Time' => 'รอบเวลา', + 'Lead Time' => 'เวลานำ', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'แผนภูมิแสดงค่าเฉลี่ยเวลานำและรอบเวลาสำหรับ %d งานล่าสุดเมื่อเวลาผ่านไป', + 'Average time into each column' => 'ค่าเฉลี่ยเวลาแต่ละคอลัมน์', + 'Lead and cycle time' => 'เวลานำและรอบเวลา', + 'Lead time: ' => 'เวลานำ: ', + 'Cycle time: ' => 'รอบเวลา: ', + 'Time spent into each column' => 'เวลาที่ใช้แต่ละคอลัมน์', + 'The lead time is the duration between the task creation and the completion.' => 'เวลานำคือระหว่างสร้างงานและจบงาน', + 'The cycle time is the duration between the start date and the completion.' => 'รอบเวลาคือระหว่างวันที่เริ่มและจบงาน', // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', - // 'Remote user' => '', + 'Set automatically the start date' => 'ตั้งค่าวันที่เริ่มต้นอัตโนมัติ', + 'Edit Authentication' => 'แก้ไขการตรวจสอบ', + 'Remote user' => 'ผู้ใช้รีโมท', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', + 'New remote user' => 'เพิ่มผู้ใช้รีโมทใหม่', + 'New local user' => 'เพิ่มผู้ใช้ท้องถิ่นใหม่', + 'Default task color' => 'สีเริ่มต้นของงาน', + 'This feature does not work with all browsers.' => 'คุณลักษณะนี้ไม่สามารถทำงานได้ทุกเบราเซอร์', // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', + 'Trigger automatically subtask time tracking' => 'เรียกโดยอัตโนมัติการติดตามเวลางานย่อย', // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', - // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', - // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', - // 'Documentation' => '', - // 'Table of contents' => '', - // 'Gantt' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', + 'Current swimlane: %s' => 'สวิมเลนปัจจุบัน: %s', + 'Current column: %s' => 'คอลัมน์ปัจจุบัน: %s', + 'Current category: %s' => 'หมวดปัจจุบัน: %s', + 'no category' => 'ไม่มีหมวด', + 'Current assignee: %s' => 'ผู้รับผิดชอบปัจจุบัน: %s', + 'not assigned' => 'ไม่กำหนด', + 'Author:' => 'ผู้แต่ง:', + 'contributors' => 'ผู้ให้กำเนิด', + 'License:' => 'สัญญาอนุญาต:', + 'License' => 'สัญญาอนุญาต', + 'Enter the text below' => 'พิมพ์ข้อความด้านล่าง', + 'Gantt chart for %s' => 'แผนภูมิแกรนท์สำหรับ %s', + 'Sort by position' => 'เรียงตามตำแหน่ง', + 'Sort by date' => 'เรียงตามวัน', + 'Add task' => 'เพิ่มงาน', + 'Start date:' => 'วันที่เริ่ม:', + 'Due date:' => 'วันครบกำหนด:', + 'There is no start date or due date for this task.' => 'งานนี้ไม่มีวันที่เริ่มหรือวันครบกำหนด', + 'Moving or resizing a task will change the start and due date of the task.' => 'การย้ายหรือปรับขนาดงานจะมีการเปลี่ยนแปลงที่วันเริ่มต้นและวันที่ครบกำหนดของงาน', + 'There is no task in your project.' => 'โปรเจคนี้ไม่มีงาน', + 'Gantt chart' => 'แผนภูมิแกรนท์', + 'People who are project managers' => 'คนที่เป็นผู้จัดการโปรเจค', + 'People who are project members' => 'คนที่เป็นสมาชิกโปรเจค', + 'NOK - Norwegian Krone' => 'NOK - โครนนอร์เวย์', + 'Show this column' => 'แสดงคอลัมนี้', + 'Hide this column' => 'ซ่อนคอลัมน์นี้', + 'open file' => 'เปิดไฟล์', + 'End date' => 'วันจบ', + 'Users overview' => 'ภาพรวมผู้ใช้', + 'Members' => 'สมาชิก', + 'Shared project' => 'แชร์โปรเจค', + 'Project managers' => 'ผู้จัดการโปรเจค', + 'Gantt chart for all projects' => 'แผนภูมิแกรนท์สำหรับทุกโปรเจค', + 'Projects list' => 'รายการโปรเจค', + 'Gantt chart for this project' => 'แผนภูมิแกรนท์สำหรับโปรเจคนี้', + 'Project board' => 'บอร์ดโปรเจค', + 'End date:' => 'วันที่จบ:', + 'There is no start date or end date for this project.' => 'ไม่มีวันที่เริ่มหรือวันที่จบของโปรเจคนี้', + 'Projects Gantt chart' => 'แผนภูมิแกรน์ของโปรเจค', + 'Link type' => 'ประเภทลิงค์', + 'Change task color when using a specific task link' => 'เปลี่ยนสีงานเมื่อมีการใช้การเชื่อมโยงงาน', + 'Task link creation or modification' => 'การสร้างการเชื่อมโยงงานหรือการปรับเปลี่ยน', + 'Milestone' => 'ขั้น', + 'Documentation: %s' => 'เอกสาร: %s', + 'Switch to the Gantt chart view' => 'เปลี่ยนเป็นมุมมองแผนภูมิแกรนท์', + 'Reset the search/filter box' => 'รีเซตกล่องค้นหา/ตัวกรอง', + 'Documentation' => 'เอกสาร', + 'Table of contents' => 'สารบัญ', + 'Gantt' => 'แกรนท์', + 'Author' => 'ผู้แต่ง', + 'Version' => 'เวอร์ชัน', + 'Plugins' => 'ปลั๊กอิน', + 'There is no plugin loaded.' => 'ไม่มีปลั๊กอินถูกโหลดไว้', + 'Set maximum column height' => 'กำหนดความสูงสูงสุดของคอลัมน์', + 'Remove maximum column height' => 'เอาความสูงสูงสุดของคอลัมน์ออก', + 'My notifications' => 'การแจ้งเตือนของฉัน', + 'Custom filters' => 'ตัวกรองกำหนดเอง', + 'Your custom filter have been created successfully.' => 'ตัวกรองกำหนดเองของคุณสร้างเรียบร้อย', + 'Unable to create your custom filter.' => 'ไม่สามารถสร้างตัวกรองกำหนดเอง', + 'Custom filter removed successfully.' => 'ลบตัวกรองกำหนดเองเรียบร้อย', + 'Unable to remove this custom filter.' => 'ไม่สามารถลบตัวกรองกำหนดเอง', + 'Edit custom filter' => 'แก้ไขตัวกรองกำหนดเอง', + 'Your custom filter have been updated successfully.' => 'ตัวกรองกำหนดเองของคุณแก้ไขเรียบร้อย', + 'Unable to update custom filter.' => 'ไม่สามารถแก้ไขตัวกรองกำหนดเอง', + 'Web' => 'เวบ', + 'New attachment on task #%d: %s' => 'แนบใหม่ของงาน #%d: %s', + 'New comment on task #%d' => 'ความคิดเห็นใหม่ของงาน #%d', + 'Comment updated on task #%d' => 'แก้ไขความคิดเห็นของงาน #%d', + 'New subtask on task #%d' => 'งานย่อยใหม่ของงาน #%d', + 'Subtask updated on task #%d' => 'แก้ไขงานย่อยของงาน #%d', + 'New task #%d: %s' => 'งานใหม่ #%d: %s', + 'Task updated #%d' => 'แก้ไขงาน #%d', + 'Task #%d closed' => 'ปิดงาน #%d', + 'Task #%d opened' => 'เปิดงาน #%d', + 'Column changed for task #%d' => 'เปลี่ยนคอลัมน์สำหรับงาน #%d', + 'New position for task #%d' => 'ตำแหน่งใหม่ของงาน #%d', + 'Swimlane changed for task #%d' => 'เปลี่ยนสวิมเลนสำหรับงาน #%d', + 'Assignee changed on task #%d' => 'เปลี่ยนผู้รับผิดชอบงาน #%d', + '%d overdue tasks' => '%d งานเกินกำหนด', + 'Task #%d is overdue' => 'งาน #%d เกินกำหนด', + 'No new notifications.' => 'ไม่มีการแจ้งเตือนใหม่', + 'Mark all as read' => 'มาร์คทั้งหมดว่าอ่านแล้ว', + 'Mark as read' => 'มาร์คว่าอ่านแล้ว', // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', + 'Collapse swimlane' => 'ย่อสวิมเลน', + 'Expand swimlane' => 'ขยายสวิมเลน', + 'Add a new filter' => 'เพิ่มตัวกรองใหม่', + 'Share with all project members' => 'แชร์ให้สมาชิกทุกคนของโปรเจค', + 'Shared' => 'แชร์', + 'Owner' => 'เจ้าของ', + 'Unread notifications' => 'การแจ้งเตือนยังไม่ได้อ่าน', + 'My filters' => 'ตัวกรองของฉัน', + 'Notification methods:' => 'ลักษณะการแจ้งเตือน:', + 'Import tasks from CSV file' => 'นำเข้างานจากไฟล์ CSV', + 'Unable to read your file' => 'ไม่สามารถอ่านไฟล์ของคุณ', + '%d task(s) have been imported successfully.' => '%d งานนำเข้าเรียบร้อย', + 'Nothing have been imported!' => 'ไม่มีอะไรถูกนำเข้า', + 'Import users from CSV file' => 'นำเข้าผู้ใช้จากไฟล์ CSV', + '%d user(s) have been imported successfully.' => '%d ผู้ใช้นำเข้าเรียบร้อย', + 'Comma' => ', - Comma', + 'Semi-colon' => '; - Semi-colon', + 'Tab' => 'Tab - Tab', + 'Vertical bar' => '| - Vertical bar', + 'Double Quote' => '" " - Double Quote', + 'Single Quote' => '\' \' - Single Quote', + '%s attached a file to the task #%d' => '%s แนบไฟล์ในงาน #%d', + 'There is no column or swimlane activated in your project!' => 'ไม่มีคอลัมน์หรือสวิมเลนเปิดใช้งานในโปรเจคของคุณ!', // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', + 'Append/Replace' => 'เพิ่มเติม/แทนที่', + 'Append' => 'เพิ่มเติม', + 'Replace' => 'แทนที่', + 'Import' => 'นำเข้า', + 'change sorting' => 'เปลี่ยนการเรียง', + 'Tasks Importation' => 'การนำเข้างาน', + 'Delimiter' => 'คั่น', + 'Enclosure' => 'กำหนดข้อความ', + 'CSV File' => 'ไฟล์ CSV', + 'Instructions' => 'คำสั่ง', + 'Your file must use the predefined CSV format' => 'ไฟล์ของคุณจะต้องใช้รูปแบบ CSV ที่กำหนดไว้ล่วงหน้า', + 'Your file must be encoded in UTF-8' => 'ไฟล์ของคุณต้องเอนโค้ดด้วย UTF-8', + 'The first row must be the header' => 'แถวแรกต้องเป็นหัวข้อ', + 'Duplicates are not verified for you' => 'รายการที่ซ้ำกันจะไม่ได้รับการตรวจสอบสำหรับคุณ', + 'The due date must use the ISO format: YYYY-MM-DD' => 'วันที่ต้องอยู่ในรูปแบบ ISO: YYYY-MM-DD', + 'Download CSV template' => 'ดาวน์โหลด CSV ต้นฉบับ', + 'No external integration registered.' => 'ไม่มีการรวมภายนอกถูกลงทะเบียนไว้', + 'Duplicates are not imported' => 'ซ้ำกันไม่สามารถนำเข้าได้', + 'Usernames must be lowercase and unique' => 'ชื่อผู้ใช้ต้องเป็นตัวพิมพ์เล็กและไม่ซ้ำ', // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', + '%s attached a new file to the task %s' => '%s แนบไฟล์ใหม่ในงาน %s', + 'Assign automatically a category based on a link' => 'กำหนดหมวดอัตโนมัติตามลิงค์', // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', + 'Assignee Username' => 'กำหนดชื่อผู้ใช้', + 'Assignee Name' => 'กำหนดชื่อ', + 'Groups' => 'กลุ่ม', + 'Members of %s' => 'สมาชิกของ %s', + 'New group' => 'กลุ่มใหม่', + 'Group created successfully.' => 'สร้างกลุ่มสำเร็จ', + 'Unable to create your group.' => 'ไม่สามารถสร้างกลุ่มของคุณ', + 'Edit group' => 'แก้ไขกลุ่ม', + 'Group updated successfully.' => 'แก้ไขกลุ่มเรียบร้อย', + 'Unable to update your group.' => 'ไม่สามารถแก้ไขกลุ่มของคุณ', + 'Add group member to "%s"' => 'เพิ่มสมาชิกในกลุ่ม %s', + 'Group member added successfully.' => 'เพิ่มสมาชิกกลุ่มเรียบร้อย', + 'Unable to add group member.' => 'ไม่สามารถเพิ่มสมาชิกกลุ่ม', + 'Remove user from group "%s"' => 'เอาผู้ใช้ออกจากกลุ่ม %s', + 'User removed successfully from this group.' => 'เอาผู้ใช้ออกจากกลุ่มนี้เรียบร้อย', + 'Unable to remove this user from the group.' => 'ไม่สามารถลบผู้ใช้ออกจากกลุ่มนี้', + 'Remove group' => 'ลบกลุ่ม', + 'Group removed successfully.' => 'ลบกลุ่มเรียบร้อย', + 'Unable to remove this group.' => 'ไม่สามารถลบกลุ่มนี้', + 'Project Permissions' => 'การอนุญาตใช้งานโปรเจค', + 'Manager' => 'ผู้จัดการ', + 'Project Manager' => 'ผู้จัดการโปรเจค', + 'Project Member' => 'สมาชิกโปรเจค', + 'Project Viewer' => 'ผู้ดูโปรเจค', + 'Your account is locked for %d minutes' => 'บัญชีของคุณถูกล็อก %d นาที', + 'Invalid captcha' => 'captcha ไม่ถูกต้อง', + 'The name must be unique' => 'ชื่อต้องไม่ซ้ำ', + 'View all groups' => 'แสดงกลุ่มทั้งหมด', + 'View group members' => 'แสดงสมาชิกกลุ่ม', // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - // 'Project members' => '', - // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'คุณต้องการลบผู้ใช้ "%s" ออกจากกลุ่ม "%s"?', + 'There is no group.' => 'ไม่มีกลุ่ม', + 'External Id' => 'ไอดีภายนอก', + 'Add group member' => 'เพิ่มสมาชิกกลุ่ม', + 'Do you really want to remove this group: "%s"?' => 'คุณต้องการลบกลุ่มนี้: "%s"?', + 'There is no user in this group.' => 'ไม่มีผู้ใช้ในกลุ่มนี้', + 'Remove this user' => 'เอาผู้ใช้คนนี้ออก', + 'Permissions' => 'การอนุญาตใช้งาน', + 'Allowed Users' => 'การอนุญาตผู้ใช้', + 'No user have been allowed specifically.' => 'ไม่มีผู้ใช้ได้รับอนุญาติเป็นพิเศษ', + 'Role' => 'บทบาท', + 'Enter user name...' => 'พิมพ์ชื่อผู้ใช้...', + 'Allowed Groups' => 'อนุญาตกลุ่ม', + 'No group have been allowed specifically.' => 'ไม่มีกลุ่มได้รับอนุญาติเป็นพิเศษ', + 'Group' => 'กลุ่ม', + 'Group Name' => 'ชื่อกลุ่ม', + 'Enter group name...' => 'พิมพ์ชื่อกลุ่ม...', + 'Role:' => 'บทบาท:', + 'Project members' => 'สมาชิกโปรเจค', + 'Compare hours for "%s"' => 'เปรียบเทียบรายชั่วโมงสำหรับ %s', + '%s mentioned you in the task #%d' => '%s กล่าวถึงคุณในงาน #%d', + '%s mentioned you in a comment on the task #%d' => '%s กล่าวถึงคุณในความคิดเห็นของงาน #%d', + 'You were mentioned in the task #%d' => 'คุณได้รับการกล่าวถึงในงาน #%d', + 'You were mentioned in a comment on the task #%d' => 'คุณได้รับการกล่าวถึงในความคิดเห็นของงาน #%d', + 'Mentioned' => 'กล่าวถึง', + 'Compare Estimated Time vs Actual Time' => 'เปรียบเทียบเวลาโดยประมาณกับเวลาที่เกิดขึ้นจริง', + 'Estimated hours: ' => 'เวลาโดยประมาณ:', + 'Actual hours: ' => 'เวลาที่เกิดขึ้นจริง:', + 'Hours Spent' => 'เวลาที่ใช้', + 'Hours Estimated' => 'เวลาโดยประมาณ', + 'Estimated Time' => 'เวลาโดยประมาณ', + 'Actual Time' => 'เวลาที่ใช้', + 'Estimated vs actual time' => 'เวลาโดยประมาณกับเวลาจริง', + 'RUB - Russian Ruble' => 'RUB - รูเบิลรัสเซีย', + 'Assign the task to the person who does the action when the column is changed' => 'กำหนดผู้รับผิดชอบงานเมื่อเปลี่ยนคอลัมน์', + 'Close a task in a specific column' => 'ปิดงานในคอลัมน์ที่เฉพาะเจาะจง', // 'Time-based One-time Password Algorithm' => '', // 'Two-Factor Provider: ' => '', // 'Disable two-factor authentication' => '', // 'Enable two-factor authentication' => '', // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', - // 'The password has never been reinitialized.' => '', - // 'Creation' => '', - // 'Expiration' => '', - // 'Password reset history' => '', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + 'Password Reset for Kanboard' => 'รีเซตรหัสผ่านสำหรับคังบอร์ด', + 'Forgot password?' => 'ลืมรหัสผ่าน?', + 'Enable "Forget Password"' => 'เปิดการใช้งาน "ลืมรหัสผ่าน"', + 'Password Reset' => 'รีเซตรหัสผ่าน', + 'New password' => 'รหัสผ่านใหม่', + 'Change Password' => 'เปลี่ยนรหัสผ่าน', + 'To reset your password click on this link:' => 'ในการรีเซตรหัสผ่านของคุณคลิ๊กที่ลิงค์นี้:', + 'Last Password Reset' => 'รีเซตรหัสผ่านครั้งล่าสุด', + 'The password has never been reinitialized.' => 'รหัสผ่านไม่เคยเริ่มใหม่อีกครั้ง', + 'Creation' => 'สร้าง', + 'Expiration' => 'สิ้นสุด', + 'Password reset history' => 'ประวัติการรีเซตรหัสผ่าน', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'ทุกงานของคอลัมน์ "%s" และสวิมเลน "%s" ถูกปิดเรียบร้อย', + 'Do you really want to close all tasks of this column?' => 'คุณต้องการปิดทุกงานในคอลัมนี้ใช่หรือไม่?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d งานในคอลัมน์ "%s" และสวิมเลน "%s" จะปิด', + 'Close all tasks of this column' => 'ปิดทุกงานในคอลัมน์นี้', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'ปลั๊กอินไม่ได้ลงทะเบียนการแจ้งเตือนในโปรเจค คุณยังสามารถกำหนดค่าการแจ้งเตือนรายบุคคลในโปรไฟล์ผู้ใช้ของคุณ', + 'My dashboard' => 'แดชบอร์ดของฉํน', + 'My profile' => 'โปรเจคของฉัน', + 'Project owner: ' => 'เจ้าของโปรเจค: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'ตัวบ่งชี้โปรโจคเป็นตัวเลือกเสริมและต้องเป็นตัวอักษรหรือตัวเลข ตัวอย่าง: MYPROJECT', + 'Project owner' => 'เจ้าของโปรเจค', + 'Those dates are useful for the project Gantt chart.' => 'วันที่ใช้สำหรับแผนภูมิแกรนท์ของโปรเจค', + 'Private projects do not have users and groups management.' => 'โปรเจคส่วนตัวไม่มีการจัดการผู้ใช้และกลุ่ม', + 'There is no project member.' => 'ไม่มีสมาชิกโปรเจค', + 'Priority' => 'ความสำคัญ', + 'Task priority' => 'ความสำคัญของงาน', + 'General' => 'ทั่วไป', + 'Dates' => 'วันที่', + 'Default priority' => 'ความสำคัญเริ่มต้น', + 'Lowest priority' => 'ความสำคัญต่ำสุด', + 'Highest priority' => 'ความสำคัญสูงสุด', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'ถ้าคุณใส่เลขศูนย์ทั้งความสำคัญต่ำและความสำคัญสูง จะเป็นการปิดการทำงานคุณลักษณะนี้', + 'Close a task when there is no activity' => 'ปิดงานเมื่อไม่มีกิจกกรมเกิดขึ้น', + 'Duration in days' => 'ระยะเวลาวันที่', + 'Send email when there is no activity on a task' => 'ส่งอีเมลเมื่อไม่มีกิจกรรมเกิดขึ้นในงาน', + 'List of external links' => 'รายการเชื่อมโยงภายนอก', + 'Unable to fetch link information.' => 'ไม่สามารถดึงข้อมูลการเชื่อมโยง', + // 'Daily background job for tasks' => '', + 'Auto' => 'อัตโนมัติ', + 'Related' => 'ที่เกี่ยวข้อง', + 'Attachment' => 'แนบ', + 'Title not found' => 'ไม่พบหัวเรื่อง', + 'Web Link' => 'เวบลิงค์', + 'External links' => 'เชื่อมโยงภายนอก', + 'Add external link' => 'เพิ่มการเชื่อมโยงภายนอก', + 'Type' => 'ประเภท', + 'Dependency' => 'ขึ้นอยู่กับ', + 'Add internal link' => 'เพิ่มการเชื่อมโยงภายใน', + 'Add a new external link' => 'เพิ่มการเชื่อมโยงภายนอกใหม่', + 'Edit external link' => 'แก้ไขการเชื่อมโยงภายนอก', + 'External link' => 'เชื่อมโยงภายนอก', + 'Copy and paste your link here...' => 'คัดลอกและวางลิงค์ของคุณที่นี้...', + 'URL' => 'URL', + 'There is no external link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายนอก', + 'Internal links' => 'เชื่อมโยงภายใน', + 'There is no internal link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายใน', + 'Assign to me' => 'ฉันรับผิดชอบ', + 'Me' => 'ฉัน', + // 'Do not duplicate anything' => '', + 'Projects management' => 'การจัดการโปรเจค', + 'Users management' => 'การจัดการผู้ใช้', + 'Groups management' => 'การจัดการกลุ่ม', + 'Create from another project' => 'สร้างโปรเจคอื่น', + 'There is no subtask at the moment.' => 'ขณะนี้ไม่มีงานย่อย', + 'open' => 'เปิด', + 'closed' => 'ปิด', + 'Priority:' => 'ความสำคัญ:', + 'Reference:' => 'อ้างถึง:', + 'Complexity:' => 'ความซับซ้อน:', + 'Swimlane:' => 'สวิมเลน:', + 'Column:' => 'คอลัมน์:', + 'Position:' => 'ตำแหน่ง:', + 'Creator:' => 'ผู้สร้าง:', + 'Time estimated:' => 'เวลาเฉลี่ย:', + '%s hours' => '%s ชั่วโมง', + 'Time spent:' => 'ใช้เวลา:', + 'Created:' => 'สร้าง:', + 'Modified:' => 'แก้ไข:', + 'Completed:' => 'เสร็จสิ้น:', + 'Started:' => 'เริ่ม:', + 'Moved:' => 'ย้าย:', + 'Task #%d' => 'งานที่ #%d', + 'Sub-tasks' => 'งานย่อย', + 'Date and time format' => 'รูปแบบของวันเวลา', + 'Time format' => 'รูปแบบของเวลา', + 'Start date: ' => 'เริ่มวันที่:', + 'End date: ' => 'จบวันที่:', + 'New due date: ' => 'วันครบกำหนดใหม่', + 'Start date changed: ' => 'เปลี่ยนวันที่เริ่ม', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index c4da560d..d1c1b6d3 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => 'Düzenle', 'remove' => 'sil', 'Remove' => 'Sil', - 'Update' => 'Güncelle', 'Yes' => 'Evet', 'No' => 'Hayır', 'cancel' => 'İptal', @@ -60,7 +59,6 @@ return array( 'Actions' => 'İşlemler', 'Inactive' => 'Aktif değil', 'Active' => 'Aktif', - 'Add this column' => 'Bu sütunu ekle', '%d tasks on the board' => '%d görev bu tabloda', '%d tasks in total' => '%d görev toplam', 'Unable to update this board.' => 'Bu tablo güncellenemiyor.', @@ -72,7 +70,6 @@ return array( 'Remove project' => 'Projeyi sil', 'Edit the board for "%s"' => 'Tabloyu "%s" için güncelle', 'All projects' => 'Tüm projeler', - 'Change columns' => 'Sütunları değiştir', 'Add a new column' => 'Yeni sütun ekle', 'Title' => 'Başlık', 'Nobody assigned' => 'Kullanıcı atanmamış', @@ -102,11 +99,8 @@ return array( 'Open a task' => 'Bir görevi aç', 'Do you really want to open this task: "%s"?' => 'Bu görevi gerçekten açmak istiyor musunuz: "%s"?', 'Back to the board' => 'Tabloya dön', - 'Created on %B %e, %Y at %k:%M %p' => '%B %e, %Y, saat %k:%M %p da oluşturuldu', 'There is nobody assigned' => 'Kimse atanmamış', 'Column on the board:' => 'Tablodaki sütun:', - 'Status is open' => 'Açık durumda', - 'Status is closed' => 'Kapalı durumda', 'Close this task' => 'Görevi kapat', 'Open this task' => 'Görevi aç', 'There is no description.' => 'Açıklama yok.', @@ -158,10 +152,6 @@ return array( 'Work in progress' => 'İşlemde', 'Done' => 'Tamamlandı', 'Application version:' => 'Uygulama versiyonu:', - 'Completed on %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p da tamamlandı', - '%B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p', - 'Date created' => 'Oluşturulma tarihi', - 'Date completed' => 'Tamamlanma tarihi', 'Id' => 'Kod', '%d closed tasks' => '%d kapatılmış görevler', 'No task for this project' => 'Bu proje için görev yok', @@ -185,9 +175,6 @@ return array( 'Edit this task' => 'Bu görevi değiştir', 'Due Date' => 'Bitiş Tarihi', 'Invalid date' => 'Geçersiz tarihi', - 'Must be done before %B %e, %Y' => '%B %e, %Y tarihinden önce yapılmalı', - '%B %e, %Y' => '%d %B %Y', - '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Otomatik işlemler', 'Your automatic action have been created successfully.' => 'Otomatik işlem başarıyla oluşturuldu', 'Unable to create your automatic action.' => 'Otomatik işleminiz oluşturulamadı', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => 'Bu işlem silinemedi', 'Action removed successfully.' => 'İşlem başarıyla silindi', 'Automatic actions for the project "%s"' => '"%s" projesi için otomatik işlemler', - 'Defined actions' => 'Tanımlanan işlemler', 'Add an action' => 'İşlem ekle', 'Event name' => 'Durum adı', 'Action name' => 'İşlem adı', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => 'Seçilen durum oluştuğunda ilgili eylemi gerçekleştir.', 'Next step' => 'Sonraki adım', 'Define action parameters' => 'İşlem parametrelerini düzenle', - 'Save this action' => 'Bu işlemi kaydet', 'Do you really want to remove this action: "%s"?' => 'Bu işlemi silmek istediğinize emin misiniz: "%s"?', 'Remove an automatic action' => 'Bir otomatik işlemi sil', 'Assign the task to a specific user' => 'Görevi bir kullanıcıya ata', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => 'Bir kullanıcıya renk tanımla', 'Column title' => 'Sütun başlığı', 'Position' => 'Pozisyon', - 'Move Up' => 'Yukarı taşı', - 'Move Down' => 'Aşağı taşı', 'Duplicate to another project' => 'Başka bir projeye kopyala', 'Duplicate' => 'Kopya oluştur', 'link' => 'bağlantı', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => 'Yorum silindi.', 'Unable to remove this comment.' => 'Bu yorum silinemiyor.', 'Do you really want to remove this comment?' => 'Bu yorumu silmek istediğinize emin misiniz?', - 'Only administrators or the creator of the comment can access to this page.' => 'Bu sayfaya yalnızca yorum sahibi ve yöneticiler erişebilir.', 'Current password for the user "%s"' => 'Kullanıcı için mevcut şifre "%s"', 'The current password is required' => 'Mevcut şifre gerekli', 'Wrong password' => 'Yanlış şifre', @@ -260,10 +242,6 @@ return array( 'External authentication failed' => 'Harici hesap doğrulaması başarısız', 'Your external account is linked to your profile successfully.' => 'Harici hesabınız profilinizle başarıyla bağlandı.', 'Email' => 'E-posta', - 'Link my Google Account' => 'Google hesabımla bağ oluştur', - 'Unlink my Google Account' => 'Google hesabımla bağı kaldır', - 'Login with my Google Account' => 'Google hesabımla giriş yap', - 'Project not found.' => 'Proje bulunamadı', 'Task removed successfully.' => 'Görev başarıyla silindi.', 'Unable to remove this task.' => 'Görev silinemiyor.', 'Remove a task' => 'Bir görevi sil', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => 'Maksimum boyutu', 'Unable to upload the file.' => 'Dosya yüklenemiyor.', 'Display another project' => 'Başka bir proje göster', - 'Login with my Github Account' => 'Github hesabımla giriş yap', - 'Link my Github Account' => 'Github hesabını ilişkilendir', - 'Unlink my Github Account' => 'Github hesabıyla bağlantıyı kopar', 'Created by %s' => '%s tarafından oluşturuldu', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Son değişiklik tarihi %d.%m.%Y, saati %H:%M', 'Tasks Export' => 'Görevleri dışa aktar', 'Tasks exportation for "%s"' => '"%s" için görevleri dışa aktar', 'Start Date' => 'Başlangıç tarihi', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => 'Yalnızca bu projelerle ilgili bildirim almak istiyorum:', 'view the task on Kanboard' => 'bu görevi Kanboard üzerinde göster', 'Public access' => 'Dışa açık erişim', - 'User management' => 'Kullanıcı yönetimi', 'Active tasks' => 'Aktif görevler', 'Disable public access' => 'Dışa açık erişimi kapat', 'Enable public access' => 'Dışa açık erişimi aç', @@ -395,11 +368,7 @@ return array( 'Change password' => 'Şifre değiştir', 'Password modification' => 'Şifre değişimi', 'External authentications' => 'Dış kimlik doğrulamaları', - 'Google Account' => 'Google hesabı', - 'Github Account' => 'Github hesabı', 'Never connected.' => 'Hiç bağlanmamış.', - 'No account linked.' => 'Bağlanmış hesap yok.', - 'Account linked.' => 'Hesap bağlandı.', 'No external authentication enabled.' => 'Dış kimlik doğrulama kapalı.', 'Password modified successfully.' => 'Şifre başarıyla değiştirildi.', 'Unable to change the password.' => 'Şifre değiştirilemiyor.', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => 'Dış kaynaklı kullanıcı adı ile göreve atananı değiştir', 'Change the category based on an external label' => 'Dış kaynaklı bir etiket ile kategori değiştir', 'Reference' => 'Referans', - 'Reference: %s' => 'Referans: %s', 'Label' => 'Etiket', 'Database' => 'Veritabanı', 'About' => 'Hakkında', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => 'Saniye olarak frekans (varsayılan 60 saniye)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Saniye olarak frekans (Bu özelliği iptal etmek için 0, varsayılan değer 10 saniye)', 'Application URL' => 'Uygulama URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Örneğin: http://example.kanboard.net/ (E-posta bildirimleri için kullanılıyor)', 'Token regenerated.' => 'Beliteç yeniden oluşturuldu.', 'Date format' => 'Tarih formatı', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"', @@ -467,9 +434,6 @@ return array( 'This project is private' => 'Bu proje özel', 'Type here to create a new sub-task' => 'Yeni bir alt görev oluşturmak için buraya yazın', 'Add' => 'Ekle', - 'Estimated time: %s hours' => 'Tahmini süre: %s Saat', - 'Time spent: %s hours' => 'Kullanılan süre: %s Saat', - 'Started on %B %e, %Y' => '%B %e %Y tarihinde başlatıldı', 'Start date' => 'Başlangıç tarihi', 'Time estimated' => 'Tahmini süre', 'There is nothing assigned to you.' => 'Size atanan hiçbir şey yok.', @@ -529,7 +493,6 @@ return array( 'Do you really want to remove this swimlane: "%s"?' => 'Bu Kulvarı silmek istediğinize emin misiniz?: "%s"?', 'Inactive swimlanes' => 'Pasif Kulvarlar', 'Remove a swimlane' => 'Kulvarı sil', - 'Rename' => 'Yeniden adlandır', 'Show default swimlane' => 'Varsayılan Kulvarı göster', 'Swimlane modification for the project "%s"' => '"%s" Projesi için Kulvar değişikliği', 'Swimlane not found.' => 'Kulvar bulunamadı', @@ -537,7 +500,6 @@ return array( 'Swimlanes' => 'Kulvarlar', 'Swimlane updated successfully.' => 'Kulvar başarıyla güncellendi.', 'The default swimlane have been updated successfully.' => 'Varsayılan Kulvarlar başarıyla güncellendi.', - 'Unable to create your swimlane.' => 'Bu Kulvarı oluşturmak mümkün değil.', 'Unable to remove this swimlane.' => 'Bu Kulvarı silmek mümkün değil.', 'Unable to update this swimlane.' => 'Bu Kulvarı değiştirmek mümkün değil.', 'Your swimlane have been created successfully.' => 'Kulvar başarıyla oluşturuldu.', @@ -612,7 +574,6 @@ return array( 'This task' => 'Bu görev', '<1h' => '<1s', '%dh' => '%ds', - // '%b %e' => '', 'Expand tasks' => 'Görevleri genişlet', 'Collapse tasks' => 'Görevleri daralt', 'Expand/collapse tasks' => 'Görevleri genişlet/daralt', @@ -622,14 +583,11 @@ return array( 'Keyboard shortcuts' => 'Klavye kısayolları', 'Open board switcher' => 'Tablo seçim listesini aç', 'Application' => 'Uygulama', - 'since %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p\'den beri', 'Compact view' => 'Ekrana sığdır', 'Horizontal scrolling' => 'Geniş görünüm', 'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm', 'No results match:' => 'Uygun sonuç bulunamadı', 'Currency' => 'Para birimi', - 'Files' => 'Dosyalar', - 'Images' => 'Resimler', 'Private project' => 'Özel proje', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', @@ -675,9 +633,6 @@ return array( 'Test your device' => 'Cihazınızı test edin', 'Assign a color when the task is moved to a specific column' => 'Görev belirli bir sütuna taşındığında rengini değiştir', '%s via Kanboard' => '%s Kanboard ile', - 'uploaded by: %s' => '%s tarafından yüklendi', - 'uploaded on: %s' => '%s tarihinda yüklendi', - 'size: %s' => 'Boyut: %s', 'Burndown chart for "%s"' => '%s icin kalan iş grafiği', 'Burndown chart' => 'Kalan iş grafiği', 'This chart show the task complexity over the time (Work Remaining).' => 'Bu grafik zorluk seviyesini zamana göre gösterir (kalan iş)', @@ -694,7 +649,6 @@ return array( 'A task cannot be linked to itself' => 'Bir görevden kendine link atanamaz', 'The exact same link already exists' => 'Birebir aynı link zaten var', 'Recurrent task is scheduled to be generated' => 'Tekrarlanan görev oluşturulması zamanlandı', - 'Recurring information' => 'Tekrarlanan bilgi', 'Score' => 'Skor', 'The identifier must be unique' => 'Kimlik özgün olmalı', 'This linked task id doesn\'t exists' => 'Link oluşturulan görec kodu mevcut değil', @@ -776,19 +730,13 @@ return array( 'Time spent changed: %sh' => 'Harcanan zaman değiştirildi: %sh', 'Time estimated changed: %sh' => 'Tahmini süre değiştirildi: %sh', 'The field "%s" have been updated' => '"%s" hanesi değiştirildi', - 'The description have been modified' => 'Açıklama değiştirildi', + 'The description has been modified' => 'Açıklama değiştirildi', 'Do you really want to close the task "%s" as well as all subtasks?' => '"%s" görevini ve tüm alt görevlerini kapatmak istediğinize emin misiniz?', - 'Swimlane: %s' => 'Kulvar: %s', 'I want to receive notifications for:' => 'Bununla ilgili bildirimler almak istiyorum:', 'All tasks' => 'Tüm görevler', 'Only for tasks assigned to me' => 'Yalnızca bana atanmış görevler için', 'Only for tasks created by me' => 'Yalnızca benim oluşturduğum görevler için', 'Only for tasks created by me and assigned to me' => 'Yalnızca benim oluşturduğum ve bana atanmış görevler için', - '%A' => '%A', - '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', - 'New due date: %B %e, %Y' => 'Yeni tamamlanma tarihi: %B %e, %Y', - 'Start date changed: %B %e, %Y' => 'Başlangıç tarihi değiştirildi: %B %e, %Y', - '%k:%M %p' => '%k:%M %p', '%%Y-%%m-%%d' => '%%Y-%%m-%%d', 'Total for all columns' => 'Tüm sütunlar için toplam', 'You need at least 2 days of data to show the chart.' => 'Grafiği göstermek için en az iki günlük veriye ihtiyaç var.', @@ -813,7 +761,6 @@ return array( 'Not assigned' => 'Atanmamış', 'View advanced search syntax' => 'Gelişmiş arama kodlarını göster', 'Overview' => 'Genel bakış', - '%b %e %Y' => '%b %e %Y', 'Board/Calendar/List view' => 'Tablo/Takvim/Liste görünümü', 'Switch to the board view' => 'Tablo görünümüne geç', 'Switch to the calendar view' => 'Takvim görünümüne geç', @@ -846,10 +793,6 @@ return array( 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Bu grafik son %d görev için zaman içinde gerçekleşen ortalama teslim ve çevrim sürelerini gösterir.', 'Average time into each column' => 'Her bir sütunda ortalama zaman', 'Lead and cycle time' => 'Teslim ve çevrim süresi', - 'Google Authentication' => 'Google doğrulaması', - 'Help on Google authentication' => 'Google doğrulaması hakkında yardım', - 'Github Authentication' => 'Github doğrulaması', - 'Help on Github authentication' => 'Github doğrulaması hakkında yardım', 'Lead time: ' => 'Teslim süresi: ', 'Cycle time: ' => 'Çevrim süresi: ', 'Time spent into each column' => 'Her sütunda harcanan zaman', @@ -858,16 +801,12 @@ return array( 'If the task is not closed the current time is used instead of the completion date.' => 'Eğer görev henüz kapatılmamışsa, tamamlanma tarihi yerine şu anki tarih kullanılır.', 'Set automatically the start date' => 'Başlangıç tarihini otomatik olarak belirle', 'Edit Authentication' => 'Doğrulamayı düzenle', - 'Google Id' => 'Google kimliği', - 'Github Id' => 'Github Kimliği', 'Remote user' => 'Uzak kullanıcı', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Uzak kullanıcıların şifreleri Kanboard veritabanında saklanmaz, örnek: LDAP, Google ve Github hesapları', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Eğer giriş formuna erişimi engelleyi seçerseniz, giriş formuna girilen bilgiler gözardı edilir.', 'New remote user' => 'Yeni uzak kullanıcı', 'New local user' => 'Yeni yerel kullanıcı', 'Default task color' => 'Varsayılan görev rengi', - 'Hide sidebar' => 'Yan menüyü gizle', - 'Expand sidebar' => 'Yan menüyü genişlet', 'This feature does not work with all browsers.' => 'Bu özellik tüm tarayıcılarla çalışmaz', 'There is no destination project available.' => 'Seçilebilecek hedef proje yok.', 'Trigger automatically subtask time tracking' => 'Altgörev zamanlayıcıyı otomatik olarak başlat', @@ -901,7 +840,6 @@ return array( 'open file' => 'dosyayı aç', 'End date' => 'Bitiş tarihi', 'Users overview' => 'Kullanıcılara genel bakış', - 'Managers' => 'Yöneticiler', 'Members' => 'Üyeler', 'Shared project' => 'Paylaşılan proje', 'Project managers' => 'Proje yöneticileri', @@ -912,19 +850,10 @@ return array( 'End date:' => 'Bitiş tarihi:', 'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.', 'Projects Gantt chart' => 'Projeler Gantt diyagramı', - 'Start date: %s' => 'Başlangıç tarihi: %s', - 'End date: %s' => 'Bitiş tarihi: %s', 'Link type' => 'Bağlantı türü', 'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir', 'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi', - 'Login with my Gitlab Account' => 'Gitlab hesabımla giriş yap', 'Milestone' => 'Kilometre taşı', - 'Gitlab Authentication' => 'Gitlab doğrulaması', - 'Help on Gitlab authentication' => 'Gitlab doğrulaması hakkında yardım', - 'Gitlab Id' => 'Gitlab kimliği', - 'Gitlab Account' => 'Gitlab hesabı', - 'Link my Gitlab Account' => 'Gitlab hesabını ilişkilendir', - 'Unlink my Gitlab Account' => 'Gitlab hesabımla bağlantıyı kopar', 'Documentation: %s' => 'Dokümantasyon: %s', 'Switch to the Gantt chart view' => 'Gantt diyagramı görünümüne geç', 'Reset the search/filter box' => 'Arama/Filtre kutusunu sıfırla', @@ -1117,5 +1046,109 @@ return array( // 'Lowest priority' => '', // 'Highest priority' => '', // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'List of external links' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'There is no external link for the moment.' => '', + // 'Internal links' => '', + // 'There is no internal link for the moment.' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'There is no subtask at the moment.' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Sub-tasks' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'There is no attachment at the moment.' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index e0b90b58..816269b2 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -8,7 +8,6 @@ return array( 'Edit' => '编辑', 'remove' => '移除', 'Remove' => '移除', - 'Update' => '更新', 'Yes' => '是', 'No' => '否', 'cancel' => '取消', @@ -20,15 +19,15 @@ return array( 'Red' => '红色', 'Orange' => '橘色', 'Grey' => '灰色', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => '褐色', + 'Deep Orange' => '橘红色', + 'Dark Grey' => '深灰色', + 'Pink' => '粉红色', + 'Teal' => '青色', + 'Cyan' => '蓝绿色', + 'Lime' => '绿黄色', + 'Light Green' => '浅绿色', + 'Amber' => '黄褐色', 'Save' => '保存', 'Login' => '登录', 'Official website:' => '官方网站:', @@ -40,7 +39,7 @@ return array( 'All users' => '所有用户', 'Username' => '用户名', 'Password' => '密码', - 'Administrator' => '管理员', + 'Administrator' => '超级管理员', 'Sign in' => '登录', 'Users' => '用户', 'No user' => '没有用户', @@ -60,7 +59,6 @@ return array( 'Actions' => '动作', 'Inactive' => '未激活', 'Active' => '激活', - 'Add this column' => '加入该栏目', '%d tasks on the board' => '看板目前有%d个任务', '%d tasks in total' => '总共有%d个任务', 'Unable to update this board.' => '无法更新该看板。', @@ -72,7 +70,6 @@ return array( 'Remove project' => '移除项目', 'Edit the board for "%s"' => '为"%s"修改看板', 'All projects' => '所有项目', - 'Change columns' => '更改栏目', 'Add a new column' => '添加新栏目', 'Title' => '标题', 'Nobody assigned' => '无人被指派', @@ -102,14 +99,11 @@ return array( 'Open a task' => '开一个任务', 'Do you really want to open this task: "%s"?' => '你确定要开这个任务吗?"%s"', 'Back to the board' => '回到看板', - 'Created on %B %e, %Y at %k:%M %p' => '创建时间:%Y/%m/%d %H:%M', - 'There is nobody assigned' => '无人负责', + 'There is nobody assigned' => '当前无人被指派', 'Column on the board:' => '看板上的栏目:', - 'Status is open' => '开启状态', - 'Status is closed' => '关闭状态', 'Close this task' => '关闭该任务', 'Open this task' => '开启该任务', - 'There is no description.' => '无描述。', + 'There is no description.' => '当前没有描述。', 'Add a new task' => '添加新任务', 'The username is required' => '需要用户名', 'The maximum length is %d characters' => '最长%d个英文字符', @@ -158,10 +152,6 @@ return array( 'Work in progress' => '工作进行中', 'Done' => '完成', 'Application version:' => '应用程序版本:', - 'Completed on %B %e, %Y at %k:%M %p' => '于%Y/%m/%d %H:%M 完成', - '%B %e, %Y at %k:%M %p' => '%Y/%m/%d %H:%M', - 'Date created' => '创建时间', - 'Date completed' => '完成时间', 'Id' => '编号', '%d closed tasks' => '%d个已关闭任务', 'No task for this project' => '该项目尚无任务', @@ -185,9 +175,6 @@ return array( 'Edit this task' => '编辑该任务', 'Due Date' => '到期时间', 'Invalid date' => '无效日期', - 'Must be done before %B %e, %Y' => '必须在%Y/%m/%d前完成', - '%B %e, %Y' => '%Y/%m/%d', - // '%b %e, %Y' => '', 'Automatic actions' => '自动动作', 'Your automatic action have been created successfully.' => '您的自动动作已成功创建', 'Unable to create your automatic action.' => '无法为您创建自动动作。', @@ -195,7 +182,6 @@ return array( 'Unable to remove this action.' => '无法移除该动作', 'Action removed successfully.' => '成功移除动作。', 'Automatic actions for the project "%s"' => '项目"%s"的自动动作', - 'Defined actions' => '已定义的动作', 'Add an action' => '添加动作', 'Event name' => '事件名称', 'Action name' => '动作名称', @@ -205,7 +191,6 @@ return array( 'When the selected event occurs execute the corresponding action.' => '当所选事件发生时执行相应动作。', 'Next step' => '下一步', 'Define action parameters' => '定义动作参数', - 'Save this action' => '保存该动作', 'Do you really want to remove this action: "%s"?' => '确定要移除动作"%s"吗?', 'Remove an automatic action' => '移除一个自动动作', 'Assign the task to a specific user' => '将该任务指派给一个用户', @@ -218,8 +203,6 @@ return array( 'Assign a color to a specific user' => '为特定用户指派颜色', 'Column title' => '栏目名称', 'Position' => '位置', - 'Move Up' => '往上移', - 'Move Down' => '往下移', 'Duplicate to another project' => '复制到另一项目', 'Duplicate' => '复制', 'link' => '连接', @@ -229,7 +212,6 @@ return array( 'Comment removed successfully.' => '评论成功移除。', 'Unable to remove this comment.' => '无法移除该评论。', 'Do you really want to remove this comment?' => '确定要移除评论吗?', - 'Only administrators or the creator of the comment can access to this page.' => '只有管理员或评论创建者可以进入该页面。', 'Current password for the user "%s"' => '用户"%s"的当前密码', 'The current password is required' => '需要输入当前密码', 'Wrong password' => '密码错误', @@ -241,7 +223,7 @@ return array( 'User agent' => '浏览器标识', 'Persistent connections' => '持续连接', 'No session.' => '无会话', - 'Expiration date' => '过期', + 'Expiration date' => '过期日期', 'Remember Me' => '记住我', 'Creation date' => '创建日期', 'Everybody' => '所有人', @@ -255,15 +237,11 @@ return array( '%d comments' => '%d个评论', '%d comment' => '%d个评论', 'Email address invalid' => '电子邮件地址无效', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', + 'Your external account is not linked anymore to your profile.' => '你的外部账户关联已解除', + 'Unable to unlink your external account.' => '无法关联到你的外部账户', + 'External authentication failed' => '外部认证失败', + 'Your external account is linked to your profile successfully.' => '你已成功关联到你的外部账户', 'Email' => '电子邮件', - 'Link my Google Account' => '关联我的google帐号', - 'Unlink my Google Account' => '去除我的google帐号关联', - 'Login with my Google Account' => '用我的google帐号登录', - 'Project not found.' => '未发现项目', 'Task removed successfully.' => '任务成功去除', 'Unable to remove this task.' => '无法移除该任务。', 'Remove a task' => '移除一个任务', @@ -327,11 +305,7 @@ return array( 'Maximum size: ' => '大小上限:', 'Unable to upload the file.' => '无法上传文件', 'Display another project' => '显示其它项目', - 'Login with my Github Account' => '用Github账号登录', - 'Link my Github Account' => '链接Github账号', - 'Unlink my Github Account' => '取消Github账号链接', 'Created by %s' => '创建者:%s', - 'Last modified on %B %e, %Y at %k:%M %p' => '最后修改:%Y/%m/%d/ %H:%M', 'Tasks Export' => '任务导出', 'Tasks exportation for "%s"' => '导出"%s"的任务', 'Start Date' => '开始时间', @@ -367,7 +341,6 @@ return array( 'I want to receive notifications only for those projects:' => '我仅需要收到下面项目的通知:', 'view the task on Kanboard' => '在看板中查看此任务', 'Public access' => '公开访问', - 'User management' => '用户管理', 'Active tasks' => '活动任务', 'Disable public access' => '停止公开访问', 'Enable public access' => '开启公开访问', @@ -395,11 +368,7 @@ return array( 'Change password' => '修改密码', 'Password modification' => '修改密码', 'External authentications' => '外部认证', - 'Google Account' => '谷歌账号', - 'Github Account' => 'Github 账号', 'Never connected.' => '从未连接。', - 'No account linked.' => '未链接账号。', - 'Account linked.' => '已经链接账号。', 'No external authentication enabled.' => '未启用外部认证。', 'Password modified successfully.' => '已经成功修改密码。', 'Unable to change the password.' => '无法修改密码。', @@ -441,7 +410,6 @@ return array( 'Change the assignee based on an external username' => '根据外部用户名修改任务分配', 'Change the category based on an external label' => '根据外部标签修改分类', 'Reference' => '参考', - 'Reference: %s' => '参考:%s', 'Label' => '标签', 'Database' => '数据库', 'About' => '关于', @@ -459,7 +427,6 @@ return array( 'Frequency in second (60 seconds by default)' => '频率,单位为秒(默认是60秒)', 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '频率,单位为秒(设置为0停用此功能,默认是10秒)', 'Application URL' => '应用URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => '例如:http://example.kanboard.net/ (用于电子邮件通知)', 'Token regenerated.' => '重新生成令牌', 'Date format' => '日期格式', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 格式总是允许的,例如:"%s" 和 "%s"', @@ -467,12 +434,9 @@ return array( 'This project is private' => '此项目为私有项目', 'Type here to create a new sub-task' => '要创建新的子任务,请在此输入', 'Add' => '添加', - 'Estimated time: %s hours' => '预计时间:%s小时', - 'Time spent: %s hours' => '花费时间:%s小时', - 'Started on %B %e, %Y' => '开始时间: %Y/%m/%d %H:%M ', 'Start date' => '启动日期', 'Time estimated' => '预计时间', - 'There is nothing assigned to you.' => '无任务指派给你。', + 'There is nothing assigned to you.' => '当前无任务指派给你。', 'My tasks' => '我的任务', 'Activity stream' => '动态记录', 'Dashboard' => '面板', @@ -522,25 +486,23 @@ return array( 'Nothing to preview...' => '没有需要预览的内容', 'Preview' => '预览', 'Write' => '书写', - 'Active swimlanes' => '活动泳道', - 'Add a new swimlane' => '添加新泳道', - 'Change default swimlane' => '修改默认泳道', - 'Default swimlane' => '默认泳道', - 'Do you really want to remove this swimlane: "%s"?' => '确定要删除泳道:"%s"?', - 'Inactive swimlanes' => '非活动泳道', - 'Remove a swimlane' => '删除泳道', - 'Rename' => '重命名', - 'Show default swimlane' => '显示默认泳道', - 'Swimlane modification for the project "%s"' => '项目"%s"的泳道变更', - 'Swimlane not found.' => '未找到泳道。', - 'Swimlane removed successfully.' => '成功删除泳道', - 'Swimlanes' => '泳道', - 'Swimlane updated successfully.' => '成功更新了泳道。', - 'The default swimlane have been updated successfully.' => '成功更新了默认泳道。', - 'Unable to create your swimlane.' => '无法创建泳道。', - 'Unable to remove this swimlane.' => '无法删除此泳道', - 'Unable to update this swimlane.' => '无法更新此泳道', - 'Your swimlane have been created successfully.' => '已经成功创建泳道。', + 'Active swimlanes' => '活动里程碑', + 'Add a new swimlane' => '添加新里程碑', + 'Change default swimlane' => '修改默认里程碑', + 'Default swimlane' => '默认里程碑', + 'Do you really want to remove this swimlane: "%s"?' => '确定要删除里程碑:"%s"?', + 'Inactive swimlanes' => '非活动里程碑', + 'Remove a swimlane' => '删除里程碑', + 'Show default swimlane' => '显示默认里程碑', + 'Swimlane modification for the project "%s"' => '项目"%s"的里程碑变更', + 'Swimlane not found.' => '未找到里程碑。', + 'Swimlane removed successfully.' => '成功删除里程碑', + 'Swimlanes' => '里程碑', + 'Swimlane updated successfully.' => '成功更新了里程碑。', + 'The default swimlane have been updated successfully.' => '成功更新了默认里程碑。', + 'Unable to remove this swimlane.' => '无法删除此里程碑', + 'Unable to update this swimlane.' => '无法更新此里程碑', + 'Your swimlane have been created successfully.' => '已经成功创建里程碑。', 'Example: "Bug, Feature Request, Improvement"' => '示例:“缺陷,功能需求,提升', 'Default categories for new projects (Comma-separated)' => '新项目的默认分类(用逗号分隔)', 'Integrations' => '整合', @@ -558,7 +520,7 @@ return array( 'Calendar' => '日程表', 'Next' => '前进', '#%d' => '#%d', - 'All swimlanes' => '全部泳道', + 'All swimlanes' => '全部里程碑', 'All colors' => '全部颜色', 'Moved to column %s' => '移动到栏目 %s', 'Change description' => '修改描述', @@ -567,11 +529,11 @@ return array( 'Edit column "%s"' => '编辑栏目"%s"', 'Select the new status of the subtask: "%s"' => '选择子任务的新状态:"%s"', 'Subtask timesheet' => '子任务时间', - 'There is nothing to show.' => '无内容。', + 'There is nothing to show.' => '当前无内容可展示。', 'Time Tracking' => '时间记录', 'You already have one subtask in progress' => '你已经有了一个进行中的子任务', 'Which parts of the project do you want to duplicate?' => '要复制项目的哪些内容?', - // 'Disallow login form' => '', + 'Disallow login form' => '禁止登陆', 'Start' => '开始', 'End' => '结束', 'Task age in days' => '任务存在天数', @@ -593,26 +555,25 @@ return array( 'Remove a link' => '删除关联', 'Task\'s links' => '任务的关联', 'The labels must be different' => '标签不能一样', - 'There is no link.' => '没有关联', + 'There is no link.' => '当前没有关联', 'This label must be unique' => '关联必须唯一', 'Unable to create your link.' => '无法创建关联。', 'Unable to update your link.' => '无法更新关联。', 'Unable to remove this link.' => '无法删除关联。', 'relates to' => '关联到', - // 'blocks' => '', - // 'is blocked by' => '', - // 'duplicates' => '', - // 'is duplicated by' => '', - // 'is a child of' => '', - // 'is a parent of' => '', - // 'targets milestone' => '', - // 'is a milestone of' => '', - // 'fixes' => '', - // 'is fixed by' => '', + 'blocks' => '阻塞', + 'is blocked by' => '阻塞于', + 'duplicates' => '重复', + 'is duplicated by' => '重复于', + 'is a child of' => '子任务自', + 'is a parent of' => '父任务于', + 'targets milestone' => '里程碑目标', + 'is a milestone of' => '属于里程碑', + 'fixes' => '修复', + 'is fixed by' => '修复于', 'This task' => '此任务', '<1h' => '<1h', '%dh' => '%dh', - // '%b %e' => '', 'Expand tasks' => '展开任务', 'Collapse tasks' => '收缩任务', 'Expand/collapse tasks' => '展开/收缩任务', @@ -622,27 +583,24 @@ return array( 'Keyboard shortcuts' => '键盘快捷方式', 'Open board switcher' => '打开面板切换器', 'Application' => '应用程序', - // 'since %B %e, %Y at %k:%M %p' => '', 'Compact view' => '紧凑视图', 'Horizontal scrolling' => '水平滚动', 'Compact/wide view' => '紧凑/宽视图', 'No results match:' => '无匹配结果:', 'Currency' => '货币', - 'Files' => '文件', - 'Images' => '图片', 'Private project' => '私人项目', - // 'AUD - Australian Dollar' => '', - // 'CAD - Canadian Dollar' => '', - // 'CHF - Swiss Francs' => '', + 'AUD - Australian Dollar' => '澳元', + 'CAD - Canadian Dollar' => '加元', + 'CHF - Swiss Francs' => '瑞士法郎', 'Custom Stylesheet' => '自定义样式表', 'download' => '下载', - // 'EUR - Euro' => '', - // 'GBP - British Pound' => '', - // 'INR - Indian Rupee' => '', - // 'JPY - Japanese Yen' => '', - // 'NZD - New Zealand Dollar' => '', - // 'RSD - Serbian dinar' => '', - // 'USD - US Dollar' => '', + 'EUR - Euro' => '欧元', + 'GBP - British Pound' => '英镑', + 'INR - Indian Rupee' => '印度卢比', + 'JPY - Japanese Yen' => '日元', + 'NZD - New Zealand Dollar' => '新西兰元', + 'RSD - Serbian dinar' => '第纳尔', + 'USD - US Dollar' => '美元', 'Destination column' => '目标栏目', 'Move the task to another column when assigned to a user' => '指定负责人时移动到其它栏目', 'Move the task to another column when assignee is cleared' => '移除负责人时移动到其它栏目', @@ -675,127 +633,117 @@ return array( 'Test your device' => '测试设备', 'Assign a color when the task is moved to a specific column' => '任务移动到指定栏目时设置颜色', '%s via Kanboard' => '%s 通过KanBoard', - 'uploaded by: %s' => '由: %s 上传', - // 'uploaded on: %s' => '', - // 'size: %s' => '', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', - // 'This chart show the task complexity over the time (Work Remaining).' => '', + 'Burndown chart for "%s"' => '燃尽图:"%s"', + 'Burndown chart' => '燃尽图', + 'This chart show the task complexity over the time (Work Remaining).' => '图表显示任务随时间(剩余时间)的复杂度', 'Screenshot taken %s' => '已截图 %s', - // 'Add a screenshot' => '', - // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', - // 'Screenshot uploaded successfully.' => '', - // 'SEK - Swedish Krona' => '', - // 'Identifier' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + 'Add a screenshot' => '添加截图', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '获取截图并按CTRL+V或者⌘+V粘贴到这里', + 'Screenshot uploaded successfully.' => '截图上传成功', + 'SEK - Swedish Krona' => '瑞郎', + 'Identifier' => '标识符', + 'Disable two factor authentication' => '禁用双重认证', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => '你真的要禁用 "%s" 的双重认证吗?', 'Edit link' => '编辑链接', - // 'Start to type task title...' => '', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', - // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', - // 'Edit recurrence' => '', - // 'Generate recurrent task' => '', - // 'Trigger to generate recurrent task' => '', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', - // 'This task has created this child task: ' => '', - // 'Day(s)' => '', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', - // 'Timeframe to calculate new due date: ' => '', - // 'Trigger to generate recurrent task: ' => '', - // 'When task is closed' => '', - // 'When task is moved from first column' => '', - // 'When task is moved to last column' => '', - // 'Year(s)' => '', + 'Start to type task title...' => '输入任务标题...', + 'A task cannot be linked to itself' => '任务不能关联到本身', + 'The exact same link already exists' => '相同的关联已存在', + 'Recurrent task is scheduled to be generated' => '循环性任务将按计划生成', + 'Score' => '积分', + 'The identifier must be unique' => '标识符必须唯一', + 'This linked task id doesn\'t exists' => '关联任务不存在', + 'This value must be alphanumeric' => '此值必须是字母或者数字', + 'Edit recurrence' => '编辑循环周期', + 'Generate recurrent task' => '生成循环任务', + 'Trigger to generate recurrent task' => '生成循环任务的触发器', + 'Factor to calculate new due date' => '超期因素', + 'Timeframe to calculate new due date' => '计算超期时间表', + 'Base date to calculate new due date' => '计算超期基准日期', + 'Action date' => '操作日期', + 'Base date to calculate new due date: ' => '基准日期', + 'This task has created this child task: ' => '此任务创建了子任务:', + 'Day(s)' => '天', + 'Existing due date' => '已超期', + 'Factor to calculate new due date: ' => '超期因素', + 'Month(s)' => '月', + 'Recurrence' => '循环', + 'This task has been created by: ' => '此任务被谁创建:', + 'Recurrent task has been generated:' => '循环任务已生成:', + 'Timeframe to calculate new due date: ' => '计算超期时间表', + 'Trigger to generate recurrent task: ' => '生成循环任务的触发器', + 'When task is closed' => '当任务关闭时', + 'When task is moved from first column' => '当任务从第一列任务栏移走时', + 'When task is moved to last column' => '当任务移动到最后一列任务栏时', + 'Year(s)' => '年', 'Calendar settings' => '日程设置', - // 'Project calendar view' => '', + 'Project calendar view' => '项目日历表', 'Project settings' => '项目设置', - // 'Show subtasks based on the time tracking' => '', - // 'Show tasks based on the creation date' => '', - // 'Show tasks based on the start date' => '', - // 'Subtasks time tracking' => '', + 'Show subtasks based on the time tracking' => '在时间跟踪上显示子任务', + 'Show tasks based on the creation date' => '显示任务创建日期于', + 'Show tasks based on the start date' => '显示任务开始日期于', + 'Subtasks time tracking' => '子任务时间跟踪', 'User calendar view' => '用户日程视图', 'Automatically update the start date' => '自动更新开始日期', - // 'iCal feed' => '', + 'iCal feed' => '日历订阅', 'Preferences' => '偏好', 'Security' => '安全', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', + 'Two factor authentication disabled' => '双重认证已禁用', + 'Two factor authentication enabled' => '双重认证已启用', + 'Unable to update this user.' => '无法更新此用户', + 'There is no user management for private projects.' => '私有项目下无用户可管理', + 'User that will receive the email' => '用户将收到邮件', 'Email subject' => '邮件主题', 'Date' => '日期', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', + 'Add a comment log when moving the task between columns' => '当任务移动到任务栏时添加评论日志', + 'Move the task to another column when the category is changed' => '当任务分类改变时移动到任务栏', + 'Send a task by email to someone' => '发送任务邮件到用户', 'Reopen a task' => '重新开始一个任务', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', + 'Column change' => '任务栏改变', + 'Position change' => '位置改变', + 'Swimlane change' => '里程碑改变', + 'Assignee change' => '指派人改变', + '[%s] Overdue tasks' => '[%s] 超期任务', 'Notification' => '通知', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', + '%s moved the task #%d to the first swimlane' => '%s将任务#%d移动到了首个里程碑', + '%s moved the task #%d to the swimlane "%s"' => '%s将任务#%d移动到了里程碑"%s"下', + 'Swimlane' => '里程碑', + 'Gravatar' => 'Gravatar头像', + '%s moved the task %s to the first swimlane' => '%s将任务%s移动到了首个里程碑', + '%s moved the task %s to the swimlane "%s"' => '%s将任务%s移动到了里程碑"%s"下', + 'This report contains all subtasks information for the given date range.' => '该报告包含了指定日期范围内的所有子任务信息。', + 'This report contains all tasks information for the given date range.' => '该报告包含了指定日期范围内的所有任务信息。', + 'Project activities for %s' => '%s的项目活动记录', + 'view the board on Kanboard' => '在看板上查看面板', + 'The task have been moved to the first swimlane' => '该任务已被移动到首个里程碑', + 'The task have been moved to another swimlane:' => '该任务已被移动到别的里程碑:', + 'Overdue tasks for the project "%s"' => '"%s"项目下的超期任务', + 'New title: %s' => '新标题:%s', + 'The task is not assigned anymore' => '该任务没有指派人', + 'New assignee: %s' => '新指派到:%s', + 'There is no category now' => '当前没有分类', + 'New category: %s' => '新分类:%s', + 'New color: %s' => '新颜色:%s', + 'New complexity: %d' => '新的复杂度:%d', + 'The due date have been removed' => '超期时间已被移除', + 'There is no description anymore' => '当前没有描述', + 'Recurrence settings have been modified' => '循环周期已被更改', + 'Time spent changed: %sh' => '时间花费已变更:%sh', + 'Time estimated changed: %sh' => '时间预估已变更:%sh', + 'The field "%s" have been updated' => '"%s"字段已更新', + 'The description has been modified' => '描述已更改', + 'Do you really want to close the task "%s" as well as all subtasks?' => '你是否要移除所有子任务的同时父任务"%s"', + 'I want to receive notifications for:' => '我想接收以下相关通知:', + 'All tasks' => '所有任务', + 'Only for tasks assigned to me' => '所有指派给我的任务', + 'Only for tasks created by me' => '所有我创建的任务', + 'Only for tasks created by me and assigned to me' => '所有我创建的并且指派给我的任务', // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - 'Stop timer' => '停止定时器', - 'Start timer' => '开启定时器', + 'Total for all columns' => '所有栏目下的', + 'You need at least 2 days of data to show the chart.' => '当前柱状图至少需要2天的数据。', + '<15m' => '小于15分钟', + '<30m' => '小于30分钟', + 'Stop timer' => '停止计时器', + 'Start timer' => '开启计时器', 'Add project member' => '添加项目成员', 'Enable notifications' => '打开通知', 'My activity stream' => '我的活动流', @@ -813,31 +761,30 @@ return array( 'Not assigned' => '未指派', 'View advanced search syntax' => '查看高级搜索语法', 'Overview' => '概览', - // '%b %e %Y' => '', 'Board/Calendar/List view' => '看板/日程/列表视图', 'Switch to the board view' => '切换到看板视图', 'Switch to the calendar view' => '切换到日程视图', 'Switch to the list view' => '切换到列表视图', 'Go to the search/filter box' => '前往搜索/过滤箱', - 'There is no activity yet.' => '目前无任何活动.', + 'There is no activity yet.' => '当前无任何活动.', 'No tasks found.' => '没有找人任何任务.', 'Keyboard shortcut: "%s"' => '快捷键: "%s"', 'List' => '列表', 'Filter' => '过滤器', 'Advanced search' => '高级搜索', 'Example of query: ' => '查询示例: ', - 'Search by project: ' => '按项目搜索: ', - 'Search by column: ' => '按列搜索: ', - 'Search by assignee: ' => '按被指派人搜索: ', - 'Search by color: ' => '按颜色搜索: ', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', + 'Search by project: ' => '按项目: ', + 'Search by column: ' => '按任务栏: ', + 'Search by assignee: ' => '按被指派人: ', + 'Search by color: ' => '按颜色: ', + 'Search by category: ' => '按分类:', + 'Search by description: ' => '按描述:', + 'Search by due date: ' => '按超期时间:', // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', + 'Average time spent into each column for "%s"' => '"%s"在每个任务栏下平均花费时间', + 'Average time spent into each column' => '每个任务栏平均花费时间', + 'Average time spent' => '平均花费时间', + 'This chart show the average time spent into each column for the last %d tasks.' => '当前柱状图表示最新%d条任务在每个任务栏下的平均花费时间', // 'Average Lead and Cycle time' => '', // 'Average lead time: ' => '', // 'Average cycle time: ' => '', @@ -846,10 +793,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', // 'Lead time: ' => '', // 'Cycle time: ' => '', // 'Time spent into each column' => '', @@ -857,24 +800,20 @@ return array( // 'The cycle time is the duration between the start date and the completion.' => '', // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'New remote user' => '', - // 'New local user' => '', + 'Edit Authentication' => '编辑认证信息', + 'Remote user' => '远程用户', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '远程用户不会在看板数据库保存密码,例如:LDAP,GOOGLE,GitHub。', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '如果选中“禁止登陆来自”,登陆表单内的验证信息将被忽略。', + 'New remote user' => '新加远程用户', + 'New local user' => '新加本地用户', 'Default task color' => '默认任务颜色', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', - // 'There is no destination project available.' => '', + 'This feature does not work with all browsers.' => '本功能只在部分浏览器下工作正常。', + 'There is no destination project available.' => '当前没有目标项目可用', 'Trigger automatically subtask time tracking' => '自动跟踪子任务时间', 'Include closed tasks in the cumulative flow diagram' => '在累计流程图中包含已关闭任务', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', + 'Current swimlane: %s' => '当前里程碑:%s', + 'Current column: %s' => '当前任务栏:%s', + 'Current category: %s' => '当前分类:%s', 'no category' => '无分类', 'Current assignee: %s' => '当前被指派人: %s', 'not assigned' => '未指派', @@ -889,233 +828,327 @@ return array( 'Add task' => '添加任务', 'Start date:' => '开始日期', 'Due date:' => '到期日期', - // 'There is no start date or due date for this task.' => '', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', + 'There is no start date or due date for this task.' => '当前任务没有开始或结束时间。', + 'Moving or resizing a task will change the start and due date of the task.' => '移动或者重设任务将改变任务开始和结束时间。', + 'There is no task in your project.' => '当前项目还没有任务', 'Gantt chart' => '甘特图', - // 'People who are project managers' => '', - // 'People who are project members' => '', - // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', + 'People who are project managers' => '项目管理员', + 'People who are project members' => '项目成员', + 'NOK - Norwegian Krone' => '克朗', + 'Show this column' => '显示任务栏', + 'Hide this column' => '隐藏任务栏', + 'open file' => '打开文件', 'End date' => '结束日期', 'Users overview' => '用户概览', - // 'Managers' => '', 'Members' => '成员', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', - // 'Documentation' => '', - // 'Table of contents' => '', - // 'Gantt' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - // 'Project members' => '', - // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', - // 'Time-based One-time Password Algorithm' => '', - // 'Two-Factor Provider: ' => '', - // 'Disable two-factor authentication' => '', - // 'Enable two-factor authentication' => '', - // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', - // 'The password has never been reinitialized.' => '', - // 'Creation' => '', - // 'Expiration' => '', - // 'Password reset history' => '', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Priority: %d' => '', + 'Shared project' => '公开项目', + 'Project managers' => '项目管理员', + 'Gantt chart for all projects' => '所有项目的甘特图', + 'Projects list' => '项目列表', + 'Gantt chart for this project' => '此项目的甘特图', + 'Project board' => '项目面板', + 'End date:' => '结束日期', + 'There is no start date or end date for this project.' => '当前项目没有开始或结束日期', + 'Projects Gantt chart' => '项目甘特图', + 'Link type' => '关联类型', + 'Change task color when using a specific task link' => '当任务关联到指定任务时改变颜色', + 'Task link creation or modification' => '任务链接创建或更新时间', + 'Milestone' => '里程碑', + 'Documentation: %s' => '文档:%s', + 'Switch to the Gantt chart view' => '切换到甘特图', + 'Reset the search/filter box' => '重置搜索/过滤框', + 'Documentation' => '文档', + 'Table of contents' => '表内容', + 'Gantt' => '甘特图', + 'Author' => '作者', + 'Version' => '版本', + 'Plugins' => '插件', + 'There is no plugin loaded.' => '当前没有插件载入', + 'Set maximum column height' => '设置任务栏最大高度', + 'Remove maximum column height' => '移除任务栏最大高度', + 'My notifications' => '我的通知', + 'Custom filters' => '自定义过滤器', + 'Your custom filter have been created successfully.' => '成功创建过滤器', + 'Unable to create your custom filter.' => '无法创建过滤器', + 'Custom filter removed successfully.' => '成功删除过滤器', + 'Unable to remove this custom filter.' => '无法删除这个过滤器', + 'Edit custom filter' => '编辑过滤器', + 'Your custom filter have been updated successfully.' => '你的过滤器更新成功', + 'Unable to update custom filter.' => '无法更新过滤器', + 'Web' => 'web', + 'New attachment on task #%d: %s' => '任务#%d下的新附件:%s', + 'New comment on task #%d' => '任务#%d下的新评论', + 'Comment updated on task #%d' => '任务#%d的评论已更新', + 'New subtask on task #%d' => '任务#%d下新的子任务', + 'Subtask updated on task #%d' => '任务#%d下的子任务已更新', + 'New task #%d: %s' => '新任务#%d:%s', + 'Task updated #%d' => '任务#%d已更新', + 'Task #%d closed' => '任务#%d已关闭', + 'Task #%d opened' => '任务#%d已打开', + 'Column changed for task #%d' => '任务#%d的任务栏已改变', + 'New position for task #%d' => '任务#%d的新状态', + 'Swimlane changed for task #%d' => '任务#%d的里程碑已改变', + 'Assignee changed on task #%d' => '任务#%d的指派人已改变', + '%d overdue tasks' => '%d条超期任务', + 'Task #%d is overdue' => '任务#%d已超期', + 'No new notifications.' => '没有新通知', + 'Mark all as read' => '标记所有为已读', + 'Mark as read' => '标记为已读', + 'Total number of tasks in this column across all swimlanes' => '此任务栏下的任务数(跨里程碑)', + 'Collapse swimlane' => '收起里程碑', + 'Expand swimlane' => '展开里程碑', + 'Add a new filter' => '添加新过滤器', + 'Share with all project members' => '对项目所有成员共享', + 'Shared' => '共享', + 'Owner' => '所有人', + 'Unread notifications' => '未读通知', + 'My filters' => '我的过滤器', + 'Notification methods:' => '通知提醒方式:', + 'Import tasks from CSV file' => '从CSV文件导入任务', + 'Unable to read your file' => '无法读取文件', + '%d task(s) have been imported successfully.' => '成功导入%d条任务。', + 'Nothing have been imported!' => '没有信息被导入!', + 'Import users from CSV file' => '从CSV文件导入用户', + '%d user(s) have been imported successfully.' => '成功导入%d个用户。', + 'Comma' => '逗号', + 'Semi-colon' => '分号', + 'Tab' => '制表符', + 'Vertical bar' => '竖线', + 'Double Quote' => '双引号', + 'Single Quote' => '单引号', + '%s attached a file to the task #%d' => '%s添加文件到任务#%d', + 'There is no column or swimlane activated in your project!' => '当前项目没有活动任务栏或里程碑', + 'Append filter (instead of replacement)' => '追加过滤', + 'Append/Replace' => '追加/替换', + 'Append' => '追加', + 'Replace' => '替换', + 'Import' => '导入', + 'change sorting' => '改变排序', + 'Tasks Importation' => '任务重要性', + 'Delimiter' => '分隔符', + 'Enclosure' => '附件', + 'CSV File' => 'CSV文件', + 'Instructions' => '操作指南', + 'Your file must use the predefined CSV format' => '文件必须为预定格式的CSV文件', + 'Your file must be encoded in UTF-8' => '文件编码必须为UTF-8', + 'The first row must be the header' => '第一行必须为表头', + 'Duplicates are not verified for you' => '无法验证重复信息', + 'The due date must use the ISO format: YYYY-MM-DD' => '超期日期必须为ISO格式:YYYY-MM-DD', + 'Download CSV template' => '下载CSV模板', + 'No external integration registered.' => '没有外部注册信息', + 'Duplicates are not imported' => '重复信息未导入', + 'Usernames must be lowercase and unique' => '用户名必须小写且唯一', + 'Passwords will be encrypted if present' => '密码将被加密', + '%s attached a new file to the task %s' => '"%s"添加了附件到任务"%s"', + 'Assign automatically a category based on a link' => '基于链接自动关联分类', + 'BAM - Konvertible Mark' => '波斯尼亚马克', + 'Assignee Username' => '指派用户名', + 'Assignee Name' => '指派名称', + 'Groups' => '用户组', + 'Members of %s' => '“%s”组成员', + 'New group' => '新加用户组', + 'Group created successfully.' => '用户组创建成功', + 'Unable to create your group.' => '无法创建你的用户组', + 'Edit group' => '编辑用户组', + 'Group updated successfully.' => '用户组更新成功', + 'Unable to update your group.' => '无法更新你的用户组', + 'Add group member to "%s"' => '添加到用户组"%s"', + 'Group member added successfully.' => '成功添加用户组成员', + 'Unable to add group member.' => '无法添加用户组成员', + 'Remove user from group "%s"' => '从"%s"组中移除用户', + 'User removed successfully from this group.' => '用户已从该用户组中删除', + 'Unable to remove this user from the group.' => '无法从该用户组中删除用户', + 'Remove group' => '删除用户组', + 'Group removed successfully.' => '用户组已删除', + 'Unable to remove this group.' => '无法删除该用户组', + 'Project Permissions' => '项目权限', + 'Manager' => '管理员', + 'Project Manager' => '项目管理员', + 'Project Member' => '项目成员', + 'Project Viewer' => '项目观察员', + 'Your account is locked for %d minutes' => '你的账户被锁定%d分钟', + 'Invalid captcha' => '验证码无效', + 'The name must be unique' => '请确保用户名唯一', + 'View all groups' => '查看所有用户组', + 'View group members' => '查看该组所有用户', + 'There is no user available.' => '当前没有有效用户', + 'Do you really want to remove the user "%s" from the group "%s"?' => '你确定把用户"%s"从"%s"中移除?', + 'There is no group.' => '当前没有用户组', + 'External Id' => '关联ID', + 'Add group member' => '添加用户组成员', + 'Do you really want to remove this group: "%s"?' => '你真的想要移除用户组:"%s"?', + 'There is no user in this group.' => '当前用户组下没有成员', + 'Remove this user' => '移除用户', + 'Permissions' => '权限', + 'Allowed Users' => '被允许的用户', + 'No user have been allowed specifically.' => '没有被允许的用户。', + 'Role' => '角色', + 'Enter user name...' => '输入用户名...', + 'Allowed Groups' => '被允许的用户组', + 'No group have been allowed specifically.' => '没有被允许的用户组。', + 'Group' => '用户组', + 'Group Name' => '用户组名称', + 'Enter group name...' => '输入用户组名称...', + 'Role:' => '角色:', + 'Project members' => '项目成员', + 'Compare hours for "%s"' => '比较"%s"的时间', + '%s mentioned you in the task #%d' => '%s在任务#%d里提及你', + '%s mentioned you in a comment on the task #%d' => '%s在任务#%d的评论里提及你', + 'You were mentioned in the task #%d' => '你在任务#%d里被提及', + 'You were mentioned in a comment on the task #%d' => '你在任务#%d的评论里被提及', + 'Mentioned' => '被提及', + 'Compare Estimated Time vs Actual Time' => '对比预估时间VS实际时间', + 'Estimated hours: ' => '预估小时数', + 'Actual hours: ' => '实际小时数', + 'Hours Spent' => '花费小时数', + 'Hours Estimated' => '预估小时数', + 'Estimated Time' => '预估时间', + 'Actual Time' => '实际时间', + 'Estimated vs actual time' => '预估时间 VS 实际时间', + 'RUB - Russian Ruble' => '卢布', + 'Assign the task to the person who does the action when the column is changed' => '当任务所属任务栏改变时分配到指定用户', + 'Close a task in a specific column' => '关闭指定任务栏下的任务', + 'Time-based One-time Password Algorithm' => '基于时间的一次性密码算法', + 'Two-Factor Provider: ' => '双重认证提供商', + 'Disable two-factor authentication' => '禁用双重认证', + 'Enable two-factor authentication' => '启用双重认证', + 'There is no integration registered at the moment.' => '当前没有注册关联', + 'Password Reset for Kanboard' => '重置kanboard密码', + 'Forgot password?' => '忘记密码?', + 'Enable "Forget Password"' => '启用找回密码', + 'Password Reset' => '密码重置', + 'New password' => '新密码', + 'Change Password' => '更改密码', + 'To reset your password click on this link:' => '点击此链接重置你的密码', + 'Last Password Reset' => '上次密码重置', + 'The password has never been reinitialized.' => '密码从未被重新初始化', + 'Creation' => '创建时间', + 'Expiration' => '过期时间', + 'Password reset history' => '密码重置历史', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '任务栏 "%s" 和 里程碑 "%s" 下的所有任务已关闭', + 'Do you really want to close all tasks of this column?' => '你确定要关闭此任务栏下的所有任务?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '任务栏 "%s" 和 里程碑 "%s" 下 %d 条任务将被关闭。', + 'Close all tasks of this column' => '关闭此任务栏下的所有任务', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '没有插件注册到项目通知接口,你仍然可以在你个人设置里启用单独的通知', + 'My dashboard' => '我的控制台', + 'My profile' => '我的个人信息', + 'Project owner: ' => '项目负责人', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '项目标识符是可选的,且只能是数字或字母组成,例如:MYPROJECT。', + 'Project owner' => '项目负责人', + 'Those dates are useful for the project Gantt chart.' => '那些时间对于项目甘特图非常有用', + 'Private projects do not have users and groups management.' => '私有项目下没有成员或组可管理', + 'There is no project member.' => '当前没有项目成员', + 'Priority' => '优先级', + 'Task priority' => '任务优先级', + 'General' => '常用', + 'Dates' => '时间', + 'Default priority' => '默认优先级', + 'Lowest priority' => '最低优先级', + 'Highest priority' => '最高优先级', + 'If you put zero to the low and high priority, this feature will be disabled.' => '如果你为优先级填0,将禁用本功能', + 'Close a task when there is no activity' => '当任务没有活动记录时关闭任务', + 'Duration in days' => '持续天数', + 'Send email when there is no activity on a task' => '当任务没有活动记录时发送邮件', + 'List of external links' => '列出外部关联', + 'Unable to fetch link information.' => '无法获取关联信息', + 'Daily background job for tasks' => '每日后台任务', + 'Auto' => '自动', + 'Related' => '相关的', + 'Attachment' => '附件', + 'Title not found' => '标题未找到', + 'Web Link' => '网页链接', + 'External links' => '外部关联', + 'Add external link' => '添加外部关联', + 'Type' => '类型', + 'Dependency' => '依赖', + 'Add internal link' => '添加内部关联', + 'Add a new external link' => '添加新的外部关联', + 'Edit external link' => '编辑外部关联', + 'External link' => '外部关联', + 'Copy and paste your link here...' => '复制并粘贴链接到当前位置...', + 'URL' => 'URL', + 'There is no external link for the moment.' => '当前没有外部关联。', + 'Internal links' => '内部关联', + 'There is no internal link for the moment.' => '当前没有内部关联。', + 'Assign to me' => '指派给我', + 'Me' => '我', + 'Do not duplicate anything' => '不再重复', + 'Projects management' => '项目管理', + 'Users management' => '用户管理', + 'Groups management' => '用户组管理', + 'Create from another project' => '从另一个项目中创建', + 'There is no subtask at the moment.' => '当前没有子任务。', + 'open' => '打开', + 'closed' => '已关闭', + 'Priority:' => '优先级:', + 'Reference:' => '引用:', + 'Complexity:' => '复杂度:', + 'Swimlane:' => '里程碑:', + 'Column:' => '列:', + 'Position:' => '位置:', + 'Creator:' => '创建者:', + 'Time estimated:' => '时间已过去:', + '%s hours' => '%s 小时', + 'Time spent:' => '时间消耗:', + 'Created:' => '已创建:', + 'Modified:' => '已修改:', + 'Completed:' => '已完成:', + 'Started:' => '已开始:', + 'Moved:' => '已移走', + 'Task #%d' => '任务#%d', + 'Sub-tasks' => '子任务', + 'Date and time format' => '时间和日期格式', + 'Time format' => '时间格式', + 'Start date: ' => '开始时间:', + 'End date: ' => '结束时间:', + 'New due date: ' => '新超期时间:', + 'Start date changed: ' => '开始时间已改变:', + 'Disable private projects' => '禁用私有项目', + 'Do you really want to remove this custom filter: "%s"?' => '你确定要移除这个自定义过滤器:"%s"?', + 'Remove a custom filter' => '移除自定义过滤器', + 'User activated successfully.' => '用户已激活。', + 'Unable to enable this user.' => '无法启用该用户。', + 'User disabled successfully.' => '用户已禁用。', + 'Unable to disable this user.' => '无法禁用该用户。', + 'All files have been uploaded successfully.' => '所有文件已成功上传。', + 'View uploaded files' => '查看已上传文件', + 'The maximum allowed file size is %sB.' => '最大上传尺寸 %sB', + 'Choose files again' => '重新选择文件', + 'Drag and drop your files here' => '拖放文件到这里', + 'choose files' => '选择文件', + 'View profile' => '查看个人信息', + 'Two Factor' => '双重认证', + 'Disable user' => '禁用用户', + 'Do you really want to disable this user: "%s"?' => '你确定要禁用该用户:"%s"?', + 'Enable user' => '启用用户', + 'Do you really want to enable this user: "%s"?' => '你确定要启用该用户:"%s"?', + 'Download' => '下载', + 'Uploaded: %s' => '上传:%s', + 'Size: %s' => '大小:%s', + 'Uploaded by %s' => '由%s上传', + 'Filename' => '文件名', + 'Size' => '大小', + 'Column created successfully.' => '新增任务栏成功。', + 'Another column with the same name exists in the project' => '当前项目中重名任务栏已存在', + 'Default filters' => '默认过滤器', + 'Your board doesn\'t have any column!' => '你的看板没有任何栏目', + 'Change column position' => '更改任务栏位置', + 'Switch to the project overview' => '切换到项目视图', + 'User filters' => '用户过滤器', + 'Category filters' => '分类过滤器', + 'Upload a file' => '上传文件', + 'There is no attachment at the moment.' => '当前没有附件。', + 'View file' => '查看文件', + 'Last activity' => '最后活动', + 'Change subtask position' => '更改子任务位置', + 'This value must be greater than %d' => '当前输入值必须大于%d', + 'Another swimlane with the same name exists in the project' => '当前项目中重名里程碑已存在', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '例如: http://example.kanboard.net/(通常用于生成绝对地址)', + 'Actions duplicated successfully.' => '动作复制成功。', + 'Unable to duplicate actions.' => '无法复制动作。', + 'Add a new action' => '添加新动作', + 'Import from another project' => '从另一个项目中导入', + 'There is no action at the moment.' => '当前没有动作。', + 'Import actions from another project' => '从另一个项目中导入动作', + 'There is no available project.' => '当前没有可用项目', ); diff --git a/app/Model/Action.php b/app/Model/Action.php index 4da2fb8f..f055d9d0 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -151,7 +151,7 @@ class Action extends Base * * @author Antonio Rabelo * @param integer $src_project_id Source project id - * @return integer $dst_project_id Destination project id + * @param integer $dst_project_id Destination project id * @return boolean */ public function duplicate($src_project_id, $dst_project_id) diff --git a/app/Model/ActionParameter.php b/app/Model/ActionParameter.php index 62b03142..53edcbc8 100644 --- a/app/Model/ActionParameter.php +++ b/app/Model/ActionParameter.php @@ -150,8 +150,8 @@ class ActionParameter extends Base case 'dest_column_id': case 'dst_column_id': case 'column_id': - $column = $this->board->getColumn($value); - return empty($column) ? false : $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; + $column = $this->column->getById($value); + return empty($column) ? false : $this->column->getColumnIdByTitle($project_id, $column['title']) ?: false; case 'user_id': case 'owner_id': return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; diff --git a/app/Model/Base.php b/app/Model/Base.php index 6fe3d74a..714b4308 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -33,86 +33,6 @@ abstract class Base extends \Kanboard\Core\Base } /** - * Remove keys from an array - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys to remove - */ - public function removeFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values)) { - unset($values[$key]); - } - } - } - - /** - * Remove keys from an array if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys to remove - */ - public function removeEmptyFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values) && empty($values[$key])) { - unset($values[$key]); - } - } - } - - /** - * Force fields to be at 0 if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function resetFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (isset($values[$key]) && empty($values[$key])) { - $values[$key] = 0; - } - } - } - - /** - * Force some fields to be integer - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function convertIntegerFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (isset($values[$key])) { - $values[$key] = (int) $values[$key]; - } - } - } - - /** - * Force some fields to be null if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function convertNullFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values) && empty($values[$key])) { - $values[$key] = null; - } - } - } - - /** * Build SQL condition for a given time range * * @access protected @@ -135,23 +55,4 @@ abstract class Base extends \Kanboard\Core\Base return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; } - - /** - * Group a collection of records by a column - * - * @access public - * @param array $collection - * @param string $column - * @return array - */ - public function groupByColumn(array $collection, $column) - { - $result = array(); - - foreach ($collection as $item) { - $result[$item[$column]][] = $item; - } - - return $result; - } } diff --git a/app/Model/Board.php b/app/Model/Board.php index 0f980f68..d41ecafe 100644 --- a/app/Model/Board.php +++ b/app/Model/Board.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use PicoDb\Database; - /** * Board model * @@ -13,13 +11,6 @@ use PicoDb\Database; class Board extends Base { /** - * SQL table name - * - * @var string - */ - const TABLE = 'columns'; - - /** * Get Kanboard default columns * * @access public @@ -73,7 +64,7 @@ class Board extends Base 'description' => $column['description'], ); - if (! $this->db->table(self::TABLE)->save($values)) { + if (! $this->db->table(Column::TABLE)->save($values)) { return false; } } @@ -86,12 +77,12 @@ class Board extends Base * * @author Antonio Rabelo * @param integer $project_from Project Template - * @return integer $project_to Project that receives the copy + * @param integer $project_to Project that receives the copy * @return boolean */ public function duplicate($project_from, $project_to) { - $columns = $this->db->table(Board::TABLE) + $columns = $this->db->table(Column::TABLE) ->columns('title', 'task_limit', 'description') ->eq('project_id', $project_from) ->asc('position') @@ -101,134 +92,6 @@ class Board extends Base } /** - * Add a new column to the board - * - * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer - */ - public function addColumn($project_id, $title, $task_limit = 0, $description = '') - { - $values = array( - 'project_id' => $project_id, - 'title' => $title, - 'task_limit' => intval($task_limit), - 'position' => $this->getLastColumnPosition($project_id) + 1, - 'description' => $description, - ); - - return $this->persist(self::TABLE, $values); - } - - /** - * Update a column - * - * @access public - * @param integer $column_id Column id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Optional description - * @return boolean - */ - public function updateColumn($column_id, $title, $task_limit = 0, $description = '') - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( - 'title' => $title, - 'task_limit' => intval($task_limit), - 'description' => $description, - )); - } - - /** - * Get columns with consecutive positions - * - * If you remove a column, the positions are not anymore consecutives - * - * @access public - * @param integer $project_id - * @return array - */ - public function getNormalizedColumnPositions($project_id) - { - $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position'); - $position = 1; - - foreach ($columns as $column_id => $column_position) { - $columns[$column_id] = $position++; - } - - return $columns; - } - - /** - * Save the new positions for a set of columns - * - * @access public - * @param array $columns Hashmap of column_id/column_position - * @return boolean - */ - public function saveColumnPositions(array $columns) - { - return $this->db->transaction(function (Database $db) use ($columns) { - - foreach ($columns as $column_id => $position) { - if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) { - return false; - } - } - }); - } - - /** - * Move a column down, increment the column position value - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @return boolean - */ - public function moveDown($project_id, $column_id) - { - $columns = $this->getNormalizedColumnPositions($project_id); - $positions = array_flip($columns); - - if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) { - $position = ++$columns[$column_id]; - $columns[$positions[$position]]--; - - return $this->saveColumnPositions($columns); - } - - return false; - } - - /** - * Move a column up, decrement the column position value - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @return boolean - */ - public function moveUp($project_id, $column_id) - { - $columns = $this->getNormalizedColumnPositions($project_id); - $positions = array_flip($columns); - - if (isset($columns[$column_id]) && $columns[$column_id] > 1) { - $position = --$columns[$column_id]; - $columns[$positions[$position]]++; - - return $this->saveColumnPositions($columns); - } - - return false; - } - - /** * Get all tasks sorted by columns and swimlanes * * @access public @@ -239,7 +102,7 @@ class Board extends Base public function getBoard($project_id, $callback = null) { $swimlanes = $this->swimlane->getSwimlanes($project_id); - $columns = $this->getColumns($project_id); + $columns = $this->column->getAll($project_id); $nb_columns = count($columns); for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { @@ -307,131 +170,4 @@ class Board extends Base return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } - - /** - * Get the first column id for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getFirstColumn($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id'); - } - - /** - * Get the last column id for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getLastColumn($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id'); - } - - /** - * Get the list of columns sorted by position [ column_id => title ] - * - * @access public - * @param integer $project_id Project id - * @param boolean $prepend Prepend a default value - * @return array - */ - public function getColumnsList($project_id, $prepend = false) - { - $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); - return $prepend ? array(-1 => t('All columns')) + $listing : $listing; - } - - /** - * Get all columns sorted by position for a given project - * - * @access public - * @param integer $project_id Project id - * @return array - */ - public function getColumns($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll(); - } - - /** - * Get the number of columns for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function countColumns($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count(); - } - - /** - * Get a column by the id - * - * @access public - * @param integer $column_id Column id - * @return array - */ - public function getColumn($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); - } - - /** - * Get a column id by the name - * - * @access public - * @param integer $project_id - * @param string $title - * @return integer - */ - public function getColumnIdByTitle($project_id, $title) - { - return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id'); - } - - /** - * Get a column title by the id - * - * @access public - * @param integer $column_id - * @return integer - */ - public function getColumnTitleById($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title'); - } - - /** - * Get the position of the last column for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getLastColumnPosition($project_id) - { - return (int) $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->desc('position') - ->findOneColumn('position'); - } - - /** - * Remove a column and all tasks associated to this column - * - * @access public - * @param integer $column_id Column id - * @return boolean - */ - public function removeColumn($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); - } } diff --git a/app/Model/Category.php b/app/Model/Category.php index 58cee738..1d5f6546 100644 --- a/app/Model/Category.php +++ b/app/Model/Category.php @@ -22,12 +22,11 @@ class Category extends Base * * @access public * @param integer $category_id Category id - * @param integer $project_id Project id * @return boolean */ - public function exists($category_id, $project_id) + public function exists($category_id) { - return $this->db->table(self::TABLE)->eq('id', $category_id)->eq('project_id', $project_id)->exists(); + return $this->db->table(self::TABLE)->eq('id', $category_id)->exists(); } /** @@ -115,25 +114,29 @@ class Category extends Base } /** - * Create default cetegories during project creation (transaction already started in Project::create()) + * Create default categories during project creation (transaction already started in Project::create()) * * @access public * @param integer $project_id + * @return boolean */ public function createDefaultCategories($project_id) { + $results = array(); $categories = explode(',', $this->config->get('project_categories')); foreach ($categories as $category) { $category = trim($category); if (! empty($category)) { - $this->db->table(self::TABLE)->insert(array( + $results[] = $this->db->table(self::TABLE)->insert(array( 'project_id' => $project_id, 'name' => $category, )); } } + + return in_array(false, $results, true); } /** @@ -188,16 +191,17 @@ class Category extends Base * * @author Antonio Rabelo * @param integer $src_project_id Source project id - * @return integer $dst_project_id Destination project id + * @param integer $dst_project_id Destination project id * @return boolean */ public function duplicate($src_project_id, $dst_project_id) { - $categories = $this->db->table(self::TABLE) - ->columns('name') - ->eq('project_id', $src_project_id) - ->asc('name') - ->findAll(); + $categories = $this->db + ->table(self::TABLE) + ->columns('name', 'description') + ->eq('project_id', $src_project_id) + ->asc('name') + ->findAll(); foreach ($categories as $category) { $category['project_id'] = $dst_project_id; diff --git a/app/Model/Color.php b/app/Model/Color.php index d341dd3c..dee28643 100644 --- a/app/Model/Color.php +++ b/app/Model/Color.php @@ -141,6 +141,7 @@ class Color extends Base * Get available colors * * @access public + * @param bool $prepend * @return array */ public function getList($prepend = false) @@ -177,7 +178,7 @@ class Color extends Base } /** - * Get Bordercolor from string + * Get border color from string * * @access public * @param string $color_id Color id diff --git a/app/Model/Column.php b/app/Model/Column.php new file mode 100644 index 00000000..ccdcb049 --- /dev/null +++ b/app/Model/Column.php @@ -0,0 +1,209 @@ +<?php + +namespace Kanboard\Model; + +/** + * Column Model + * + * @package model + * @author Frederic Guillot + */ +class Column extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'columns'; + + /** + * Get a column by the id + * + * @access public + * @param integer $column_id Column id + * @return array + */ + public function getById($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); + } + + /** + * Get the first column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getFirstColumnId($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id'); + } + + /** + * Get the last column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumnId($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id'); + } + + /** + * Get the position of the last column for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumnPosition($project_id) + { + return (int) $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->desc('position') + ->findOneColumn('position'); + } + + /** + * Get a column id by the name + * + * @access public + * @param integer $project_id + * @param string $title + * @return integer + */ + public function getColumnIdByTitle($project_id, $title) + { + return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id'); + } + + /** + * Get a column title by the id + * + * @access public + * @param integer $column_id + * @return integer + */ + public function getColumnTitleById($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title'); + } + + /** + * Get all columns sorted by position for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll(); + } + + /** + * Get the list of columns sorted by position [ column_id => title ] + * + * @access public + * @param integer $project_id Project id + * @param boolean $prepend Prepend a default value + * @return array + */ + public function getList($project_id, $prepend = false) + { + $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; + } + + /** + * Add a new column to the board + * + * @access public + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @return boolean|integer + */ + public function create($project_id, $title, $task_limit = 0, $description = '') + { + $values = array( + 'project_id' => $project_id, + 'title' => $title, + 'task_limit' => intval($task_limit), + 'position' => $this->getLastColumnPosition($project_id) + 1, + 'description' => $description, + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Update a column + * + * @access public + * @param integer $column_id Column id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Optional description + * @return boolean + */ + public function update($column_id, $title, $task_limit = 0, $description = '') + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( + 'title' => $title, + 'task_limit' => intval($task_limit), + 'description' => $description, + )); + } + + /** + * Remove a column and all tasks associated to this column + * + * @access public + * @param integer $column_id Column id + * @return boolean + */ + public function remove($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); + } + + /** + * Change column position + * + * @access public + * @param integer $project_id + * @param integer $column_id + * @param integer $position + * @return boolean + */ + public function changePosition($project_id, $column_id, $position) + { + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) { + return false; + } + + $column_ids = $this->db->table(self::TABLE)->eq('project_id', $project_id)->neq('id', $column_id)->asc('position')->findAllByColumn('id'); + $offset = 1; + $results = array(); + + foreach ($column_ids as $current_column_id) { + if ($offset == $position) { + $offset++; + } + + $results[] = $this->db->table(self::TABLE)->eq('id', $current_column_id)->update(array('position' => $offset)); + $offset++; + } + + $results[] = $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); + } +} diff --git a/app/Model/Config.php b/app/Model/Config.php index 55999310..7b254c8d 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -76,6 +76,7 @@ class Config extends Setting 'en_US' => 'English', 'es_ES' => 'Español', 'fr_FR' => 'Français', + 'el_GR' => 'Grec', 'it_IT' => 'Italiano', 'hu_HU' => 'Magyar', 'my_MY' => 'Melayu', @@ -131,7 +132,8 @@ class Config extends Setting 'zh_CN' => 'zh-cn', 'ja_JP' => 'ja', 'th_TH' => 'th', - 'id_ID' => 'id' + 'id_ID' => 'id', + 'el_GR' => 'el', ); $lang = $this->getCurrentLanguage(); @@ -237,6 +239,7 @@ class Config extends Setting * Prepare data before save * * @access public + * @param array $values * @return array */ public function prepare(array $values) diff --git a/app/Model/File.php b/app/Model/File.php index be62cdb3..03ea691d 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -2,31 +2,44 @@ namespace Kanboard\Model; +use Exception; use Kanboard\Event\FileEvent; use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** - * File model + * Base File Model * * @package model * @author Frederic Guillot */ -class File extends Base +abstract class File extends Base { /** - * SQL table name - * - * @var string - */ - const TABLE = 'files'; - - /** - * Events + * Get PicoDb query to get all files * - * @var string + * @access protected + * @return \PicoDb\Table */ - const EVENT_CREATE = 'file.create'; + protected function getQuery() + { + return $this->db + ->table(static::TABLE) + ->columns( + static::TABLE.'.id', + static::TABLE.'.name', + static::TABLE.'.path', + static::TABLE.'.is_image', + static::TABLE.'.'.static::FOREIGN_KEY, + static::TABLE.'.date', + static::TABLE.'.user_id', + static::TABLE.'.size', + User::TABLE.'.username', + User::TABLE.'.name as user_name' + ) + ->join(User::TABLE, 'id', 'user_id') + ->asc(static::TABLE.'.name'); + } /** * Get a file by the id @@ -37,146 +50,120 @@ class File extends Base */ public function getById($file_id) { - return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne(); + return $this->db->table(static::TABLE)->eq('id', $file_id)->findOne(); } /** - * Remove a file + * Get all files * * @access public - * @param integer $file_id File id - * @return bool + * @param integer $id + * @return array */ - public function remove($file_id) + public function getAll($id) { - try { - $file = $this->getbyId($file_id); - $this->objectStorage->remove($file['path']); - - if ($file['is_image'] == 1) { - $this->objectStorage->remove($this->getThumbnailPath($file['path'])); - } - - return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove(); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return false; - } + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->findAll(); } /** - * Remove all files for a given task + * Get all images * * @access public - * @param integer $task_id Task id - * @return bool + * @param integer $id + * @return array */ - public function removeAll($task_id) + public function getAllImages($id) { - $file_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->asc('id')->findAllByColumn('id'); - $results = array(); - - foreach ($file_ids as $file_id) { - $results[] = $this->remove($file_id); - } + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 1)->findAll(); + } - return ! in_array(false, $results, true); + /** + * Get all files without images + * + * @access public + * @param integer $id + * @return array + */ + public function getAllDocuments($id) + { + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 0)->findAll(); } /** * Create a file entry in the database * * @access public - * @param integer $task_id Task id + * @param integer $id Foreign key * @param string $name Filename * @param string $path Path on the disk * @param integer $size File size * @return bool|integer */ - public function create($task_id, $name, $path, $size) + public function create($id, $name, $path, $size) { - $result = $this->db->table(self::TABLE)->save(array( - 'task_id' => $task_id, + $values = array( + static::FOREIGN_KEY => $id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, 'size' => $size, 'user_id' => $this->userSession->getId() ?: 0, 'date' => time(), - )); + ); - if ($result) { - $this->container['dispatcher']->dispatch( - self::EVENT_CREATE, - new FileEvent(array('task_id' => $task_id, 'name' => $name)) - ); + $result = $this->db->table(static::TABLE)->insert($values); - return (int) $this->db->getLastId(); + if ($result) { + $file_id = (int) $this->db->getLastId(); + $event = new FileEvent($values + array('file_id' => $file_id)); + $this->dispatcher->dispatch(static::EVENT_CREATE, $event); + return $file_id; } return false; } /** - * Get PicoDb query to get all files + * Remove all files * * @access public - * @return \PicoDb\Table + * @param integer $id + * @return bool */ - public function getQuery() + public function removeAll($id) { - return $this->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.id', - self::TABLE.'.name', - self::TABLE.'.path', - self::TABLE.'.is_image', - self::TABLE.'.task_id', - self::TABLE.'.date', - self::TABLE.'.user_id', - self::TABLE.'.size', - User::TABLE.'.username', - User::TABLE.'.name as user_name' - ) - ->join(User::TABLE, 'id', 'user_id') - ->asc(self::TABLE.'.name'); - } + $file_ids = $this->db->table(static::TABLE)->eq(static::FOREIGN_KEY, $id)->asc('id')->findAllByColumn('id'); + $results = array(); - /** - * Get all files for a given task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAll($task_id) - { - return $this->getQuery()->eq('task_id', $task_id)->findAll(); - } + foreach ($file_ids as $file_id) { + $results[] = $this->remove($file_id); + } - /** - * Get all images for a given task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAllImages($task_id) - { - return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 1)->findAll(); + return ! in_array(false, $results, true); } /** - * Get all files without images for a given task + * Remove a file * * @access public - * @param integer $task_id Task id - * @return array + * @param integer $file_id File id + * @return bool */ - public function getAllDocuments($task_id) + public function remove($file_id) { - return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 0)->findAll(); + try { + $file = $this->getById($file_id); + $this->objectStorage->remove($file['path']); + + if ($file['is_image'] == 1) { + $this->objectStorage->remove($this->getThumbnailPath($file['path'])); + } + + return $this->db->table(static::TABLE)->eq('id', $file['id'])->remove(); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } } /** @@ -202,127 +189,96 @@ class File extends Base } /** - * Return the image mimetype based on the file extension + * Generate the path for a thumbnails * * @access public - * @param $filename + * @param string $key Storage key * @return string */ - public function getImageMimeType($filename) + public function getThumbnailPath($key) { - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - switch ($extension) { - case 'jpeg': - case 'jpg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - case 'gif': - return 'image/gif'; - default: - return 'image/jpeg'; - } + return 'thumbnails'.DIRECTORY_SEPARATOR.$key; } /** * Generate the path for a new filename * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id + * @param integer $id Foreign key * @param string $filename Filename * @return string */ - public function generatePath($project_id, $task_id, $filename) + public function generatePath($id, $filename) { - return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); + return static::PATH_PREFIX.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); } /** - * Generate the path for a thumbnails + * Upload multiple files * * @access public - * @param string $key Storage key - * @return string - */ - public function getThumbnailPath($key) - { - return 'thumbnails'.DIRECTORY_SEPARATOR.$key; - } - - /** - * Handle file upload - * - * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $form_name File form name + * @param integer $id + * @param array $files * @return bool */ - public function uploadFiles($project_id, $task_id, $form_name) + public function uploadFiles($id, array $files) { try { - $file = $this->request->getFileInfo($form_name); - - if (empty($file)) { + if (empty($files)) { return false; } - foreach ($file['error'] as $key => $error) { - if ($error == UPLOAD_ERR_OK && $file['size'][$key] > 0) { - $original_filename = $file['name'][$key]; - $uploaded_filename = $file['tmp_name'][$key]; - $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); - - if ($this->isImage($original_filename)) { - $this->generateThumbnailFromFile($uploaded_filename, $destination_filename); - } - - $this->objectStorage->moveUploadedFile($uploaded_filename, $destination_filename); - - $this->create( - $task_id, - $original_filename, - $destination_filename, - $file['size'][$key] - ); - } + foreach (array_keys($files['error']) as $key) { + $file = array( + 'name' => $files['name'][$key], + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + ); + + $this->uploadFile($id, $file); } return true; - } catch (ObjectStorageException $e) { + } catch (Exception $e) { $this->logger->error($e->getMessage()); return false; } } /** - * Handle screenshot upload + * Upload a file * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $blob Base64 encoded image - * @return bool|integer + * @param integer $id + * @param array $file */ - public function uploadScreenshot($project_id, $task_id, $blob) + public function uploadFile($id, array $file) { - $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png'; - return $this->uploadContent($project_id, $task_id, $original_filename, $blob); + if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { + $destination_filename = $this->generatePath($id, $file['name']); + + if ($this->isImage($file['name'])) { + $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename); + } + + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); + $this->create($id, $file['name'], $destination_filename, $file['size']); + } else { + throw new Exception('File not uploaded: '.var_export($file['error'], true)); + } } /** * Handle file upload (base64 encoded content) * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $original_filename Filename - * @param string $blob Base64 encoded file + * @param integer $id + * @param string $original_filename + * @param string $blob * @return bool|integer */ - public function uploadContent($project_id, $task_id, $original_filename, $blob) + public function uploadContent($id, $original_filename, $blob) { try { $data = base64_decode($blob); @@ -331,7 +287,7 @@ class File extends Base return false; } - $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); + $destination_filename = $this->generatePath($id, $original_filename); $this->objectStorage->put($destination_filename, $data); if ($this->isImage($original_filename)) { @@ -339,7 +295,7 @@ class File extends Base } return $this->create( - $task_id, + $id, $original_filename, $destination_filename, strlen($data) diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php index f5be020e..feb5f5a3 100644 --- a/app/Model/LastLogin.php +++ b/app/Model/LastLogin.php @@ -39,14 +39,14 @@ class LastLogin extends Base $this->cleanup($user_id); return $this->db - ->table(self::TABLE) - ->insert(array( - 'auth_type' => $auth_type, - 'user_id' => $user_id, - 'ip' => $ip, - 'user_agent' => substr($user_agent, 0, 255), - 'date_creation' => time(), - )); + ->table(self::TABLE) + ->insert(array( + 'auth_type' => $auth_type, + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => substr($user_agent, 0, 255), + 'date_creation' => time(), + )); } /** @@ -65,9 +65,9 @@ class LastLogin extends Base if (count($connections) >= self::NB_LOGINS) { $this->db->table(self::TABLE) - ->eq('user_id', $user_id) - ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) - ->remove(); + ->eq('user_id', $user_id) + ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) + ->remove(); } } diff --git a/app/Model/Link.php b/app/Model/Link.php index 7b81a237..903a98d6 100644 --- a/app/Model/Link.php +++ b/app/Model/Link.php @@ -90,7 +90,7 @@ class Link extends Base * * @access public * @param integer $exclude_id Exclude this link - * @param booelan $prepend Prepend default value + * @param boolean $prepend Prepend default value * @return array */ public function getList($exclude_id = 0, $prepend = true) diff --git a/app/Model/Metadata.php b/app/Model/Metadata.php index 690b2265..cb66c717 100644 --- a/app/Model/Metadata.php +++ b/app/Model/Metadata.php @@ -76,6 +76,7 @@ abstract class Metadata extends Base * @access public * @param integer $entity_id * @param array $values + * @return boolean */ public function save($entity_id, array $values) { diff --git a/app/Model/Notification.php b/app/Model/Notification.php index 87c1a796..c252aa31 100644 --- a/app/Model/Notification.php +++ b/app/Model/Notification.php @@ -72,7 +72,7 @@ class Notification extends Base return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); case Comment::EVENT_CREATE: return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); - case File::EVENT_CREATE: + case TaskFile::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); case Task::EVENT_USER_MENTION: return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); @@ -94,7 +94,7 @@ class Notification extends Base public function getTitleWithoutAuthor($event_name, array $event_data) { switch ($event_name) { - case File::EVENT_CREATE: + case TaskFile::EVENT_CREATE: return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); case Comment::EVENT_CREATE: return e('New comment on task #%d', $event_data['comment']['task_id']); diff --git a/app/Model/NotificationType.php b/app/Model/NotificationType.php index 9d8317b7..289aae9c 100644 --- a/app/Model/NotificationType.php +++ b/app/Model/NotificationType.php @@ -80,7 +80,7 @@ abstract class NotificationType extends Base * * @access public * @param string $type - * @return NotificationInterface + * @return \Kanboard\Notification\NotificationInterface */ public function getType($type) { diff --git a/app/Model/OverdueNotification.php b/app/Model/OverdueNotification.php deleted file mode 100644 index 84565548..00000000 --- a/app/Model/OverdueNotification.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Task Overdue Notification model - * - * @package model - * @author Frederic Guillot - */ -class OverdueNotification extends Base -{ - /** - * Send overdue tasks - * - * @access public - */ - public function sendOverdueTaskNotifications() - { - $tasks = $this->taskFinder->getOverdueTasks(); - - foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { - $users = $this->userNotification->getUsersWithNotificationEnabled($project_id); - - foreach ($users as $user) { - $this->sendUserOverdueTaskNotifications($user, $project_tasks); - } - } - - return $tasks; - } - - /** - * Send overdue tasks for a given user - * - * @access public - * @param array $user - * @param array $tasks - */ - public function sendUserOverdueTaskNotifications(array $user, array $tasks) - { - $user_tasks = array(); - - foreach ($tasks as $task) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, array('task' => $task))) { - $user_tasks[] = $task; - } - } - - if (! empty($user_tasks)) { - $this->userNotification->sendUserNotification( - $user, - Task::EVENT_OVERDUE, - array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) - ); - } - } -} diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php index c2d7dde9..5cfd3c97 100644 --- a/app/Model/PasswordReset.php +++ b/app/Model/PasswordReset.php @@ -20,7 +20,7 @@ class PasswordReset extends Base /** * Token duration (30 minutes) * - * @var string + * @var integer */ const DURATION = 1800; diff --git a/app/Model/Project.php b/app/Model/Project.php index d0a8bfc8..d2e5b7ce 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -241,7 +241,7 @@ class Project extends Base { $stats = array(); $stats['nb_active_tasks'] = 0; - $columns = $this->board->getColumns($project_id); + $columns = $this->column->getAll($project_id); $column_stats = $this->board->getColumnStats($project_id); foreach ($columns as &$column) { @@ -265,7 +265,7 @@ class Project extends Base */ public function getColumnStats(array &$project) { - $project['columns'] = $this->board->getColumns($project['id']); + $project['columns'] = $this->column->getAll($project['id']); $stats = $this->board->getColumnStats($project['id']); foreach ($project['columns'] as &$column) { @@ -334,7 +334,7 @@ class Project extends Base $values['identifier'] = strtoupper($values['identifier']); } - $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); @@ -402,7 +402,7 @@ class Project extends Base $values['identifier'] = strtoupper($values['identifier']); } - $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); return $this->exists($values['id']) && $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); diff --git a/app/Model/ProjectDailyColumnStats.php b/app/Model/ProjectDailyColumnStats.php index cf79be84..2bcc4d55 100644 --- a/app/Model/ProjectDailyColumnStats.php +++ b/app/Model/ProjectDailyColumnStats.php @@ -84,7 +84,7 @@ class ProjectDailyColumnStats extends Base */ public function getAggregatedMetrics($project_id, $from, $to, $field = 'total') { - $columns = $this->board->getColumnsList($project_id); + $columns = $this->column->getList($project_id); $metrics = $this->getMetrics($project_id, $from, $to); return $this->buildAggregate($metrics, $columns, $field); } diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php index f0c66834..9c5f80ad 100644 --- a/app/Model/ProjectDuplication.php +++ b/app/Model/ProjectDuplication.php @@ -2,6 +2,8 @@ namespace Kanboard\Model; +use Kanboard\Core\Security\Role; + /** * Project Duplication * @@ -12,6 +14,28 @@ namespace Kanboard\Model; class ProjectDuplication extends Base { /** + * Get list of optional models to duplicate + * + * @access public + * @return string[] + */ + public function getOptionalSelection() + { + return array('category', 'projectPermission', 'action', 'swimlane', 'task'); + } + + /** + * Get list of all possible models to duplicate + * + * @access public + * @return string[] + */ + public function getPossibleSelection() + { + return array('board', 'category', 'projectPermission', 'action', 'swimlane', 'task'); + } + + /** * Get a valid project name for the duplication * * @access public @@ -31,78 +55,106 @@ class ProjectDuplication extends Base } /** - * Create a project from another one - * - * @param integer $project_id Project Id - * @return integer Cloned Project Id - */ - public function copy($project_id) - { - $project = $this->project->getById($project_id); - - $values = array( - 'name' => $this->getClonedProjectName($project['name']), - 'is_active' => true, - 'last_modified' => 0, - 'token' => '', - 'is_public' => 0, - 'is_private' => empty($project['is_private']) ? 0 : 1, - ); - - if (! $this->db->table(Project::TABLE)->save($values)) { - return 0; - } - - return $this->db->getLastId(); - } - - /** * Clone a project with all settings * - * @param integer $project_id Project Id - * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task' - * @return integer Cloned Project Id + * @param integer $src_project_id Project Id + * @param array $selection Selection of optional project parts to duplicate + * @param integer $owner_id Owner of the project + * @param string $name Name of the project + * @param boolean $private Force the project to be private + * @return integer Cloned Project Id */ - public function duplicate($project_id, $part_selection = array('category', 'action')) + public function duplicate($src_project_id, $selection = array('projectPermission', 'category', 'action'), $owner_id = 0, $name = null, $private = null) { $this->db->startTransaction(); // Get the cloned project Id - $clone_project_id = $this->copy($project_id); + $dst_project_id = $this->copy($src_project_id, $owner_id, $name, $private); - if (! $clone_project_id) { + if ($dst_project_id === false) { $this->db->cancelTransaction(); return false; } // Clone Columns, Categories, Permissions and Actions - $optional_parts = array('swimlane', 'action', 'category'); - foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) { + foreach ($this->getPossibleSelection() as $model) { // Skip if optional part has not been selected - if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) { + if (in_array($model, $this->getOptionalSelection()) && ! in_array($model, $selection)) { continue; } - if (! $this->$model->duplicate($project_id, $clone_project_id)) { + // Skip permissions for private projects + if ($private && $model === 'projectPermission') { + continue; + } + + if (! $this->$model->duplicate($src_project_id, $dst_project_id)) { $this->db->cancelTransaction(); return false; } } + if (! $this->makeOwnerManager($dst_project_id, $owner_id)) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); - // Clone Tasks if in $part_selection - if (in_array('task', $part_selection)) { - $tasks = $this->taskFinder->getAll($project_id); + return (int) $dst_project_id; + } + + /** + * Create a project from another one + * + * @access private + * @param integer $src_project_id + * @param integer $owner_id + * @param string $name + * @param boolean $private + * @return integer + */ + private function copy($src_project_id, $owner_id = 0, $name = null, $private = null) + { + $project = $this->project->getById($src_project_id); + $is_private = empty($project['is_private']) ? 0 : 1; + + $values = array( + 'name' => $name ?: $this->getClonedProjectName($project['name']), + 'is_active' => 1, + 'last_modified' => time(), + 'token' => '', + 'is_public' => 0, + 'is_private' => $private ? 1 : $is_private, + 'owner_id' => $owner_id, + ); + + if (! $this->db->table(Project::TABLE)->save($values)) { + return false; + } + + return $this->db->getLastId(); + } + + /** + * Make sure that the creator of the duplicated project is alsp owner + * + * @access private + * @param integer $dst_project_id + * @param integer $owner_id + * @return boolean + */ + private function makeOwnerManager($dst_project_id, $owner_id) + { + if ($owner_id > 0) { + $this->projectUserRole->removeUser($dst_project_id, $owner_id); - foreach ($tasks as $task) { - if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) { - return false; - } + if (! $this->projectUserRole->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) { + return false; } } - return (int) $clone_project_id; + return true; } } diff --git a/app/Model/ProjectFile.php b/app/Model/ProjectFile.php new file mode 100644 index 00000000..aa9bf15b --- /dev/null +++ b/app/Model/ProjectFile.php @@ -0,0 +1,40 @@ +<?php + +namespace Kanboard\Model; + +/** + * Project File Model + * + * @package model + * @author Frederic Guillot + */ +class ProjectFile extends File +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_has_files'; + + /** + * SQL foreign key + * + * @var string + */ + const FOREIGN_KEY = 'project_id'; + + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'projects'; + + /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'project.file.create'; +} diff --git a/app/Model/ProjectGroupRole.php b/app/Model/ProjectGroupRole.php index 591b28c6..afad4a44 100644 --- a/app/Model/ProjectGroupRole.php +++ b/app/Model/ProjectGroupRole.php @@ -106,6 +106,7 @@ class ProjectGroupRole extends Base ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE) ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE) ->eq(self::TABLE.'.project_id', $project_id) + ->eq(User::TABLE.'.is_active', 1) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->asc(User::TABLE.'.username') ->findAll(); @@ -165,7 +166,7 @@ class ProjectGroupRole extends Base * Copy group access from a project to another one * * @param integer $project_src_id Project Template - * @return integer $project_dst_id Project that receives the copy + * @param integer $project_dst_id Project that receives the copy * @return boolean */ public function duplicate($project_src_id, $project_dst_id) diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index cea62e13..db1573ae 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -107,7 +107,8 @@ class ProjectPermission extends Base */ public function isAssignable($project_id, $user_id) { - return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + return $this->user->isActive($user_id) && + in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); } /** diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php index 8149a253..56da679c 100644 --- a/app/Model/ProjectUserRole.php +++ b/app/Model/ProjectUserRole.php @@ -152,13 +152,14 @@ class ProjectUserRole extends Base public function getAssignableUsers($project_id) { if ($this->projectPermission->isEverybodyAllowed($project_id)) { - return $this->user->getList(); + return $this->user->getActiveUsersList(); } $userMembers = $this->db->table(self::TABLE) ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) + ->eq(User::TABLE.'.is_active', 1) + ->eq(self::TABLE.'.project_id', $project_id) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->findAll(); diff --git a/app/Model/Setting.php b/app/Model/Setting.php index 44e6c065..6d29c6ec 100644 --- a/app/Model/Setting.php +++ b/app/Model/Setting.php @@ -75,6 +75,7 @@ abstract class Setting extends Base * * @access public * @param array $values + * @return boolean */ public function save(array $values) { diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php index 0e039bb3..3f5cfe83 100644 --- a/app/Model/Subtask.php +++ b/app/Model/Subtask.php @@ -168,8 +168,8 @@ class Subtask extends Base */ public function prepare(array &$values) { - $this->removeFields($values, array('another_subtask')); - $this->resetFields($values, array('time_estimated', 'time_spent')); + $this->helper->model->removeFields($values, array('another_subtask')); + $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); } /** @@ -263,89 +263,36 @@ class Subtask extends Base } /** - * Get subtasks with consecutive positions - * - * If you remove a subtask, the positions are not anymore consecutives - * - * @access public - * @param integer $task_id - * @return array - */ - public function getNormalizedPositions($task_id) - { - $subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position'); - $position = 1; - - foreach ($subtasks as $subtask_id => $subtask_position) { - $subtasks[$subtask_id] = $position++; - } - - return $subtasks; - } - - /** - * Save the new positions for a set of subtasks - * - * @access public - * @param array $subtasks Hashmap of column_id/column_position - * @return boolean - */ - public function savePositions(array $subtasks) - { - return $this->db->transaction(function (Database $db) use ($subtasks) { - - foreach ($subtasks as $subtask_id => $position) { - if (! $db->table(Subtask::TABLE)->eq('id', $subtask_id)->update(array('position' => $position))) { - return false; - } - } - }); - } - - /** - * Move a subtask down, increment the position value + * Save subtask position * * @access public * @param integer $task_id * @param integer $subtask_id + * @param integer $position * @return boolean */ - public function moveDown($task_id, $subtask_id) + public function changePosition($task_id, $subtask_id, $position) { - $subtasks = $this->getNormalizedPositions($task_id); - $positions = array_flip($subtasks); - - if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] < count($subtasks)) { - $position = ++$subtasks[$subtask_id]; - $subtasks[$positions[$position]]--; - - return $this->savePositions($subtasks); + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) { + return false; } - 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(); - /** - * Move a subtask up, decrement the position value - * - * @access public - * @param integer $task_id - * @param integer $subtask_id - * @return boolean - */ - public function moveUp($task_id, $subtask_id) - { - $subtasks = $this->getNormalizedPositions($task_id); - $positions = array_flip($subtasks); - - if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) { - $position = --$subtasks[$subtask_id]; - $subtasks[$positions[$position]]++; + foreach ($subtask_ids as $current_subtask_id) { + if ($offset == $position) { + $offset++; + } - return $this->savePositions($subtasks); + $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); + $offset++; } - return false; + $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); } /** @@ -353,15 +300,16 @@ class Subtask extends Base * * @access public * @param integer $subtask_id - * @return bool + * @return boolean|integer */ public function toggleStatus($subtask_id) { $subtask = $this->getById($subtask_id); + $status = ($subtask['status'] + 1) % 3; $values = array( 'id' => $subtask['id'], - 'status' => ($subtask['status'] + 1) % 3, + 'status' => $status, 'task_id' => $subtask['task_id'], ); @@ -369,7 +317,7 @@ class Subtask extends Base $values['user_id'] = $this->userSession->getId(); } - return $this->update($values); + return $this->update($values) ? $status : false; } /** @@ -435,10 +383,10 @@ class Subtask extends Base return $this->db->transaction(function (Database $db) use ($src_task_id, $dst_task_id) { $subtasks = $db->table(Subtask::TABLE) - ->columns('title', 'time_estimated', 'position') - ->eq('task_id', $src_task_id) - ->asc('position') - ->findAll(); + ->columns('title', 'time_estimated', 'position') + ->eq('task_id', $src_task_id) + ->asc('position') + ->findAll(); foreach ($subtasks as &$subtask) { $subtask['task_id'] = $dst_task_id; diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php index a741dbb5..b766b542 100644 --- a/app/Model/SubtaskTimeTracking.php +++ b/app/Model/SubtaskTimeTracking.php @@ -302,11 +302,11 @@ class SubtaskTimeTracking extends Base { $hook = 'model:subtask-time-tracking:calculate:time-spent'; $start_time = $this->db - ->table(self::TABLE) - ->eq('subtask_id', $subtask_id) - ->eq('user_id', $user_id) - ->eq('end', 0) - ->findOneColumn('start'); + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->findOneColumn('start'); if (empty($start_time)) { return 0; diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php index e5124e8e..721f20d3 100644 --- a/app/Model/Swimlane.php +++ b/app/Model/Swimlane.php @@ -96,10 +96,11 @@ class Swimlane extends Base */ public function getDefault($project_id) { - $result = $this->db->table(Project::TABLE) - ->eq('id', $project_id) - ->columns('id', 'default_swimlane', 'show_default_swimlane') - ->findOne(); + $result = $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->columns('id', 'default_swimlane', 'show_default_swimlane') + ->findOne(); if ($result['default_swimlane'] === 'Default swimlane') { $result['default_swimlane'] = t($result['default_swimlane']); @@ -117,10 +118,11 @@ class Swimlane extends Base */ public function getAll($project_id) { - return $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->orderBy('position', 'asc') - ->findAll(); + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->findAll(); } /** @@ -133,9 +135,10 @@ class Swimlane extends Base */ public function getAllByStatus($project_id, $status = self::ACTIVE) { - $query = $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', $status); + $query = $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status); if ($status == self::ACTIVE) { $query->asc('position'); @@ -155,17 +158,19 @@ class Swimlane extends Base */ public function getSwimlanes($project_id) { - $swimlanes = $this->db->table(self::TABLE) - ->columns('id', 'name', 'description') - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->orderBy('position', 'asc') - ->findAll(); - - $default_swimlane = $this->db->table(Project::TABLE) - ->eq('id', $project_id) - ->eq('show_default_swimlane', 1) - ->findOneColumn('default_swimlane'); + $swimlanes = $this->db + ->table(self::TABLE) + ->columns('id', 'name', 'description') + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->orderBy('position', 'asc') + ->findAll(); + + $default_swimlane = $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('show_default_swimlane', 1) + ->findOneColumn('default_swimlane'); if ($default_swimlane) { if ($default_swimlane === 'Default swimlane') { @@ -200,11 +205,12 @@ class Swimlane extends Base $swimlanes[0] = $default === 'Default swimlane' ? t($default) : $default; } - return $swimlanes + $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE)) - ->orderBy('position', 'asc') - ->getAll('id', 'name'); + return $swimlanes + $this->db + ->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE)) + ->orderBy('position', 'asc') + ->getAll('id', 'name'); } /** @@ -232,9 +238,10 @@ class Swimlane extends Base */ public function update(array $values) { - return $this->db->table(self::TABLE) - ->eq('id', $values['id']) - ->update($values); + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update($values); } /** @@ -247,12 +254,46 @@ class Swimlane extends Base public function updateDefault(array $values) { return $this->db - ->table(Project::TABLE) - ->eq('id', $values['id']) - ->update(array( - 'default_swimlane' => $values['default_swimlane'], - 'show_default_swimlane' => $values['show_default_swimlane'], - )); + ->table(Project::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'default_swimlane' => $values['default_swimlane'], + 'show_default_swimlane' => $values['show_default_swimlane'], + )); + } + + /** + * Enable the default swimlane + * + * @access public + * @param integer $project_id + * @return bool + */ + public function enableDefault($project_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->update(array( + 'show_default_swimlane' => 1, + )); + } + + /** + * Disable the default swimlane + * + * @access public + * @param integer $project_id + * @return bool + */ + public function disableDefault($project_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->update(array( + 'show_default_swimlane' => 0, + )); } /** @@ -264,10 +305,11 @@ class Swimlane extends Base */ public function getLastPosition($project_id) { - return $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', 1) - ->count() + 1; + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->count() + 1; } /** @@ -281,12 +323,12 @@ class Swimlane extends Base public function disable($project_id, $swimlane_id) { $result = $this->db - ->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array( - 'is_active' => self::INACTIVE, - 'position' => 0, - )); + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::INACTIVE, + 'position' => 0, + )); if ($result) { // Re-order positions @@ -307,12 +349,12 @@ class Swimlane extends Base public function enable($project_id, $swimlane_id) { return $this->db - ->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array( - 'is_active' => self::ACTIVE, - 'position' => $this->getLastPosition($project_id), - )); + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::ACTIVE, + 'position' => $this->getLastPosition($project_id), + )); } /** @@ -353,11 +395,13 @@ class Swimlane extends Base public function updatePositions($project_id) { $position = 0; - $swimlanes = $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', 1) - ->asc('position') - ->findAllByColumn('id'); + $swimlanes = $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->asc('position') + ->asc('id') + ->findAllByColumn('id'); if (! $swimlanes) { return false; @@ -365,77 +409,50 @@ class Swimlane extends Base foreach ($swimlanes as $swimlane_id) { $this->db->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array('position' => ++$position)); + ->eq('id', $swimlane_id) + ->update(array('position' => ++$position)); } return true; } /** - * Move a swimlane down, increment the position value + * Change swimlane position * * @access public - * @param integer $project_id Project id - * @param integer $swimlane_id Swimlane id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $position * @return boolean */ - public function moveDown($project_id, $swimlane_id) + public function changePosition($project_id, $swimlane_id, $position) { - $swimlanes = $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->asc('position') - ->getAll('id', 'position'); - - $positions = array_flip($swimlanes); - - if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) { - $position = ++$swimlanes[$swimlane_id]; - $swimlanes[$positions[$position]]--; - - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); - $this->db->closeTransaction(); - - return true; + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) { + return false; } - return false; - } + $swimlane_ids = $this->db->table(self::TABLE) + ->eq('is_active', 1) + ->eq('project_id', $project_id) + ->neq('id', $swimlane_id) + ->asc('position') + ->findAllByColumn('id'); - /** - * Move a swimlane up, decrement the position value - * - * @access public - * @param integer $project_id Project id - * @param integer $swimlane_id Swimlane id - * @return boolean - */ - public function moveUp($project_id, $swimlane_id) - { - $swimlanes = $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->asc('position') - ->getAll('id', 'position'); - - $positions = array_flip($swimlanes); - - if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) { - $position = --$swimlanes[$swimlane_id]; - $swimlanes[$positions[$position]]++; + $offset = 1; + $results = array(); - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); - $this->db->closeTransaction(); + foreach ($swimlane_ids as $current_swimlane_id) { + if ($offset == $position) { + $offset++; + } - return true; + $results[] = $this->db->table(self::TABLE)->eq('id', $current_swimlane_id)->update(array('position' => $offset)); + $offset++; } - return false; + $results[] = $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); } /** diff --git a/app/Model/Task.php b/app/Model/Task.php index 7aa9e312..f8b41b9f 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -42,6 +42,7 @@ class Task extends Base const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; const EVENT_OVERDUE = 'task.overdue'; const EVENT_USER_MENTION = 'task.user.mention'; + const EVENT_DAILY_CRONJOB = 'task.cronjob.daily'; /** * Recurrence: status @@ -91,7 +92,7 @@ class Task extends Base return false; } - $this->file->removeAll($task_id); + $this->taskFile->removeAll($task_id); return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); } @@ -198,4 +199,25 @@ class Task extends Base return round(($position * 100) / count($columns), 1); } + + /** + * Helper method to duplicate all tasks to another project + * + * @access public + * @param integer $src_project_id + * @param integer $dst_project_id + * @return boolean + */ + public function duplicate($src_project_id, $dst_project_id) + { + $task_ids = $this->taskFinder->getAllIds($src_project_id, array(Task::STATUS_OPEN, Task::STATUS_CLOSED)); + + foreach ($task_ids as $task_id) { + if (! $this->taskDuplication->duplicateToProject($task_id, $dst_project_id)) { + return false; + } + } + + return true; + } } diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php index bdfec3cb..cff56744 100644 --- a/app/Model/TaskAnalytic.php +++ b/app/Model/TaskAnalytic.php @@ -48,7 +48,7 @@ class TaskAnalytic extends Base public function getTimeSpentByColumn(array $task) { $result = array(); - $columns = $this->board->getColumnsList($task['project_id']); + $columns = $this->column->getList($task['project_id']); $sums = $this->transition->getTimeSpentByTask($task['id']); foreach ($columns as $column_id => $column_title) { diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index 975cc7a3..2d2e5504 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -49,13 +49,14 @@ class TaskCreation extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due')); - $this->dateParser->convert($values, array('date_started'), true); - $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $values = $this->dateParser->convert($values, array('date_due')); + $values = $this->dateParser->convert($values, array('date_started'), true); + + $this->helper->model->removeFields($values, array('another_task')); + $this->helper->model->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $this->column->getFirstColumnId($values['project_id']); } if (empty($values['color_id'])) { diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php index e81fb232..ebdd4d29 100644 --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -64,7 +64,7 @@ class TaskDuplication extends Base if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { $values['recurrence_parent'] = $task_id; - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $this->column->getFirstColumnId($values['project_id']); $this->calculateRecurringTaskDueDate($values); $recurring_task_id = $this->save($task_id, $values); @@ -155,6 +155,7 @@ class TaskDuplication extends Base * * @access public * @param array $values + * @return array */ public function checkDestinationProjectValues(array &$values) { @@ -181,12 +182,12 @@ class TaskDuplication extends Base // Check if the column exists for the destination project if ($values['column_id'] > 0) { - $values['column_id'] = $this->board->getColumnIdByTitle( + $values['column_id'] = $this->column->getColumnIdByTitle( $values['project_id'], - $this->board->getColumnTitleById($values['column_id']) + $this->column->getColumnTitleById($values['column_id']) ); - $values['column_id'] = $values['column_id'] ?: $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $values['column_id'] ?: $this->column->getFirstColumnId($values['project_id']); } return $values; diff --git a/app/Model/TaskExternalLink.php b/app/Model/TaskExternalLink.php new file mode 100644 index 00000000..f2c756b4 --- /dev/null +++ b/app/Model/TaskExternalLink.php @@ -0,0 +1,99 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task External Link Model + * + * @package model + * @author Frederic Guillot + */ +class TaskExternalLink extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_external_links'; + + /** + * Get all links + * + * @access public + * @param integer $task_id + * @return array + */ + public function getAll($task_id) + { + $types = $this->externalLinkManager->getTypes(); + + $links = $this->db->table(self::TABLE) + ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username') + ->eq('task_id', $task_id) + ->asc('title') + ->join(User::TABLE, 'id', 'creator_id') + ->findAll(); + + foreach ($links as &$link) { + $link['dependency_label'] = $this->externalLinkManager->getDependencyLabel($link['link_type'], $link['dependency']); + $link['type'] = isset($types[$link['link_type']]) ? $types[$link['link_type']] : t('Unknown'); + } + + return $links; + } + + /** + * Get link + * + * @access public + * @param integer $link_id + * @return array + */ + public function getById($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne(); + } + + /** + * Add a new link in the database + * + * @access public + * @param array $values Form values + * @return boolean|integer + */ + public function create(array $values) + { + unset($values['id']); + $values['creator_id'] = $this->userSession->getId(); + $values['date_creation'] = time(); + $values['date_modification'] = $values['date_creation']; + + return $this->persist(self::TABLE, $values); + } + + /** + * Modify external link + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function update(array $values) + { + $values['date_modification'] = time(); + return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + } + + /** + * Remove a link + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function remove($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); + } +} diff --git a/app/Model/TaskFile.php b/app/Model/TaskFile.php new file mode 100644 index 00000000..45a3b97f --- /dev/null +++ b/app/Model/TaskFile.php @@ -0,0 +1,54 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task File Model + * + * @package model + * @author Frederic Guillot + */ +class TaskFile extends File +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_files'; + + /** + * SQL foreign key + * + * @var string + */ + const FOREIGN_KEY = 'task_id'; + + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'tasks'; + + /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'task.file.create'; + + /** + * Handle screenshot upload + * + * @access public + * @param integer $task_id Task id + * @param string $blob Base64 encoded image + * @return bool|integer + */ + public function uploadScreenshot($task_id, $blob) + { + $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; + return $this->uploadContent($task_id, $original_filename, $blob); + } +} diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index 7ceb4a97..1883298d 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -469,7 +469,7 @@ class TaskFilter extends Base $this->query->beginOr(); foreach ($values as $project) { - $this->query->ilike(Board::TABLE.'.title', $project); + $this->query->ilike(Column::TABLE.'.title', $project); } $this->query->closeOr(); diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 1c83136b..0492a9bf 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -38,14 +38,14 @@ class TaskFinder extends Base Task::TABLE.'.time_spent', Task::TABLE.'.time_estimated', Project::TABLE.'.name AS project_name', - Board::TABLE.'.title AS column_name', + Column::TABLE.'.title AS column_name', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name' ) ->eq(Task::TABLE.'.is_active', $is_active) ->in(Project::TABLE.'.id', $project_ids) ->join(Project::TABLE, 'id', 'project_id') - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE); } @@ -88,11 +88,12 @@ class TaskFinder extends Base return $this->db ->table(Task::TABLE) ->columns( - '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', - '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files', - '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', - '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', - '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', + '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', + '(SELECT COUNT(*) FROM '.TaskFile::TABLE.' WHERE task_id=tasks.id) AS nb_files', + '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', + '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', + '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links', '(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone', 'tasks.id', 'tasks.reference', @@ -128,15 +129,15 @@ class TaskFinder extends Base User::TABLE.'.name AS assignee_name', Category::TABLE.'.name AS category_name', Category::TABLE.'.description AS category_description', - Board::TABLE.'.title AS column_name', - Board::TABLE.'.position AS column_position', + Column::TABLE.'.title AS column_name', + Column::TABLE.'.position AS column_position', Swimlane::TABLE.'.name AS swimlane_name', Project::TABLE.'.default_swimlane', Project::TABLE.'.name AS project_name' ) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) ->join(Category::TABLE, 'id', 'category_id', Task::TABLE) - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE) ->join(Project::TABLE, 'id', 'project_id', Task::TABLE); } @@ -179,6 +180,23 @@ class TaskFinder extends Base } /** + * Get all tasks for a given project and status + * + * @access public + * @param integer $project_id + * @param array $status + * @return array + */ + public function getAllIds($project_id, array $status = array(Task::STATUS_OPEN)) + { + return $this->db + ->table(Task::TABLE) + ->eq(Task::TABLE.'.project_id', $project_id) + ->in(Task::TABLE.'.is_active', $status) + ->findAllByColumn('id'); + } + + /** * Get overdue tasks query * * @access public diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php index 87aae55e..e46ea476 100644 --- a/app/Model/TaskLink.php +++ b/app/Model/TaskLink.php @@ -75,22 +75,24 @@ class TaskLink extends Base Task::TABLE.'.title', Task::TABLE.'.is_active', Task::TABLE.'.project_id', + Task::TABLE.'.column_id', + Task::TABLE.'.color_id', Task::TABLE.'.time_spent AS task_time_spent', Task::TABLE.'.time_estimated AS task_time_estimated', Task::TABLE.'.owner_id AS task_assignee_id', User::TABLE.'.username AS task_assignee_username', User::TABLE.'.name AS task_assignee_name', - Board::TABLE.'.title AS column_title', + Column::TABLE.'.title AS column_title', Project::TABLE.'.name AS project_name' ) ->eq(self::TABLE.'.task_id', $task_id) ->join(Link::TABLE, 'id', 'link_id') ->join(Task::TABLE, 'id', 'opposite_task_id') - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) ->asc(Link::TABLE.'.id') - ->desc(Board::TABLE.'.position') + ->desc(Column::TABLE.'.position') ->desc(Task::TABLE.'.is_active') ->asc(Task::TABLE.'.position') ->asc(Task::TABLE.'.id') diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php index eee7b2e0..a77b78a4 100644 --- a/app/Model/TaskModification.php +++ b/app/Model/TaskModification.php @@ -84,11 +84,12 @@ class TaskModification extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due')); - $this->dateParser->convert($values, array('date_started'), true); - $this->removeFields($values, array('another_task', 'id')); - $this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); - $this->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); + $values = $this->dateParser->convert($values, array('date_due')); + $values = $this->dateParser->convert($values, array('date_started'), true); + + $this->helper->model->removeFields($values, array('another_task', 'id')); + $this->helper->model->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); + $this->helper->model->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); $values['date_modification'] = time(); } diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php index fac2153e..b1e02589 100644 --- a/app/Model/TaskPermission.php +++ b/app/Model/TaskPermission.php @@ -18,7 +18,8 @@ class TaskPermission extends Base * Regular users can't remove tasks from other people * * @public - * @return boolean + * @param array $task + * @return bool */ public function canRemoveTask(array $task) { diff --git a/app/Model/Transition.php b/app/Model/Transition.php index b1f8f678..870d95fd 100644 --- a/app/Model/Transition.php +++ b/app/Model/Transition.php @@ -3,7 +3,7 @@ namespace Kanboard\Model; /** - * Transition model + * Transition * * @package model * @author Frederic Guillot @@ -21,20 +21,22 @@ class Transition extends Base * Save transition event * * @access public - * @param integer $user_id - * @param array $task - * @return boolean + * @param integer $user_id + * @param array $task_event + * @return bool */ - public function save($user_id, array $task) + public function save($user_id, array $task_event) { + $time = time(); + return $this->db->table(self::TABLE)->insert(array( 'user_id' => $user_id, - 'project_id' => $task['project_id'], - 'task_id' => $task['task_id'], - 'src_column_id' => $task['src_column_id'], - 'dst_column_id' => $task['dst_column_id'], - 'date' => time(), - 'time_spent' => time() - $task['date_moved'] + 'project_id' => $task_event['project_id'], + 'task_id' => $task_event['task_id'], + 'src_column_id' => $task_event['src_column_id'], + 'dst_column_id' => $task_event['dst_column_id'], + 'date' => $time, + 'time_spent' => $time - $task_event['date_moved'] )); } @@ -76,8 +78,8 @@ class Transition extends Base ->eq('task_id', $task_id) ->desc('date') ->join(User::TABLE, 'id', 'user_id') - ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } @@ -116,71 +118,11 @@ class Transition extends Base ->lte('date', $to) ->eq(self::TABLE.'.project_id', $project_id) ->desc('date') + ->desc(self::TABLE.'.id') ->join(Task::TABLE, 'id', 'task_id') ->join(User::TABLE, 'id', 'user_id') - ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } - - /** - * Get project export - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function export($project_id, $from, $to) - { - $results = array($this->getColumns()); - $transitions = $this->getAllByProjectAndDate($project_id, $from, $to); - - foreach ($transitions as $transition) { - $results[] = $this->format($transition); - } - - return $results; - } - - /** - * Get column titles - * - * @access public - * @return string[] - */ - public function getColumns() - { - return array( - e('Id'), - e('Task Title'), - e('Source column'), - e('Destination column'), - e('Executer'), - e('Date'), - e('Time spent'), - ); - } - - /** - * Format the output of a transition array - * - * @access public - * @param array $transition - * @return array - */ - public function format(array $transition) - { - $values = array(); - $values[] = $transition['id']; - $values[] = $transition['title']; - $values[] = $transition['src_column']; - $values[] = $transition['dst_column']; - $values[] = $transition['name'] ?: $transition['username']; - $values[] = date('Y-m-d H:i', $transition['date']); - $values[] = round($transition['time_spent'] / 3600, 2); - - return $values; - } } diff --git a/app/Model/User.php b/app/Model/User.php index ac0e7491..0e11422b 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -41,6 +41,18 @@ class User extends Base } /** + * Return true if the user is active + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function isActive($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->eq('is_active', 1)->exists(); + } + + /** * Get query to fetch all users * * @access public @@ -48,20 +60,7 @@ class User extends Base */ public function getQuery() { - return $this->db - ->table(self::TABLE) - ->columns( - 'id', - 'username', - 'name', - 'email', - 'role', - 'is_ldap_user', - 'notifications_enabled', - 'google_id', - 'github_id', - 'twofactor_activated' - ); + return $this->db->table(self::TABLE); } /** @@ -138,7 +137,7 @@ class User extends Base * * @access public * @param string $username Username - * @return array + * @return integer */ public function getIdByUsername($username) { @@ -206,9 +205,9 @@ class User extends Base * @param boolean $prepend Prepend "All users" * @return array */ - public function getList($prepend = false) + public function getActiveUsersList($prepend = false) { - $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll(); + $users = $this->db->table(self::TABLE)->eq('is_active', 1)->columns('id', 'username', 'name')->findAll(); $listing = $this->prepareList($users); if ($prepend) { @@ -254,10 +253,10 @@ class User extends Base } } - $this->removeFields($values, array('confirmation', 'current_password')); - $this->resetFields($values, array('is_ldap_user', 'disable_login_form')); - $this->convertNullFields($values, array('gitlab_id')); - $this->convertIntegerFields($values, array('gitlab_id')); + $this->helper->model->removeFields($values, array('confirmation', 'current_password')); + $this->helper->model->resetFields($values, array('is_ldap_user', 'disable_login_form')); + $this->helper->model->convertNullFields($values, array('gitlab_id')); + $this->helper->model->convertIntegerFields($values, array('gitlab_id')); } /** @@ -278,7 +277,7 @@ class User extends Base * * @access public * @param array $values Form values - * @return array + * @return boolean */ public function update(array $values) { @@ -294,6 +293,30 @@ class User extends Base } /** + * Disable a specific user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function disable($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 0)); + } + + /** + * Enable a specific user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function enable($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 1)); + } + + /** * Remove a specific user * * @access public diff --git a/app/Notification/Mail.php b/app/Notification/Mail.php index d05dbdf2..c924fb50 100644 --- a/app/Notification/Mail.php +++ b/app/Notification/Mail.php @@ -4,7 +4,7 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; use Kanboard\Model\Task; -use Kanboard\Model\File; +use Kanboard\Model\TaskFile; use Kanboard\Model\Comment; use Kanboard\Model\Subtask; @@ -82,7 +82,7 @@ class Mail extends Base implements NotificationInterface public function getMailSubject($event_name, array $event_data) { switch ($event_name) { - case File::EVENT_CREATE: + case TaskFile::EVENT_CREATE: $subject = $this->getStandardMailSubject(e('New attachment'), $event_data); break; case Comment::EVENT_CREATE: diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 8f1db510..9a551e99 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,56 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 103; +const VERSION = 107; + +function version_107(PDO $pdo) +{ + $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'"); +} + +function version_106(PDO $pdo) +{ + $pdo->exec('RENAME TABLE files TO task_has_files'); + + $pdo->exec(" + CREATE TABLE project_has_files ( + `id` INT NOT NULL AUTO_INCREMENT, + `project_id` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + `path` VARCHAR(255) NOT NULL, + `is_image` TINYINT(1) DEFAULT 0, + `size` INT DEFAULT 0 NOT NULL, + `user_id` INT DEFAULT 0 NOT NULL, + `date` INT DEFAULT 0 NOT NULL, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8" + ); +} + +function version_105(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_active TINYINT(1) DEFAULT 1"); +} + +function version_104(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_external_links ( + id INT NOT NULL AUTO_INCREMENT, + link_type VARCHAR(100) NOT NULL, + dependency VARCHAR(100) NOT NULL, + title VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + date_creation INT NOT NULL, + date_modification INT NOT NULL, + task_id INT NOT NULL, + creator_id INT DEFAULT 0, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_103(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index a7bf8054..6aed1491 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,54 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 83; +const VERSION = 87; + +function version_87(PDO $pdo) +{ + $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'"); +} + +function version_86(PDO $pdo) +{ + $pdo->exec('ALTER TABLE files RENAME TO task_has_files'); + + $pdo->exec(" + CREATE TABLE project_has_files ( + id SERIAL PRIMARY KEY, + project_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + path VARCHAR(255) NOT NULL, + is_image BOOLEAN DEFAULT '0', + size INTEGER DEFAULT 0 NOT NULL, + user_id INTEGER DEFAULT 0 NOT NULL, + date INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )" + ); +} + +function version_85(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT '1'"); +} + +function version_84(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_external_links ( + id SERIAL PRIMARY KEY, + link_type VARCHAR(100) NOT NULL, + dependency VARCHAR(100) NOT NULL, + title VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + date_creation INT NOT NULL, + date_modification INT NOT NULL, + task_id INT NOT NULL, + creator_id INT DEFAULT 0, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ) + "); +} function version_83(PDO $pdo) { diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql index 4b26aec7..b5620400 100644 --- a/app/Schema/Sql/mysql.sql +++ b/app/Schema/Sql/mysql.sql @@ -29,7 +29,7 @@ CREATE TABLE `actions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `project_id` int(11) NOT NULL, `event_name` varchar(50) NOT NULL, - `action_name` varchar(50) NOT NULL, + `action_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `project_id` (`project_id`), CONSTRAINT `actions_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE @@ -95,23 +95,6 @@ CREATE TABLE `custom_filters` ( CONSTRAINT `custom_filters_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -DROP TABLE IF EXISTS `files`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `files` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL, - `path` varchar(255) DEFAULT NULL, - `is_image` tinyint(1) DEFAULT '0', - `task_id` int(11) NOT NULL, - `date` bigint(20) DEFAULT NULL, - `user_id` int(11) NOT NULL DEFAULT '0', - `size` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `files_task_idx` (`task_id`), - CONSTRAINT `files_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `group_has_users`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -142,7 +125,7 @@ CREATE TABLE `last_logins` ( `id` int(11) NOT NULL AUTO_INCREMENT, `auth_type` varchar(25) DEFAULT NULL, `user_id` int(11) DEFAULT NULL, - `ip` varchar(40) DEFAULT NULL, + `ip` varchar(45) DEFAULT NULL, `user_agent` varchar(255) DEFAULT NULL, `date_creation` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), @@ -161,6 +144,22 @@ CREATE TABLE `links` ( UNIQUE KEY `label` (`label`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `password_reset`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `password_reset` ( + `token` varchar(80) NOT NULL, + `user_id` int(11) NOT NULL, + `date_expiration` int(11) NOT NULL, + `date_creation` int(11) NOT NULL, + `ip` varchar(45) NOT NULL, + `user_agent` varchar(255) NOT NULL, + `is_active` tinyint(1) NOT NULL, + PRIMARY KEY (`token`), + KEY `user_id` (`user_id`), + CONSTRAINT `password_reset_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `plugin_schema_versions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -237,6 +236,23 @@ CREATE TABLE `project_has_categories` ( CONSTRAINT `project_has_categories_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `project_has_files`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `project_has_files` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `project_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `path` varchar(255) NOT NULL, + `is_image` tinyint(1) DEFAULT '0', + `size` int(11) NOT NULL DEFAULT '0', + `user_id` int(11) NOT NULL DEFAULT '0', + `date` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `project_id` (`project_id`), + CONSTRAINT `project_has_files_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `project_has_groups`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -304,6 +320,10 @@ CREATE TABLE `projects` ( `identifier` varchar(50) DEFAULT '', `start_date` varchar(10) DEFAULT '', `end_date` varchar(10) DEFAULT '', + `owner_id` int(11) DEFAULT '0', + `priority_default` int(11) DEFAULT '0', + `priority_start` int(11) DEFAULT '0', + `priority_end` int(11) DEFAULT '3', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -313,7 +333,7 @@ DROP TABLE IF EXISTS `remember_me`; CREATE TABLE `remember_me` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, - `ip` varchar(40) DEFAULT NULL, + `ip` varchar(45) DEFAULT NULL, `user_agent` varchar(255) DEFAULT NULL, `token` varchar(255) DEFAULT NULL, `sequence` varchar(255) DEFAULT NULL, @@ -390,6 +410,41 @@ CREATE TABLE `swimlanes` ( CONSTRAINT `swimlanes_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `task_has_external_links`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `task_has_external_links` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `link_type` varchar(100) NOT NULL, + `dependency` varchar(100) NOT NULL, + `title` varchar(255) NOT NULL, + `url` varchar(255) NOT NULL, + `date_creation` int(11) NOT NULL, + `date_modification` int(11) NOT NULL, + `task_id` int(11) NOT NULL, + `creator_id` int(11) DEFAULT '0', + PRIMARY KEY (`id`), + KEY `task_id` (`task_id`), + CONSTRAINT `task_has_external_links_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `task_has_files`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `task_has_files` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `path` varchar(255) DEFAULT NULL, + `is_image` tinyint(1) DEFAULT '0', + `task_id` int(11) NOT NULL, + `date` bigint(20) DEFAULT NULL, + `user_id` int(11) NOT NULL DEFAULT '0', + `size` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `files_task_idx` (`task_id`), + CONSTRAINT `task_has_files_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `task_has_links`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -451,6 +506,7 @@ CREATE TABLE `tasks` ( `recurrence_basedate` int(11) NOT NULL DEFAULT '0', `recurrence_parent` int(11) DEFAULT NULL, `recurrence_child` int(11) DEFAULT NULL, + `priority` int(11) DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_task_active` (`is_active`), KEY `column_id` (`column_id`), @@ -558,6 +614,7 @@ CREATE TABLE `users` ( `lock_expiration_date` bigint(20) DEFAULT NULL, `gitlab_id` int(11) DEFAULT NULL, `role` varchar(25) NOT NULL DEFAULT 'app-user', + `is_active` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `users_username_idx` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -581,7 +638,7 @@ CREATE TABLE `users` ( LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; -INSERT INTO `settings` VALUES ('api_token','a5ee9b5e8a97d1da2b725d5f3d66caec8b5fe195968c33f8081badd17c35'),('application_currency','USD'),('application_date_format','m/d/Y'),('application_language','en_US'),('application_stylesheet',''),('application_timezone','UTC'),('application_url',''),('board_columns',''),('board_highlight_period','172800'),('board_private_refresh_interval','10'),('board_public_refresh_interval','60'),('calendar_project_tasks','date_started'),('calendar_user_subtasks_time_tracking','0'),('calendar_user_tasks','date_started'),('cfd_include_closed_tasks','1'),('default_color','yellow'),('integration_gravatar','0'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','bb74b2b3d79f562520d1f495b6c0c1f9ff1d996d1d60374fd67b47637239'),('webhook_url',''); +INSERT INTO `settings` VALUES ('api_token','cd9c46c6bdaa6afc49b3385dabe0b78c059bc124b1f72c2f47c9ca604cf1'),('application_currency','USD'),('application_date_format','m/d/Y'),('application_language','en_US'),('application_stylesheet',''),('application_timezone','UTC'),('application_url',''),('board_columns',''),('board_highlight_period','172800'),('board_private_refresh_interval','10'),('board_public_refresh_interval','60'),('calendar_project_tasks','date_started'),('calendar_user_subtasks_time_tracking','0'),('calendar_user_tasks','date_started'),('cfd_include_closed_tasks','1'),('default_color','yellow'),('integration_gravatar','0'),('password_reset','1'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','32387b121de8fe6031a6b71b7b1b9cae411a909539aa9d494cf69ac5f2ee'),('webhook_url',''); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -610,4 +667,4 @@ UNLOCK TABLES; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$EIYcgiDba33chGTqlP4cYeXSF7WC5RJPr8eMGsx.8gVT1Q4OUgkay', 'app-admin');INSERT INTO schema_version VALUES ('98'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('107'); diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql index bca9dd73..c613ddb4 100644 --- a/app/Schema/Sql/postgres.sql +++ b/app/Schema/Sql/postgres.sql @@ -68,7 +68,7 @@ CREATE TABLE actions ( id integer NOT NULL, project_id integer NOT NULL, event_name character varying(50) NOT NULL, - action_name character varying(50) NOT NULL + action_name character varying(255) NOT NULL ); @@ -202,22 +202,6 @@ ALTER SEQUENCE custom_filters_id_seq OWNED BY custom_filters.id; -- --- Name: files; Type: TABLE; Schema: public; Owner: -; Tablespace: --- - -CREATE TABLE files ( - id integer NOT NULL, - name character varying(255) NOT NULL, - path character varying(255), - is_image boolean DEFAULT false, - task_id integer NOT NULL, - date bigint DEFAULT 0 NOT NULL, - user_id integer DEFAULT 0 NOT NULL, - size integer DEFAULT 0 NOT NULL -); - - --- -- Name: group_has_users; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -265,7 +249,7 @@ CREATE TABLE last_logins ( id integer NOT NULL, auth_type character varying(25), user_id integer, - ip character varying(40), + ip character varying(45), user_agent character varying(255), date_creation bigint ); @@ -321,6 +305,21 @@ ALTER SEQUENCE links_id_seq OWNED BY links.id; -- +-- Name: password_reset; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE password_reset ( + token character varying(80) NOT NULL, + user_id integer NOT NULL, + date_expiration integer NOT NULL, + date_creation integer NOT NULL, + ip character varying(45) NOT NULL, + user_agent character varying(255) NOT NULL, + is_active boolean NOT NULL +); + + +-- -- Name: plugin_schema_versions; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -461,6 +460,41 @@ ALTER SEQUENCE project_has_categories_id_seq OWNED BY project_has_categories.id; -- +-- Name: project_has_files; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE project_has_files ( + id integer NOT NULL, + project_id integer NOT NULL, + name character varying(255) NOT NULL, + path character varying(255) NOT NULL, + is_image boolean DEFAULT false, + size integer DEFAULT 0 NOT NULL, + user_id integer DEFAULT 0 NOT NULL, + date integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: project_has_files_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE project_has_files_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_has_files_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE project_has_files_id_seq OWNED BY project_has_files.id; + + +-- -- Name: project_has_groups; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -541,7 +575,11 @@ CREATE TABLE projects ( description text, identifier character varying(50) DEFAULT ''::character varying, start_date character varying(10) DEFAULT ''::character varying, - end_date character varying(10) DEFAULT ''::character varying + end_date character varying(10) DEFAULT ''::character varying, + owner_id integer DEFAULT 0, + priority_default integer DEFAULT 0, + priority_start integer DEFAULT 0, + priority_end integer DEFAULT 3 ); @@ -571,7 +609,7 @@ ALTER SEQUENCE projects_id_seq OWNED BY projects.id; CREATE TABLE remember_me ( id integer NOT NULL, user_id integer, - ip character varying(40), + ip character varying(45), user_agent character varying(255), token character varying(255), sequence character varying(255), @@ -701,6 +739,58 @@ ALTER SEQUENCE swimlanes_id_seq OWNED BY swimlanes.id; -- +-- Name: task_has_external_links; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE task_has_external_links ( + id integer NOT NULL, + link_type character varying(100) NOT NULL, + dependency character varying(100) NOT NULL, + title character varying(255) NOT NULL, + url character varying(255) NOT NULL, + date_creation integer NOT NULL, + date_modification integer NOT NULL, + task_id integer NOT NULL, + creator_id integer DEFAULT 0 +); + + +-- +-- Name: task_has_external_links_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE task_has_external_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: task_has_external_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE task_has_external_links_id_seq OWNED BY task_has_external_links.id; + + +-- +-- Name: task_has_files; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE task_has_files ( + id integer NOT NULL, + name character varying(255) NOT NULL, + path character varying(255), + is_image boolean DEFAULT false, + task_id integer NOT NULL, + date bigint DEFAULT 0 NOT NULL, + user_id integer DEFAULT 0 NOT NULL, + size integer DEFAULT 0 NOT NULL +); + + +-- -- Name: task_has_files_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -716,7 +806,7 @@ CREATE SEQUENCE task_has_files_id_seq -- Name: task_has_files_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE task_has_files_id_seq OWNED BY files.id; +ALTER SEQUENCE task_has_files_id_seq OWNED BY task_has_files.id; -- @@ -813,7 +903,8 @@ CREATE TABLE tasks ( recurrence_timeframe integer DEFAULT 0 NOT NULL, recurrence_basedate integer DEFAULT 0 NOT NULL, recurrence_parent integer, - recurrence_child integer + recurrence_child integer, + priority integer DEFAULT 0 ); @@ -978,7 +1069,8 @@ CREATE TABLE users ( nb_failed_login integer DEFAULT 0, lock_expiration_date bigint DEFAULT 0, gitlab_id integer, - role character varying(25) DEFAULT 'app-user'::character varying NOT NULL + role character varying(25) DEFAULT 'app-user'::character varying NOT NULL, + is_active boolean DEFAULT true ); @@ -1040,13 +1132,6 @@ ALTER TABLE ONLY custom_filters ALTER COLUMN id SET DEFAULT nextval('custom_filt -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY files ALTER COLUMN id SET DEFAULT nextval('task_has_files_id_seq'::regclass); - - --- --- Name: id; Type: DEFAULT; Schema: public; Owner: - --- - ALTER TABLE ONLY groups ALTER COLUMN id SET DEFAULT nextval('groups_id_seq'::regclass); @@ -1096,6 +1181,13 @@ ALTER TABLE ONLY project_has_categories ALTER COLUMN id SET DEFAULT nextval('pro -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- +ALTER TABLE ONLY project_has_files ALTER COLUMN id SET DEFAULT nextval('project_has_files_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + ALTER TABLE ONLY project_has_notification_types ALTER COLUMN id SET DEFAULT nextval('project_has_notification_types_id_seq'::regclass); @@ -1138,6 +1230,20 @@ ALTER TABLE ONLY swimlanes ALTER COLUMN id SET DEFAULT nextval('swimlanes_id_seq -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- +ALTER TABLE ONLY task_has_external_links ALTER COLUMN id SET DEFAULT nextval('task_has_external_links_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY task_has_files ALTER COLUMN id SET DEFAULT nextval('task_has_files_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + ALTER TABLE ONLY task_has_links ALTER COLUMN id SET DEFAULT nextval('task_has_links_id_seq'::regclass); @@ -1281,6 +1387,14 @@ ALTER TABLE ONLY links -- +-- Name: password_reset_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY password_reset + ADD CONSTRAINT password_reset_pkey PRIMARY KEY (token); + + +-- -- Name: plugin_schema_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1329,6 +1443,14 @@ ALTER TABLE ONLY project_has_categories -- +-- Name: project_has_files_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY project_has_files + ADD CONSTRAINT project_has_files_pkey PRIMARY KEY (id); + + +-- -- Name: project_has_groups_group_id_project_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1417,10 +1539,18 @@ ALTER TABLE ONLY swimlanes -- +-- Name: task_has_external_links_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY task_has_external_links + ADD CONSTRAINT task_has_external_links_pkey PRIMARY KEY (id); + + +-- -- Name: task_has_files_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- -ALTER TABLE ONLY files +ALTER TABLE ONLY task_has_files ADD CONSTRAINT task_has_files_pkey PRIMARY KEY (id); @@ -1536,7 +1666,7 @@ CREATE INDEX comments_task_idx ON comments USING btree (task_id); -- Name: files_task_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: -- -CREATE INDEX files_task_idx ON files USING btree (task_id); +CREATE INDEX files_task_idx ON task_has_files USING btree (task_id); -- @@ -1687,6 +1817,14 @@ ALTER TABLE ONLY last_logins -- +-- Name: password_reset_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY password_reset + ADD CONSTRAINT password_reset_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- -- Name: project_activities_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1743,6 +1881,14 @@ ALTER TABLE ONLY project_has_categories -- +-- Name: project_has_files_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY project_has_files + ADD CONSTRAINT project_has_files_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + + +-- -- Name: project_has_groups_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1823,10 +1969,18 @@ ALTER TABLE ONLY swimlanes -- +-- Name: task_has_external_links_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY task_has_external_links + ADD CONSTRAINT task_has_external_links_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + + +-- -- Name: task_has_files_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY files +ALTER TABLE ONLY task_has_files ADD CONSTRAINT task_has_files_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; @@ -1967,16 +2121,6 @@ ALTER TABLE ONLY user_has_unread_notifications -- --- Name: public; Type: ACL; Schema: -; Owner: - --- - -REVOKE ALL ON SCHEMA public FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM fred; -GRANT ALL ON SCHEMA public TO fred; -GRANT ALL ON SCHEMA public TO PUBLIC; - - --- -- PostgreSQL database dump complete -- @@ -2001,8 +2145,8 @@ INSERT INTO settings (option, value) VALUES ('board_highlight_period', '172800') INSERT INTO settings (option, value) VALUES ('board_public_refresh_interval', '60'); INSERT INTO settings (option, value) VALUES ('board_private_refresh_interval', '10'); INSERT INTO settings (option, value) VALUES ('board_columns', ''); -INSERT INTO settings (option, value) VALUES ('webhook_token', '686b427497298001641d9d300d53b0b23da6a4a2cc0ce17d24d346219fca'); -INSERT INTO settings (option, value) VALUES ('api_token', '3387e930ebe3984d59eac1fb07bb112916c846cfe2f01b513349c24fc045'); +INSERT INTO settings (option, value) VALUES ('webhook_token', 'c7caaf8f87ad391800e3989d7abfd98a6066a6f801fc151012bb5c4ee3cb'); +INSERT INTO settings (option, value) VALUES ('api_token', 'b0a6f56fe236fc9639fc6914e92365aa627d95cd790aa7e0c5a3ebebf844'); INSERT INTO settings (option, value) VALUES ('application_language', 'en_US'); INSERT INTO settings (option, value) VALUES ('application_timezone', 'UTC'); INSERT INTO settings (option, value) VALUES ('application_url', ''); @@ -2019,6 +2163,7 @@ INSERT INTO settings (option, value) VALUES ('webhook_url', ''); INSERT INTO settings (option, value) VALUES ('default_color', 'yellow'); INSERT INTO settings (option, value) VALUES ('subtask_time_tracking', '1'); INSERT INTO settings (option, value) VALUES ('cfd_include_closed_tasks', '1'); +INSERT INTO settings (option, value) VALUES ('password_reset', '1'); -- @@ -2066,4 +2211,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true); -- PostgreSQL database dump complete -- -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$EIYcgiDba33chGTqlP4cYeXSF7WC5RJPr8eMGsx.8gVT1Q4OUgkay', 'app-admin');INSERT INTO schema_version VALUES ('78'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('87'); diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 08689749..8bcad291 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,54 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 95; +const VERSION = 99; + +function version_99(PDO $pdo) +{ + $pdo->exec("UPDATE project_activities SET event_name='task.file.create' WHERE event_name='file.create'"); +} + +function version_98(PDO $pdo) +{ + $pdo->exec('ALTER TABLE files RENAME TO task_has_files'); + + $pdo->exec(" + CREATE TABLE project_has_files ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + name TEXT COLLATE NOCASE NOT NULL, + path TEXT NOT NULL, + is_image INTEGER DEFAULT 0, + size INTEGER DEFAULT 0 NOT NULL, + user_id INTEGER DEFAULT 0 NOT NULL, + date INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )" + ); +} + +function version_97(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1"); +} + +function version_96(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_external_links ( + id INTEGER PRIMARY KEY, + link_type TEXT NOT NULL, + dependency TEXT NOT NULL, + title TEXT NOT NULL, + url TEXT NOT NULL, + date_creation INTEGER NOT NULL, + date_modification INTEGER NOT NULL, + task_id INTEGER NOT NULL, + creator_id INTEGER DEFAULT 0, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ) + "); +} function version_95(PDO $pdo) { diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php index 0aba29f1..3692f190 100644 --- a/app/ServiceProvider/ActionProvider.php +++ b/app/ServiceProvider/ActionProvider.php @@ -23,12 +23,14 @@ use Kanboard\Action\TaskCloseColumn; use Kanboard\Action\TaskCreation; use Kanboard\Action\TaskDuplicateAnotherProject; use Kanboard\Action\TaskEmail; +use Kanboard\Action\TaskEmailNoActivity; use Kanboard\Action\TaskMoveAnotherProject; use Kanboard\Action\TaskMoveColumnAssigned; use Kanboard\Action\TaskMoveColumnCategoryChange; use Kanboard\Action\TaskMoveColumnUnAssigned; use Kanboard\Action\TaskOpen; use Kanboard\Action\TaskUpdateStartDate; +use Kanboard\Action\TaskCloseNoActivity; /** * Action Provider @@ -63,9 +65,11 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskAssignUser($container)); $container['actionManager']->register(new TaskClose($container)); $container['actionManager']->register(new TaskCloseColumn($container)); + $container['actionManager']->register(new TaskCloseNoActivity($container)); $container['actionManager']->register(new TaskCreation($container)); $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); $container['actionManager']->register(new TaskEmail($container)); + $container['actionManager']->register(new TaskEmailNoActivity($container)); $container['actionManager']->register(new TaskMoveAnotherProject($container)); $container['actionManager']->register(new TaskMoveColumnAssigned($container)); $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index a516cffe..5ed28fe1 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -11,9 +11,6 @@ use Kanboard\Core\Security\Role; use Kanboard\Auth\RememberMeAuth; use Kanboard\Auth\DatabaseAuth; use Kanboard\Auth\LdapAuth; -use Kanboard\Auth\GitlabAuth; -use Kanboard\Auth\GithubAuth; -use Kanboard\Auth\GoogleAuth; use Kanboard\Auth\TotpAuth; use Kanboard\Auth\ReverseProxyAuth; @@ -47,18 +44,6 @@ class AuthenticationProvider implements ServiceProviderInterface $container['authenticationManager']->register(new LdapAuth($container)); } - if (GITLAB_AUTH) { - $container['authenticationManager']->register(new GitlabAuth($container)); - } - - if (GITHUB_AUTH) { - $container['authenticationManager']->register(new GithubAuth($container)); - } - - if (GOOGLE_AUTH) { - $container['authenticationManager']->register(new GoogleAuth($container)); - } - $container['projectAccessMap'] = $this->getProjectAccessMap(); $container['applicationAccessMap'] = $this->getApplicationAccessMap(); @@ -82,6 +67,8 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER)); $acl->add('Action', '*', Role::PROJECT_MANAGER); + $acl->add('ActionProject', '*', Role::PROJECT_MANAGER); + $acl->add('ActionCreation', '*', Role::PROJECT_MANAGER); $acl->add('Analytic', '*', Role::PROJECT_MANAGER); $acl->add('Board', 'save', Role::PROJECT_MEMBER); $acl->add('BoardPopover', '*', Role::PROJECT_MEMBER); @@ -91,22 +78,28 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Comment', '*', Role::PROJECT_MEMBER); $acl->add('Customfilter', '*', Role::PROJECT_MEMBER); $acl->add('Export', '*', Role::PROJECT_MANAGER); - $acl->add('File', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); + $acl->add('TaskFile', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); $acl->add('Gantt', '*', Role::PROJECT_MANAGER); $acl->add('Project', array('share', 'integrations', 'notifications', 'duplicate', 'disable', 'enable', 'remove'), Role::PROJECT_MANAGER); $acl->add('ProjectPermission', '*', Role::PROJECT_MANAGER); $acl->add('ProjectEdit', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectFile', '*', Role::PROJECT_MEMBER); $acl->add('Projectuser', '*', Role::PROJECT_MANAGER); $acl->add('Subtask', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskRestriction', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskStatus', '*', Role::PROJECT_MEMBER); $acl->add('Swimlane', '*', Role::PROJECT_MANAGER); $acl->add('Task', 'remove', Role::PROJECT_MEMBER); $acl->add('Taskcreation', '*', Role::PROJECT_MEMBER); $acl->add('Taskduplication', '*', Role::PROJECT_MEMBER); + $acl->add('TaskRecurrence', '*', Role::PROJECT_MEMBER); $acl->add('TaskImport', '*', Role::PROJECT_MANAGER); $acl->add('Tasklink', '*', Role::PROJECT_MEMBER); + $acl->add('Tasklink', array('show'), Role::PROJECT_VIEWER); + $acl->add('TaskExternalLink', '*', Role::PROJECT_MEMBER); + $acl->add('TaskExternalLink', array('show'), Role::PROJECT_VIEWER); $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER); $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER); - $acl->add('Timer', '*', Role::PROJECT_MEMBER); $acl->add('UserHelper', array('mention'), Role::PROJECT_MEMBER); return $acl; @@ -126,7 +119,6 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC)); $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC)); - $acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC); $acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC); $acl->add('Captcha', '*', Role::APP_PUBLIC); $acl->add('PasswordReset', '*', Role::APP_PUBLIC); @@ -141,12 +133,12 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Gantt', array('projects', 'saveProjectDate'), Role::APP_MANAGER); $acl->add('Group', '*', Role::APP_ADMIN); $acl->add('Link', '*', Role::APP_ADMIN); - $acl->add('Project', array('users', 'allowEverybody', 'allow', 'role', 'revoke', 'create'), Role::APP_MANAGER); - $acl->add('ProjectPermission', '*', Role::APP_USER); + $acl->add('ProjectCreation', 'create', Role::APP_MANAGER); $acl->add('Projectuser', '*', Role::APP_MANAGER); $acl->add('Twofactor', 'disable', Role::APP_ADMIN); $acl->add('UserImport', '*', Role::APP_ADMIN); - $acl->add('User', array('index', 'create', 'save', 'authentication', 'remove'), Role::APP_ADMIN); + $acl->add('User', array('index', 'create', 'save', 'authentication'), Role::APP_ADMIN); + $acl->add('UserStatus', '*', Role::APP_ADMIN); return $acl; } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index df4e183b..b883c905 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -27,19 +27,19 @@ class ClassProvider implements ServiceProviderInterface 'Board', 'Category', 'Color', + 'Column', 'Comment', 'Config', 'Currency', 'CustomFilter', - 'File', 'Group', 'GroupMember', 'LastLogin', 'Link', 'Notification', - 'OverdueNotification', 'PasswordReset', 'Project', + 'ProjectFile', 'ProjectActivity', 'ProjectDuplication', 'ProjectDailyColumnStats', @@ -53,26 +53,24 @@ class ClassProvider implements ServiceProviderInterface 'ProjectUserRoleFilter', 'RememberMeSession', 'Subtask', - 'SubtaskExport', 'SubtaskTimeTracking', 'Swimlane', 'Task', 'TaskAnalytic', 'TaskCreation', 'TaskDuplication', - 'TaskExport', + 'TaskExternalLink', 'TaskFinder', + 'TaskFile', 'TaskFilter', 'TaskLink', 'TaskModification', 'TaskPermission', 'TaskPosition', 'TaskStatus', - 'TaskImport', 'TaskMetadata', 'Transition', 'User', - 'UserImport', 'UserLocking', 'UserMention', 'UserNotification', @@ -97,6 +95,7 @@ class ClassProvider implements ServiceProviderInterface 'CommentValidator', 'CurrencyValidator', 'CustomFilterValidator', + 'ExternalLinkValidator', 'GroupValidator', 'LinkValidator', 'PasswordResetValidator', @@ -107,11 +106,18 @@ class ClassProvider implements ServiceProviderInterface 'TaskLinkValidator', 'UserValidator', ), + 'Import' => array( + 'TaskImport', + 'UserImport', + ), + 'Export' => array( + 'SubtaskExport', + 'TaskExport', + 'TransitionExport', + ), 'Core' => array( 'DateParser', - 'Helper', 'Lexer', - 'Template', ), 'Core\Event' => array( 'EventManager', diff --git a/app/ServiceProvider/ExternalLinkProvider.php b/app/ServiceProvider/ExternalLinkProvider.php new file mode 100644 index 00000000..c4bbc4cf --- /dev/null +++ b/app/ServiceProvider/ExternalLinkProvider.php @@ -0,0 +1,34 @@ +<?php + +namespace Kanboard\ServiceProvider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Kanboard\Core\ExternalLink\ExternalLinkManager; +use Kanboard\ExternalLink\WebLinkProvider; +use Kanboard\ExternalLink\AttachmentLinkProvider; + +/** + * External Link Provider + * + * @package serviceProvider + * @author Frederic Guillot + */ +class ExternalLinkProvider implements ServiceProviderInterface +{ + /** + * Register providers + * + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container + */ + public function register(Container $container) + { + $container['externalLinkManager'] = new ExternalLinkManager($container); + $container['externalLinkManager']->register(new WebLinkProvider($container)); + $container['externalLinkManager']->register(new AttachmentLinkProvider($container)); + + return $container; + } +} diff --git a/app/ServiceProvider/GroupProvider.php b/app/ServiceProvider/GroupProvider.php index dff4b23a..b222b218 100644 --- a/app/ServiceProvider/GroupProvider.php +++ b/app/ServiceProvider/GroupProvider.php @@ -26,7 +26,10 @@ class GroupProvider implements ServiceProviderInterface public function register(Container $container) { $container['groupManager'] = new GroupManager; - $container['groupManager']->register(new DatabaseBackendGroupProvider($container)); + + if (DB_GROUP_PROVIDER) { + $container['groupManager']->register(new DatabaseBackendGroupProvider($container)); + } if (LDAP_AUTH && LDAP_GROUP_PROVIDER) { $container['groupManager']->register(new LdapBackendGroupProvider($container)); diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php new file mode 100644 index 00000000..42a0c85e --- /dev/null +++ b/app/ServiceProvider/HelperProvider.php @@ -0,0 +1,34 @@ +<?php + +namespace Kanboard\ServiceProvider; + +use Kanboard\Core\Helper; +use Kanboard\Core\Template; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class HelperProvider implements ServiceProviderInterface +{ + public function register(Container $container) + { + $container['helper'] = new Helper($container); + $container['helper']->register('app', '\Kanboard\Helper\AppHelper'); + $container['helper']->register('asset', '\Kanboard\Helper\AssetHelper'); + $container['helper']->register('board', '\Kanboard\Helper\BoardHelper'); + $container['helper']->register('dt', '\Kanboard\Helper\DateHelper'); + $container['helper']->register('file', '\Kanboard\Helper\FileHelper'); + $container['helper']->register('form', '\Kanboard\Helper\FormHelper'); + $container['helper']->register('hook', '\Kanboard\Helper\HookHelper'); + $container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper'); + $container['helper']->register('model', '\Kanboard\Helper\ModelHelper'); + $container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper'); + $container['helper']->register('task', '\Kanboard\Helper\TaskHelper'); + $container['helper']->register('text', '\Kanboard\Helper\TextHelper'); + $container['helper']->register('url', '\Kanboard\Helper\UrlHelper'); + $container['helper']->register('user', '\Kanboard\Helper\UserHelper'); + + $container['template'] = new Template($container['helper']); + + return $container; + } +} diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php index 057a1b3c..d551f25d 100644 --- a/app/ServiceProvider/RouteProvider.php +++ b/app/ServiceProvider/RouteProvider.php @@ -38,18 +38,21 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks'); $container['route']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar'); $container['route']->addRoute('dashboard/:user_id/activity', 'app', 'activity'); + $container['route']->addRoute('dashboard/:user_id/notifications', 'app', 'notifications'); // Search routes $container['route']->addRoute('search', 'search', 'index'); $container['route']->addRoute('search/:search', 'search', 'index'); + // ProjectCreation routes + $container['route']->addRoute('project/create', 'ProjectCreation', 'create'); + $container['route']->addRoute('project/create/private', 'ProjectCreation', 'createPrivate'); + // Project routes $container['route']->addRoute('projects', 'project', 'index'); - $container['route']->addRoute('project/create', 'project', 'create'); - $container['route']->addRoute('project/create/private', 'project', 'createPrivate'); $container['route']->addRoute('project/:project_id', 'project', 'show'); $container['route']->addRoute('p/:project_id', 'project', 'show'); - $container['route']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index'); + $container['route']->addRoute('project/:project_id/customer-filters', 'customfilter', 'index'); $container['route']->addRoute('project/:project_id/share', 'project', 'share'); $container['route']->addRoute('project/:project_id/notifications', 'project', 'notifications'); $container['route']->addRoute('project/:project_id/integrations', 'project', 'integrations'); @@ -60,6 +63,9 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index'); $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1'); + // Project Overview + $container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show'); + // ProjectEdit routes $container['route']->addRoute('project/:project_id/edit', 'ProjectEdit', 'edit'); $container['route']->addRoute('project/:project_id/edit/dates', 'ProjectEdit', 'dates'); @@ -75,27 +81,15 @@ class RouteProvider implements ServiceProviderInterface // Action routes $container['route']->addRoute('project/:project_id/actions', 'action', 'index'); - $container['route']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm'); // Column routes $container['route']->addRoute('project/:project_id/columns', 'column', 'index'); - $container['route']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit'); - $container['route']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm'); - $container['route']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move'); // Swimlane routes $container['route']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup'); - $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown'); // Category routes $container['route']->addRoute('project/:project_id/categories', 'category', 'index'); - $container['route']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit'); - $container['route']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm'); // Task routes $container['route']->addRoute('project/:project_id/task/:task_id', 'task', 'show'); @@ -103,26 +97,12 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('public/task/:task_id/:token', 'task', 'readonly'); $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task'); - $container['route']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot'); - $container['route']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create'); - $container['route']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create'); - $container['route']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create'); $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions'); $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics'); - $container['route']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove'); - - $container['route']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit'); - $container['route']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description'); - $container['route']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence'); - - $container['route']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close'); - $container['route']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open'); - - $container['route']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate'); - $container['route']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy'); - $container['route']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy'); - $container['route']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move'); - $container['route']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move'); + $container['route']->addRoute('project/:project_id/task/:task_id/subtasks', 'subtask', 'show'); + $container['route']->addRoute('project/:project_id/task/:task_id/time-tracking', 'task', 'timetracking'); + $container['route']->addRoute('project/:project_id/task/:task_id/internal/links', 'tasklink', 'show'); + $container['route']->addRoute('project/:project_id/task/:task_id/external/links', 'TaskExternalLink', 'show'); // Exports $container['route']->addRoute('export/tasks/:project_id', 'export', 'tasks'); @@ -130,6 +110,15 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('export/transitions/:project_id', 'export', 'transitions'); $container['route']->addRoute('export/summary/:project_id', 'export', 'summary'); + // Analytics routes + $container['route']->addRoute('analytics/tasks/:project_id', 'analytic', 'tasks'); + $container['route']->addRoute('analytics/users/:project_id', 'analytic', 'users'); + $container['route']->addRoute('analytics/cfd/:project_id', 'analytic', 'cfd'); + $container['route']->addRoute('analytics/burndown/:project_id', 'analytic', 'burndown'); + $container['route']->addRoute('analytics/average-time-column/:project_id', 'analytic', 'averageTimeByColumn'); + $container['route']->addRoute('analytics/lead-cycle-time/:project_id', 'analytic', 'leadAndCycleTime'); + $container['route']->addRoute('analytics/estimated-spent-time/:project_id', 'analytic', 'compareHours'); + // Board routes $container['route']->addRoute('board/:project_id', 'board', 'show'); $container['route']->addRoute('b/:project_id', 'board', 'show'); @@ -147,11 +136,6 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('gantt/:project_id', 'gantt', 'project'); $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project'); - // Subtask routes - $container['route']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create'); - $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm'); - $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit'); - // Feed routes $container['route']->addRoute('feed/project/:token', 'feed', 'project'); $container['route']->addRoute('feed/user/:token', 'feed', 'user'); @@ -174,7 +158,6 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('user/:user_id/accounts', 'user', 'external'); $container['route']->addRoute('user/:user_id/integrations', 'user', 'integrations'); $container['route']->addRoute('user/:user_id/authentication', 'user', 'authentication'); - $container['route']->addRoute('user/:user_id/remove', 'user', 'remove'); $container['route']->addRoute('user/:user_id/2fa', 'twofactor', 'index'); // Groups @@ -205,9 +188,6 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('documentation', 'doc', 'show'); // Auth routes - $container['route']->addRoute('oauth/google', 'oauth', 'google'); - $container['route']->addRoute('oauth/github', 'oauth', 'github'); - $container['route']->addRoute('oauth/gitlab', 'oauth', 'gitlab'); $container['route']->addRoute('login', 'auth', 'login'); $container['route']->addRoute('logout', 'auth', 'logout'); diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php index e839385f..dfb95a00 100644 --- a/app/Subscriber/AuthSubscriber.php +++ b/app/Subscriber/AuthSubscriber.php @@ -89,6 +89,7 @@ class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface * Increment failed login counter * * @access public + * @param AuthFailureEvent $event */ public function onLoginFailure(AuthFailureEvent $event) { diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php index 07660050..651b8a96 100644 --- a/app/Subscriber/NotificationSubscriber.php +++ b/app/Subscriber/NotificationSubscriber.php @@ -6,7 +6,7 @@ use Kanboard\Event\GenericEvent; use Kanboard\Model\Task; use Kanboard\Model\Comment; use Kanboard\Model\Subtask; -use Kanboard\Model\File; +use Kanboard\Model\TaskFile; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class NotificationSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -28,7 +28,7 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn Comment::EVENT_CREATE => 'handleEvent', Comment::EVENT_UPDATE => 'handleEvent', Comment::EVENT_USER_MENTION => 'handleEvent', - File::EVENT_CREATE => 'handleEvent', + TaskFile::EVENT_CREATE => 'handleEvent', ); } diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php index 6d5aee89..09a5665a 100644 --- a/app/Subscriber/RecurringTaskSubscriber.php +++ b/app/Subscriber/RecurringTaskSubscriber.php @@ -21,9 +21,9 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI $this->logger->debug('Subscriber executed: '.__METHOD__); if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { - if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) { + if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->column->getFirstColumnId($event['project_id']) == $event['src_column_id']) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); - } elseif ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) { + } elseif ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->column->getLastColumnId($event['project_id']) == $event['dst_column_id']) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); } } diff --git a/app/Template/action/event.php b/app/Template/action/event.php deleted file mode 100644 index 7f968a97..00000000 --- a/app/Template/action/event.php +++ /dev/null @@ -1,25 +0,0 @@ -<div class="page-header"> - <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> -</div> - -<h3><?= t('Choose an event') ?></h3> -<form method="post" action="<?= $this->url->href('action', 'params', array('project_id' => $project['id'])) ?>"> - - <?= $this->form->csrf() ?> - - <?= $this->form->hidden('project_id', $values) ?> - <?= $this->form->hidden('action_name', $values) ?> - - <?= $this->form->label(t('Event'), 'event_name') ?> - <?= $this->form->select('event_name', $events, $values) ?><br/> - - <div class="form-help"> - <?= t('When the selected event occurs execute the corresponding action.') ?> - </div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> - </div> -</form>
\ No newline at end of file diff --git a/app/Template/action/index.php b/app/Template/action/index.php index 8275f080..63d63887 100644 --- a/app/Template/action/index.php +++ b/app/Template/action/index.php @@ -1,75 +1,71 @@ <div class="page-header"> <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <ul> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add a new action'), 'ActionCreation', 'create', array('project_id' => $project['id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-copy fa-fw"></i> + <?= $this->url->link(t('Import from another project'), 'ActionProject', 'project', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> </div> -<?php if (! empty($actions)): ?> +<?php if (empty($actions)): ?> + <p class="alert"><?= t('There is no action at the moment.') ?></p> +<?php else: ?> + <table> + <tr> + <th><?= t('Automatic actions') ?></th> + <th><?= t('Action parameters') ?></th> + <th><?= t('Action') ?></th> + </tr> -<h3><?= t('Defined actions') ?></h3> -<table> - <tr> - <th><?= t('Automatic actions') ?></th> - <th><?= t('Action parameters') ?></th> - <th><?= t('Action') ?></th> - </tr> - - <?php foreach ($actions as $action): ?> - <tr> - <td> - <ul> - <li> - <?= t('Event name') ?> = - <strong><?= $this->text->in($action['event_name'], $available_events) ?></strong> - </li> - <li> - <?= t('Action name') ?> = - <strong><?= $this->text->in($action['action_name'], $available_actions) ?></strong> - </li> - <ul> - </td> - <td> - <ul> - <?php foreach ($action['params'] as $param_name => $param_value): ?> - <li> - <?= $this->text->in($param_name, $available_params[$action['action_name']]) ?> = - <strong> - <?php if ($this->text->contains($param_name, 'column_id')): ?> - <?= $this->text->in($param_value, $columns_list) ?> - <?php elseif ($this->text->contains($param_name, 'user_id')): ?> - <?= $this->text->in($param_value, $users_list) ?> - <?php elseif ($this->text->contains($param_name, 'project_id')): ?> - <?= $this->text->in($param_value, $projects_list) ?> - <?php elseif ($this->text->contains($param_name, 'color_id')): ?> - <?= $this->text->in($param_value, $colors_list) ?> - <?php elseif ($this->text->contains($param_name, 'category_id')): ?> - <?= $this->text->in($param_value, $categories_list) ?> - <?php elseif ($this->text->contains($param_name, 'link_id')): ?> - <?= $this->text->in($param_value, $links_list) ?> - <?php else: ?> - <?= $this->e($param_value) ?> - <?php endif ?> - </strong> - </li> - <?php endforeach ?> - </ul> - </td> - <td> - <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id'])) ?> - </td> - </tr> - <?php endforeach ?> -</table> - -<?php endif ?> - -<h3><?= t('Add an action') ?></h3> -<form method="post" action="<?= $this->url->href('action', 'event', array('project_id' => $project['id'])) ?>" class="listing"> - <?= $this->form->csrf() ?> - <?= $this->form->hidden('project_id', $values) ?> - - <?= $this->form->label(t('Action'), 'action_name') ?> - <?= $this->form->select('action_name', $available_actions, $values) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file + <?php foreach ($actions as $action): ?> + <tr> + <td> + <ul> + <li> + <?= t('Event name') ?> = + <strong><?= $this->text->in($action['event_name'], $available_events) ?></strong> + </li> + <li> + <?= t('Action name') ?> = + <strong><?= $this->text->in($action['action_name'], $available_actions) ?></strong> + </li> + <ul> + </td> + <td> + <ul> + <?php foreach ($action['params'] as $param_name => $param_value): ?> + <li> + <?= $this->text->in($param_name, $available_params[$action['action_name']]) ?> = + <strong> + <?php if ($this->text->contains($param_name, 'column_id')): ?> + <?= $this->text->in($param_value, $columns_list) ?> + <?php elseif ($this->text->contains($param_name, 'user_id')): ?> + <?= $this->text->in($param_value, $users_list) ?> + <?php elseif ($this->text->contains($param_name, 'project_id')): ?> + <?= $this->text->in($param_value, $projects_list) ?> + <?php elseif ($this->text->contains($param_name, 'color_id')): ?> + <?= $this->text->in($param_value, $colors_list) ?> + <?php elseif ($this->text->contains($param_name, 'category_id')): ?> + <?= $this->text->in($param_value, $categories_list) ?> + <?php elseif ($this->text->contains($param_name, 'link_id')): ?> + <?= $this->text->in($param_value, $links_list) ?> + <?php else: ?> + <?= $this->text->e($param_value) ?> + <?php endif ?> + </strong> + </li> + <?php endforeach ?> + </ul> + </td> + <td> + <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id']), false, 'popover') ?> + </td> + </tr> + <?php endforeach ?> + </table> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/action/remove.php b/app/Template/action/remove.php index c8d4dfe4..070a7918 100644 --- a/app/Template/action/remove.php +++ b/app/Template/action/remove.php @@ -10,6 +10,6 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/action_creation/create.php b/app/Template/action_creation/create.php new file mode 100644 index 00000000..bccb19b3 --- /dev/null +++ b/app/Template/action_creation/create.php @@ -0,0 +1,16 @@ +<div class="page-header"> + <h2><?= t('Add an action') ?></h2> +</div> +<form class="popover-form" method="post" action="<?= $this->url->href('ActionCreation', 'event', array('project_id' => $project['id'])) ?>"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Action'), 'action_name') ?> + <?= $this->form->select('action_name', $available_actions, $values) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Next step') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'Action', 'index', array(), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/action_creation/event.php b/app/Template/action_creation/event.php new file mode 100644 index 00000000..e7e5aaf9 --- /dev/null +++ b/app/Template/action_creation/event.php @@ -0,0 +1,27 @@ +<div class="page-header"> + <h2><?= t('Choose an event') ?></h2> +</div> + +<form class="popover-form" method="post" action="<?= $this->url->href('ActionCreation', 'params', array('project_id' => $project['id'])) ?>"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->hidden('action_name', $values) ?> + + <?= $this->form->label(t('Action'), 'action_name') ?> + <?= $this->form->select('action_name', $available_actions, $values, array(), array('disabled')) ?> + + <?= $this->form->label(t('Event'), 'event_name') ?> + <?= $this->form->select('event_name', $events, $values) ?> + + <div class="form-help"> + <?= t('When the selected event occurs execute the corresponding action.') ?> + </div> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Next step') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/action/params.php b/app/Template/action_creation/params.php index dcfaa9cc..59ff6ce9 100644 --- a/app/Template/action/params.php +++ b/app/Template/action_creation/params.php @@ -1,9 +1,8 @@ <div class="page-header"> - <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <h2><?= t('Define action parameters') ?></h2> </div> -<h3><?= t('Define action parameters') ?></h3> -<form method="post" action="<?= $this->url->href('action', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('ActionCreation', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -11,36 +10,43 @@ <?= $this->form->hidden('event_name', $values) ?> <?= $this->form->hidden('action_name', $values) ?> - <?php foreach ($action_params as $param_name => $param_desc): ?> + <?= $this->form->label(t('Action'), 'action_name') ?> + <?= $this->form->select('action_name', $available_actions, $values, array(), array('disabled')) ?> + + <?= $this->form->label(t('Event'), 'event_name') ?> + <?= $this->form->select('event_name', $events, $values, array(), array('disabled')) ?> + <?php foreach ($action_params as $param_name => $param_desc): ?> <?php if ($this->text->contains($param_name, 'column_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?> <?php elseif ($this->text->contains($param_name, 'user_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?> <?php elseif ($this->text->contains($param_name, 'project_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?> <?php elseif ($this->text->contains($param_name, 'color_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?> <?php elseif ($this->text->contains($param_name, 'category_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?> <?php elseif ($this->text->contains($param_name, 'link_id')): ?> <?= $this->form->label($param_desc, $param_name) ?> - <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?><br/> + <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?> + <?php elseif ($this->text->contains($param_name, 'duration')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->number('params['.$param_name.']', $values) ?> <?php else: ?> <?= $this->form->label($param_desc, $param_name) ?> <?= $this->form->text('params['.$param_name.']', $values) ?> <?php endif ?> - <?php endforeach ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/action_project/project.php b/app/Template/action_project/project.php new file mode 100644 index 00000000..226f3b19 --- /dev/null +++ b/app/Template/action_project/project.php @@ -0,0 +1,20 @@ +<div class="page-header"> + <h2><?= t('Import actions from another project') ?></h2> +</div> +<?php if (empty($projects_list)): ?> + <p class="alert"><?= t('There is no available project.') ?></p> +<?php else: ?> + <form class="popover-form" method="post" action="<?= $this->url->href('ActionProject', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Create from another project'), 'src_project_id') ?> + <?= $this->form->select('src_project_id', $projects_list) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'Action', 'index', array(), false, 'close-popover') ?> + </div> + </form> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/activity/task.php b/app/Template/activity/task.php index cc4aad03..04c64c63 100644 --- a/app/Template/activity/task.php +++ b/app/Template/activity/task.php @@ -1,3 +1,7 @@ +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> + <div class="page-header"> <h2><?= t('Activity stream') ?></h2> </div> diff --git a/app/Template/analytic/avg_time_columns.php b/app/Template/analytic/avg_time_columns.php index 7b9d7cf9..5f6c6b35 100644 --- a/app/Template/analytic/avg_time_columns.php +++ b/app/Template/analytic/avg_time_columns.php @@ -16,7 +16,7 @@ </tr> <?php foreach ($metrics as $column): ?> <tr> - <td><?= $this->e($column['title']) ?></td> + <td><?= $this->text->e($column['title']) ?></td> <td><?= $this->dt->duration($column['average']) ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/analytic/burndown.php b/app/Template/analytic/burndown.php index 3dfb6ee8..ed6c8aeb 100644 --- a/app/Template/analytic/burndown.php +++ b/app/Template/analytic/burndown.php @@ -27,7 +27,7 @@ </div> <div class="form-inline-group"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form> diff --git a/app/Template/analytic/cfd.php b/app/Template/analytic/cfd.php index 45f53e0f..ee259c70 100644 --- a/app/Template/analytic/cfd.php +++ b/app/Template/analytic/cfd.php @@ -27,6 +27,6 @@ </div> <div class="form-inline-group"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form> diff --git a/app/Template/analytic/compare_hours.php b/app/Template/analytic/compare_hours.php index bb145d61..8249e7ba 100644 --- a/app/Template/analytic/compare_hours.php +++ b/app/Template/analytic/compare_hours.php @@ -4,8 +4,8 @@ <div class="listing"> <ul> - <li><?= t('Estimated hours: ').'<strong>'.$this->e($metrics['open']['time_estimated'] + $metrics['closed']['time_estimated']) ?></strong></li> - <li><?= t('Actual hours: ').'<strong>'.$this->e($metrics['open']['time_spent'] + $metrics['closed']['time_spent']) ?></strong></li> + <li><?= t('Estimated hours: ').'<strong>'.$this->text->e($metrics['open']['time_estimated'] + $metrics['closed']['time_estimated']) ?></strong></li> + <li><?= t('Actual hours: ').'<strong>'.$this->text->e($metrics['open']['time_spent'] + $metrics['closed']['time_spent']) ?></strong></li> </ul> </div> @@ -34,10 +34,10 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> <?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?> @@ -47,10 +47,10 @@ <?php endif ?> </td> <td> - <?= $this->e($task['time_estimated']) ?> + <?= $this->text->e($task['time_estimated']) ?> </td> <td> - <?= $this->e($task['time_spent']) ?> + <?= $this->text->e($task['time_spent']) ?> </td> </tr> <?php endforeach ?> diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php index ff532fc0..f1dba552 100644 --- a/app/Template/analytic/layout.php +++ b/app/Template/analytic/layout.php @@ -31,9 +31,8 @@ </li> </ul> </div> - <section class="sidebar-container" id="analytic-section"> - - <?= $this->render('analytic/sidebar', array('project' => $project)) ?> + <section class="sidebar-container"> + <?= $this->render($sidebar_template, array('project' => $project)) ?> <div class="sidebar-content"> <?= $content_for_sublayout ?> diff --git a/app/Template/analytic/lead_cycle_time.php b/app/Template/analytic/lead_cycle_time.php index 8e04bd6d..82ffe534 100644 --- a/app/Template/analytic/lead_cycle_time.php +++ b/app/Template/analytic/lead_cycle_time.php @@ -31,7 +31,7 @@ </div> <div class="form-inline-group"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form> diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index 19eef8d5..76289b9f 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -22,7 +22,8 @@ <li <?= $this->app->checkMenuSelection('analytic', 'compareHours') ?>> <?= $this->url->link(t('Estimated vs actual time'), 'analytic', 'compareHours', array('project_id' => $project['id'])) ?> </li> + + <?= $this->hook->render('template:analytic:sidebar', array('project' => $project)) ?> + </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div> diff --git a/app/Template/analytic/tasks.php b/app/Template/analytic/tasks.php index 7392ee56..9e7b1fd7 100644 --- a/app/Template/analytic/tasks.php +++ b/app/Template/analytic/tasks.php @@ -18,7 +18,7 @@ <?php foreach ($metrics as $metric): ?> <tr> <td> - <?= $this->e($metric['column_title']) ?> + <?= $this->text->e($metric['column_title']) ?> </td> <td> <?= $metric['nb_tasks'] ?> diff --git a/app/Template/analytic/users.php b/app/Template/analytic/users.php index 514d7133..9d1d3a1e 100644 --- a/app/Template/analytic/users.php +++ b/app/Template/analytic/users.php @@ -18,7 +18,7 @@ <?php foreach ($metrics as $metric): ?> <tr> <td> - <?= $this->e($metric['user']) ?> + <?= $this->text->e($metric['user']) ?> </td> <td> <?= $metric['nb_tasks'] ?> diff --git a/app/Template/app/filters_helper.php b/app/Template/app/filters_helper.php index e4cbb942..c16c2251 100644 --- a/app/Template/app/filters_helper.php +++ b/app/Template/app/filters_helper.php @@ -1,6 +1,6 @@ <?= $this->hook->render('template:app:filters-helper:before', isset($project) ? array('project' => $project) : array()) ?> -<div class="dropdown filters"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Filters') ?></a> +<div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> <li><a href="#" class="filter-helper filter-reset" data-filter="<?= isset($reset) ? $reset : '' ?>" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li> <li><a href="#" class="filter-helper" data-filter="status:open assignee:me"><?= t('My tasks') ?></a></li> diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php index ad1d5a9e..200cb0d7 100644 --- a/app/Template/app/layout.php +++ b/app/Template/app/layout.php @@ -1,16 +1,18 @@ <section id="main"> <div class="page-header page-header-mobile"> <ul> - <?php if ($this->user->hasAccess('project', 'create')): ?> + <?php if ($this->user->hasAccess('ProjectCreation', 'create')): ?> <li> <i class="fa fa-plus fa-fw"></i> - <?= $this->url->link(t('New project'), 'project', 'create') ?> + <?= $this->url->link(t('New project'), 'ProjectCreation', 'create', array(), false, 'popover') ?> </li> <?php endif ?> + <?php if ($this->app->config('disable_private_project', 0) == 0): ?> <li> <i class="fa fa-lock fa-fw"></i> - <?= $this->url->link(t('New private project'), 'project', 'createPrivate') ?> + <?= $this->url->link(t('New private project'), 'ProjectCreation', 'createPrivate', array(), false, 'popover') ?> </li> + <?php endif ?> <li> <i class="fa fa-search fa-fw"></i> <?= $this->url->link(t('Search'), 'search', 'index') ?> @@ -19,20 +21,10 @@ <i class="fa fa-folder fa-fw"></i> <?= $this->url->link(t('Project management'), 'project', 'index') ?> </li> - <?php if ($this->user->hasAccess('user', 'index')): ?> - <li> - <i class="fa fa-user fa-fw"></i> - <?= $this->url->link(t('User management'), 'user', 'index') ?> - </li> - <li> - <i class="fa fa-cog fa-fw"></i> - <?= $this->url->link(t('Settings'), 'config', 'index') ?> - </li> - <?php endif ?> </ul> </div> <section class="sidebar-container" id="dashboard"> - <?= $this->render('app/sidebar', array('user' => $user)) ?> + <?= $this->render($sidebar_template, array('user' => $user)) ?> <div class="sidebar-content"> <?= $content_for_sublayout ?> </div> diff --git a/app/Template/app/notifications.php b/app/Template/app/notifications.php index 511f377b..4cb3c571 100644 --- a/app/Template/app/notifications.php +++ b/app/Template/app/notifications.php @@ -49,7 +49,7 @@ <?php endif ?> </td> <td> - <?= dt('%B %e, %Y at %k:%M %p', $notification['date_creation']) ?> + <?= $this->dt->datetime($notification['date_creation']) ?> </td> <td> <i class="fa fa-check fa-fw"></i> diff --git a/app/Template/app/overview.php b/app/Template/app/overview.php index ebb3b412..0b354791 100644 --- a/app/Template/app/overview.php +++ b/app/Template/app/overview.php @@ -1,11 +1,10 @@ -<div class="search"> +<div class="filter-box"> <form method="get" action="<?= $this->url->dir() ?>" class="search"> <?= $this->form->hidden('controller', array('controller' => 'search')) ?> <?= $this->form->hidden('action', array('action' => 'index')) ?> <?= $this->form->text('search', array(), array(), array('placeholder="'.t('Search').'"'), 'form-input-large') ?> + <?= $this->render('app/filters_helper') ?> </form> - - <?= $this->render('app/filters_helper') ?> </div> <?= $this->render('app/projects', array('paginator' => $project_paginator, 'user' => $user)) ?> diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php index 61899c96..4ab8b106 100644 --- a/app/Template/app/projects.php +++ b/app/Template/app/projects.php @@ -8,7 +8,7 @@ <tr> <th class="column-5"><?= $paginator->order('Id', 'id') ?></th> <th class="column-3"><?= $paginator->order('<i class="fa fa-lock fa-fw" title="'.t('Private project').'"></i>', 'is_private') ?></th> - <th class="column-25"><?= $paginator->order(t('Project'), 'name') ?></th> + <th class="column-25"><?= $paginator->order(t('Project'), \Kanboard\Model\Project::TABLE.'.name') ?></th> <th><?= t('Columns') ?></th> </tr> <?php foreach ($paginator->getCollection() as $project): ?> @@ -29,9 +29,9 @@ <?= $this->url->link('<i class="fa fa-list"></i>', 'listing', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('List')) ?> <?= $this->url->link('<i class="fa fa-calendar"></i>', 'calendar', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Calendar')) ?> - <?= $this->url->link($this->e($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?> + <?= $this->url->link($this->text->e($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?> <?php if (! empty($project['description'])): ?> - <span class="tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($project['description'])) ?>'> <i class="fa fa-info-circle"></i> </span> <?php endif ?> @@ -39,7 +39,7 @@ <td class="dashboard-project-stats"> <?php foreach ($project['columns'] as $column): ?> <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong> - <span><?= $this->e($column['title']) ?></span> + <span><?= $this->text->e($column['title']) ?></span> <?php endforeach ?> </td> </tr> diff --git a/app/Template/app/sidebar.php b/app/Template/app/sidebar.php index b5e14aaf..66d15b14 100644 --- a/app/Template/app/sidebar.php +++ b/app/Template/app/sidebar.php @@ -1,5 +1,5 @@ <div class="sidebar"> - <h2><?= $this->e($user['name'] ?: $user['username']) ?></h2> + <h2><?= $this->text->e($user['name'] ?: $user['username']) ?></h2> <ul> <li <?= $this->app->checkMenuSelection('app', 'index') ?>> <?= $this->url->link(t('Overview'), 'app', 'index', array('user_id' => $user['id'])) ?> @@ -24,6 +24,4 @@ </li> <?= $this->hook->render('template:dashboard:sidebar') ?> </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div>
\ No newline at end of file diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php index b4c87bab..cca09481 100644 --- a/app/Template/app/subtasks.php +++ b/app/Template/app/subtasks.php @@ -6,7 +6,7 @@ <?php else: ?> <table class="table-fixed table-small"> <tr> - <th class="column-10"><?= $paginator->order('Id', 'tasks.id') ?></th> + <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> <th><?= $paginator->order(t('Task'), 'task_name') ?></th> <th><?= $paginator->order(t('Subtask'), 'title') ?></th> @@ -15,24 +15,24 @@ <?php foreach ($paginator->getCollection() as $subtask): ?> <tr> <td class="task-table color-<?= $subtask['color_id'] ?>"> - <?= $this->url->link('#'.$subtask['task_id'], 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + <?= $this->render('task/dropdown', array('task' => array('id' => $subtask['task_id'], 'project_id' => $subtask['project_id']))) ?> </td> <td> - <?= $this->url->link($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> + <?= $this->url->link($this->text->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> </td> <td> - <?= $this->url->link($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + <?= $this->url->link($this->text->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> </td> <td> - <?= $this->subtask->toggleStatus($subtask, 'dashboard') ?> + <?= $this->subtask->toggleStatus($subtask, $subtask['project_id']) ?> </td> <td> <?php if (! empty($subtask['time_spent'])): ?> - <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <strong><?= $this->text->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> <?php endif ?> <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <strong><?= $this->text->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> <?php endif ?> </td> </tr> diff --git a/app/Template/app/tasks.php b/app/Template/app/tasks.php index 1213e405..d7826fb7 100644 --- a/app/Template/app/tasks.php +++ b/app/Template/app/tasks.php @@ -6,7 +6,7 @@ <?php else: ?> <table class="table-fixed table-small"> <tr> - <th class="column-8"><?= $paginator->order('Id', 'tasks.id') ?></th> + <th class="column-5"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> <th><?= $paginator->order(t('Task'), 'title') ?></th> <th class="column-20"><?= t('Time tracking') ?></th> @@ -15,25 +15,25 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->url->link('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->render('task/dropdown', array('task' => $task)) ?> </td> <td> - <?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?= $this->url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> </td> <td> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </td> <td> <?php if (! empty($task['time_spent'])): ?> - <strong><?= $this->e($task['time_spent']).'h' ?></strong> <?= t('spent') ?> + <strong><?= $this->text->e($task['time_spent']).'h' ?></strong> <?= t('spent') ?> <?php endif ?> <?php if (! empty($task['time_estimated'])): ?> - <strong><?= $this->e($task['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <strong><?= $this->text->e($task['time_estimated']).'h' ?></strong> <?= t('estimated') ?> <?php endif ?> </td> <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> + <?= $this->dt->date($task['date_due']) ?> </td> </tr> <?php endforeach ?> diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php index a1059d6f..cc562170 100644 --- a/app/Template/auth/index.php +++ b/app/Template/auth/index.php @@ -3,7 +3,7 @@ <?= $this->hook->render('template:auth:login-form:before') ?> <?php if (isset($errors['login'])): ?> - <p class="alert alert-error"><?= $this->e($errors['login']) ?></p> + <p class="alert alert-error"><?= $this->text->e($errors['login']) ?></p> <?php endif ?> <?php if (! HIDE_LOGIN_FORM): ?> @@ -28,7 +28,7 @@ <?php endif ?> <div class="form-actions"> - <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Sign in') ?></button> </div> <?php if ($this->app->config('password_reset') == 1): ?> <div class="reset-password"> @@ -39,21 +39,4 @@ <?php endif ?> <?= $this->hook->render('template:auth:login-form:after') ?> - - <?php if (GOOGLE_AUTH || GITHUB_AUTH || GITLAB_AUTH): ?> - <ul class="no-bullet"> - <?php if (GOOGLE_AUTH): ?> - <li><?= $this->url->link(t('Login with my Google Account'), 'oauth', 'google') ?></li> - <?php endif ?> - - <?php if (GITHUB_AUTH): ?> - <li><?= $this->url->link(t('Login with my Github Account'), 'oauth', 'github') ?></li> - <?php endif ?> - - <?php if (GITLAB_AUTH): ?> - <li><?= $this->url->link(t('Login with my Gitlab Account'), 'oauth', 'gitlab') ?></li> - <?php endif ?> - </ul> - <?php endif ?> - </div>
\ No newline at end of file diff --git a/app/Template/board/popover_assignee.php b/app/Template/board/popover_assignee.php index e86ba420..87e16816 100644 --- a/app/Template/board/popover_assignee.php +++ b/app/Template/board/popover_assignee.php @@ -2,18 +2,17 @@ <div class="page-header"> <h2><?= t('Change assignee for the task "%s"', $values['title']) ?></h2> </div> - <form method="post" action="<?= $this->url->href('BoardPopover', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>"> + <form class="popover-form" method="post" action="<?= $this->url->href('BoardPopover', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('project_id', $values) ?> - <?= $this->form->label(t('Assignee'), 'owner_id') ?> - <?= $this->form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?><br/> + <?= $this->task->selectAssignee($users_list, $values, array(), array('autofocus')) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> </div> diff --git a/app/Template/board/popover_category.php b/app/Template/board/popover_category.php index 224ce8d1..e3794760 100644 --- a/app/Template/board/popover_category.php +++ b/app/Template/board/popover_category.php @@ -2,18 +2,17 @@ <div class="page-header"> <h2><?= t('Change category for the task "%s"', $values['title']) ?></h2> </div> - <form method="post" action="<?= $this->url->href('BoardPopover', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>"> + <form class="popover-form" method="post" action="<?= $this->url->href('BoardPopover', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $project['id'])) ?>"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('project_id', $values) ?> - <?= $this->form->label(t('Category'), 'category_id') ?> - <?= $this->form->select('category_id', $categories_list, $values, array(), array('autofocus')) ?><br/> + <?= $this->task->selectCategory($categories_list, $values, array(), array('autofocus'), true) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> </div> diff --git a/app/Template/board/popover_close_all_tasks_column.php b/app/Template/board/popover_close_all_tasks_column.php index da6b9ad7..5090f499 100644 --- a/app/Template/board/popover_close_all_tasks_column.php +++ b/app/Template/board/popover_close_all_tasks_column.php @@ -10,7 +10,7 @@ <p class="alert"><?= t('%d task(s) in the column "%s" and the swimlane "%s" will be closed.', $nb_tasks, $column, $swimlane) ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-red"> + <button type="submit" class="btn btn-red"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> </div> diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php index 8c97f7d5..48538c88 100644 --- a/app/Template/board/table_column.php +++ b/app/Template/board/table_column.php @@ -26,10 +26,10 @@ <span class="board-column-title"> <?php if ($not_editable): ?> - <?= $this->e($column['title']) ?> + <?= $this->text->e($column['title']) ?> <?php else: ?> <span class="dropdown"> - <a href="#" class="dropdown-menu"><?= $this->e($column['title']) ?> <i class="fa fa-caret-down"></i></a> + <a href="#" class="dropdown-menu"><?= $this->text->e($column['title']) ?> <i class="fa fa-caret-down"></i></a> <ul> <li> <i class="fa fa-minus-square fa-fw"></i> @@ -47,8 +47,8 @@ </span> <?php if (! $not_editable && ! empty($column['description'])): ?> - <span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> - <i class="fa fa-info-circle"></i> + <span class="tooltip pull-right" title='<?= $this->text->e($this->text->markdown($column['description'])) ?>'> + <i class="fa fa-info-circle"></i> </span> <?php endif ?> @@ -60,7 +60,7 @@ <?php if ($column['task_limit']): ?> <span title="<?= t('Task limit') ?>"> - (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>) + (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->text->e($column['task_limit']) ?>) </span> <?php else: ?> <span title="<?= t('Task count') ?>" class="board-column-header-task-count"> diff --git a/app/Template/board/table_swimlane.php b/app/Template/board/table_swimlane.php index 44607859..349b9acb 100644 --- a/app/Template/board/table_swimlane.php +++ b/app/Template/board/table_swimlane.php @@ -8,7 +8,7 @@ </a> <?php endif ?> - <?= $this->e($swimlane['name']) ?> + <?= $this->text->e($swimlane['name']) ?> <?php if (! $not_editable && ! empty($swimlane['description'])): ?> <span diff --git a/app/Template/board/table_tasks.php b/app/Template/board/table_tasks.php index e99e14fb..fd9ce5e7 100644 --- a/app/Template/board/table_tasks.php +++ b/app/Template/board/table_tasks.php @@ -22,7 +22,7 @@ <div class="board-column-collapsed"> <div class="board-rotation-wrapper"> <div class="board-column-title board-rotation board-toggle-column-view" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>"> - <i class="fa fa-plus-square tooltip" title="<?= $this->e($column['title']) ?>"></i> <?= $this->e($column['title']) ?> + <i class="fa fa-plus-square tooltip" title="<?= $this->text->e($column['title']) ?>"></i> <?= $this->text->e($column['title']) ?> </div> </div> </div> diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php index 26f3b1d4..d8b21a5b 100644 --- a/app/Template/board/task_footer.php +++ b/app/Template/board/task_footer.php @@ -2,10 +2,10 @@ <div class="task-board-category-container"> <span class="task-board-category"> <?php if ($not_editable): ?> - <?= $this->e($task['category_name']) ?> + <?= $this->text->e($task['category_name']) ?> <?php else: ?> <?= $this->url->link( - $this->e($task['category_name']), + $this->text->e($task['category_name']), 'boardPopover', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), @@ -22,7 +22,7 @@ <?php if (! empty($task['date_due'])): ?> <span class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>"> <i class="fa fa-calendar"></i> - <?= (date('Y') === date('Y', $task['date_due']) ? dt('%b %e', $task['date_due']) : dt('%b %e %Y', $task['date_due'])) ?> + <?= $this->dt->date($task['date_due']) ?> </span> <?php endif ?> @@ -35,7 +35,11 @@ <?php endif ?> <?php if (! empty($task['nb_links'])): ?> - <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i> <?= $task['nb_links'] ?></span> + <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork fa-fw"></i><?= $task['nb_links'] ?></span> + <?php endif ?> + + <?php if (! empty($task['nb_external_links'])): ?> + <span title="<?= t('External links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'externallinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-external-link fa-fw"></i><?= $task['nb_external_links'] ?></span> <?php endif ?> <?php if (! empty($task['nb_subtasks'])): ?> @@ -57,11 +61,11 @@ <?php endif ?> <?php if ($task['score']): ?> - <span class="task-score"><?= $this->e($task['score']) ?></span> + <span class="task-score"><?= $this->text->e($task['score']) ?></span> <?php endif ?> <?php if (! empty($task['time_estimated'])): ?> - <span class="task-time-estimated" title="<?= t('Time estimated') ?>"><?= $this->e($task['time_estimated']).'h' ?></span> + <span class="task-time-estimated" title="<?= t('Time estimated') ?>"><?= $this->text->e($task['time_estimated']).'h' ?></span> <?php endif ?> <?php if ($task['is_milestone'] == 1): ?> diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php index b5ed125d..bd582185 100644 --- a/app/Template/board/task_menu.php +++ b/app/Template/board/task_menu.php @@ -7,11 +7,12 @@ <li><i class="fa fa-pencil-square-o fa-fw"></i> <?= $this->url->link(t('Edit this task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <li><i class="fa fa-comment-o fa-fw"></i> <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <li><i class="fa fa-code-fork fa-fw"></i> <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> + <li><i class="fa fa-external-link fa-fw"></i> <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <li><i class="fa fa-camera fa-fw"></i> <?= $this->url->link(t('Add a screenshot'), 'BoardPopover', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <?php if ($task['is_active'] == 1): ?> - <li><i class="fa fa-close fa-fw"></i> <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li> + <li><i class="fa fa-close fa-fw"></i> <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <?php else: ?> - <li><i class="fa fa-check-square-o fa-fw"></i> <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li> + <li><i class="fa fa-check-square-o fa-fw"></i> <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> <?php endif ?> </ul> </span>
\ No newline at end of file diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php index 4880af00..50efe2f6 100644 --- a/app/Template/board/task_private.php +++ b/app/Template/board/task_private.php @@ -23,11 +23,11 @@ <?php endif ?> <?php if (! empty($task['assignee_username'])): ?> - <span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>"> - <?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> + <span title="<?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?>"> + <?= $this->text->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> </span> - <?php endif ?> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title tooltip', $this->e($task['title'])) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title tooltip', $this->text->e($task['title'])) ?> </div> <?php else: ?> <div class="task-board-expanded"> @@ -57,7 +57,7 @@ t('Change assignee') ) ?> <?php else: ?> - <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?> <?php endif ?> </span> <?php endif ?> @@ -72,7 +72,7 @@ <?php endif ?> <div class="task-board-title"> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </div> <?= $this->render('board/task_footer', array( diff --git a/app/Template/board/task_public.php b/app/Template/board/task_public.php index d02722bb..9058f897 100644 --- a/app/Template/board/task_public.php +++ b/app/Template/board/task_public.php @@ -19,7 +19,7 @@ </span> <div class="task-board-title"> - <?= $this->url->link($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> </div> <?= $this->render('board/task_footer', array( diff --git a/app/Template/board/tooltip_comments.php b/app/Template/board/tooltip_comments.php index 2e2c0c1e..6665bc7d 100644 --- a/app/Template/board/tooltip_comments.php +++ b/app/Template/board/tooltip_comments.php @@ -2,9 +2,9 @@ <?php foreach ($comments as $comment): ?> <p class="comment-title"> <?php if (! empty($comment['username'])): ?> - <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ + <span class="comment-username"><?= $this->text->e($comment['name'] ?: $comment['username']) ?></span> @ <?php endif ?> - <span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date_creation']) ?></span> + <span class="comment-date"><?= $this->dt->datetime($comment['date_creation']) ?></span> </p> <div class="comment-inner"> diff --git a/app/Template/board/tooltip_external_links.php b/app/Template/board/tooltip_external_links.php new file mode 100644 index 00000000..24cd1d88 --- /dev/null +++ b/app/Template/board/tooltip_external_links.php @@ -0,0 +1,20 @@ +<table class="table-striped table-small"> + <tr> + <th class="column-20"><?= t('Type') ?></th> + <th class="column-80"><?= t('Title') ?></th> + <th class="column-10"><?= t('Dependency') ?></th> + </tr> + <?php foreach ($links as $link): ?> + <tr> + <td> + <?= $link['type'] ?> + </td> + <td> + <a href="<?= $link['url'] ?>" target="_blank"><?= $this->text->e($link['title']) ?></a> + </td> + <td> + <?= $this->text->e($link['dependency_label']) ?> + </td> + </tr> + <?php endforeach ?> +</table>
\ No newline at end of file diff --git a/app/Template/board/tooltip_files.php b/app/Template/board/tooltip_files.php index 407309b3..8ee18626 100644 --- a/app/Template/board/tooltip_files.php +++ b/app/Template/board/tooltip_files.php @@ -3,14 +3,14 @@ <tr> <th> <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i> - <?= $this->e($file['name']) ?> + <?= $this->text->e($file['name']) ?> </th> </tr> <tr> <td> - <i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + <i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> <?php if ($file['is_image'] == 1): ?> - <i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + <i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> <?php endif ?> </td> </tr> diff --git a/app/Template/board/tooltip_subtasks.php b/app/Template/board/tooltip_subtasks.php index 5c273e08..126a319f 100644 --- a/app/Template/board/tooltip_subtasks.php +++ b/app/Template/board/tooltip_subtasks.php @@ -1,7 +1,14 @@ -<section id="tooltip-subtasks"> +<table class="table-stripped"> <?php foreach ($subtasks as $subtask): ?> - <?= $this->subtask->toggleStatus($subtask, 'board', $task['project_id']) ?> - <?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?> - <br/> + <tr> + <td class="column-80"> + <?= $this->subtask->toggleStatus($subtask, $task['project_id']) ?> + </td> + <td> + <?php if (! empty($subtask['username'])): ?> + <?= $this->text->e($subtask['name'] ?: $subtask['username']) ?> + <?php endif ?> + </td> + </tr> <?php endforeach ?> -</section> +</table> diff --git a/app/Template/board/tooltip_tasklinks.php b/app/Template/board/tooltip_tasklinks.php index 62304330..cabe43d8 100644 --- a/app/Template/board/tooltip_tasklinks.php +++ b/app/Template/board/tooltip_tasklinks.php @@ -1,19 +1,24 @@ <div class="tooltip-tasklinks"> - <ul> - <?php foreach ($links as $link): ?> - <li> - <strong><?= t($link['label']) ?></strong> - [<i><?= $link['project_name'] ?></i>] - <?= $this->url->link( - $this->e('#'.$link['task_id'].' - '.$link['title']), - 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), - false, - $link['is_active'] ? '' : 'task-link-closed' - ) ?> - <?php if (! empty($link['task_assignee_username'])): ?> - [<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>] - <?php endif ?> - </li> + <dl> + <?php foreach ($links as $label => $grouped_links): ?> + <dt><strong><?= t($label) ?></strong></dt> + <?php foreach ($grouped_links as $link): ?> + <dd> + <span class="progress"><?= $this->task->getProgress($link).'%' ?></span> + <?= $this->url->link( + $this->text->e('#'.$link['task_id'].' '.$link['title']), + 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php if (! empty($link['task_assignee_username'])): ?> + [<?= $this->text->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>] + <?php endif ?> + <?php if ($task['project_id'] != $link['project_id']): ?> + (<i><?= $link['project_name'] ?></i>) + <?php endif ?> + </dd> + <?php endforeach ?> <?php endforeach ?> - </ul> + </dl> </div>
\ No newline at end of file diff --git a/app/Template/board/view_private.php b/app/Template/board/view_private.php index 63d261f6..b5e38c66 100644 --- a/app/Template/board/view_private.php +++ b/app/Template/board/view_private.php @@ -1,6 +1,6 @@ <section id="main"> - <?= $this->render('project/filters', array( + <?= $this->render('project_header/header', array( 'project' => $project, 'filters' => $filters, 'categories_list' => $categories_list, diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php index d74e945e..7085b51e 100644 --- a/app/Template/calendar/show.php +++ b/app/Template/calendar/show.php @@ -1,5 +1,5 @@ <section id="main"> - <?= $this->render('project/filters', array( + <?= $this->render('project_header/header', array( 'project' => $project, 'filters' => $filters, )) ?> diff --git a/app/Template/category/edit.php b/app/Template/category/edit.php index 1aae2f2a..78156e6f 100644 --- a/app/Template/category/edit.php +++ b/app/Template/category/edit.php @@ -2,7 +2,7 @@ <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> </div> -<form method="post" action="<?= $this->url->href('category', 'update', array('project_id' => $project['id'], 'category_id' => $values['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('category', 'update', array('project_id' => $project['id'], 'category_id' => $values['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -33,8 +33,8 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/category/index.php b/app/Template/category/index.php index 4602f3b9..b3bdfd81 100644 --- a/app/Template/category/index.php +++ b/app/Template/category/index.php @@ -9,16 +9,16 @@ </tr> <?php foreach ($categories as $category_id => $category_name): ?> <tr> - <td><?= $this->e($category_name) ?></td> + <td><?= $this->text->e($category_name) ?></td> <td> <div class="dropdown"> <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> <li> - <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id)) ?> + <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id), false, 'popover') ?> </li> <li> - <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id)) ?> + <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id), false, 'popover') ?> </li> </ul> </div> @@ -40,6 +40,6 @@ <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/category/remove.php b/app/Template/category/remove.php index ce589785..cad58d37 100644 --- a/app/Template/category/remove.php +++ b/app/Template/category/remove.php @@ -11,7 +11,7 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'category', 'remove', array('project_id' => $project['id'], 'category_id' => $category['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </div> </section>
\ No newline at end of file diff --git a/app/Template/column/create.php b/app/Template/column/create.php new file mode 100644 index 00000000..747b1529 --- /dev/null +++ b/app/Template/column/create.php @@ -0,0 +1,41 @@ +<div class="page-header"> + <h2><?= t('Add a new column') ?></h2> +</div> +<form class="popover-form" method="post" action="<?= $this->url->href('Column', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Task limit'), 'task_limit') ?> + <?= $this->form->number('task_limit', $values, $errors) ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php index a17affd8..206322cd 100644 --- a/app/Template/column/edit.php +++ b/app/Template/column/edit.php @@ -2,7 +2,7 @@ <h2><?= t('Edit column "%s"', $column['title']) ?></h2> </div> -<form method="post" action="<?= $this->url->href('column', 'update', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('column', 'update', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -37,8 +37,8 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/column/index.php b/app/Template/column/index.php index 17651d46..eef176f3 100644 --- a/app/Template/column/index.php +++ b/app/Template/column/index.php @@ -1,92 +1,56 @@ <div class="page-header"> <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2> + <ul> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add a new column'), 'Column', 'create', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> </div> -<?php if (! empty($columns)): ?> - - <?php $first_position = $columns[0]['position']; ?> - <?php $last_position = $columns[count($columns) - 1]['position']; ?> - - <h3><?= t('Change columns') ?></h3> - <table> +<?php if (empty($columns)): ?> + <p class="alert alert-error"><?= t('Your board doesn\'t have any column!') ?></p> +<?php else: ?> + <table + class="columns-table table-stripped" + data-save-position-url="<?= $this->url->href('Column', 'move', array('project_id' => $project['id'])) ?>"> + <thead> <tr> <th class="column-70"><?= t('Column title') ?></th> <th class="column-25"><?= t('Task limit') ?></th> <th class="column-5"><?= t('Actions') ?></th> </tr> + </thead> + <tbody> <?php foreach ($columns as $column): ?> - <tr> - <td><?= $this->e($column['title']) ?> - <?php if (! empty($column['description'])): ?> - <span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> - <i class="fa fa-info-circle"></i> - </span> - <?php endif ?> + <tr data-column-id="<?= $column['id'] ?>"> + <td> + <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change column position') ?>"></i> + <?= $this->text->e($column['title']) ?> + <?php if (! empty($column['description'])): ?> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($column['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </td> + <td> + <?= $this->text->e($column['task_limit']) ?> </td> - <td><?= $this->e($column['task_limit']) ?></td> <td> <div class="dropdown"> <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> <li> - <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> - </li> - <?php if ($column['position'] != $first_position): ?> - <li> - <?= $this->url->link(t('Move Up'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> - </li> - <?php endif ?> - <?php if ($column['position'] != $last_position): ?> - <li> - <?= $this->url->link(t('Move Down'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> + <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> </li> - <?php endif ?> <li> - <?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> + <?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> </li> </ul> </div> </td> </tr> <?php endforeach ?> + </tbody> </table> - <?php endif ?> - -<h3><?= t('Add a new column') ?></h3> -<form method="post" action="<?= $this->url->href('column', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off"> - - <?= $this->form->csrf() ?> - - <?= $this->form->hidden('project_id', $values) ?> - - <?= $this->form->label(t('Title'), 'title') ?> - <?= $this->form->text('title', $values, $errors, array('required', 'maxlength="50"')) ?> - - <?= $this->form->label(t('Task limit'), 'task_limit') ?> - <?= $this->form->number('task_limit', $values, $errors) ?> - - <?= $this->form->label(t('Description'), 'description') ?> - - <div class="form-tabs"> - <div class="write-area"> - <?= $this->form->textarea('description', $values, $errors) ?> - </div> - <div class="preview-area"> - <div class="markdown"></div> - </div> - <ul class="form-tabs-nav"> - <li class="form-tab form-tab-selected"> - <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> - </li> - <li class="form-tab"> - <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> - </li> - </ul> - </div> - <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Template/column/remove.php b/app/Template/column/remove.php index 28d0928f..ccab889d 100644 --- a/app/Template/column/remove.php +++ b/app/Template/column/remove.php @@ -10,6 +10,6 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'column', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/comment/create.php b/app/Template/comment/create.php index e9a6404d..574eec9f 100644 --- a/app/Template/comment/create.php +++ b/app/Template/comment/create.php @@ -1,8 +1,7 @@ <div class="page-header"> <h2><?= t('Add a comment') ?></h2> </div> - -<form method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" autocomplete="off" class="form-comment"> +<form class="popover-form" method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off" class="form-comment"> <?= $this->form->csrf() ?> <?= $this->form->hidden('task_id', $values) ?> <?= $this->form->hidden('user_id', $values) ?> @@ -38,14 +37,10 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?php if (! isset($skip_cancel)): ?> <?= t('or') ?> - <?php if (isset($ajax)): ?> - <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> - <?php else: ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php endif ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> <?php endif ?> </div> </form> diff --git a/app/Template/comment/edit.php b/app/Template/comment/edit.php index e01f3da4..ad6f58fb 100644 --- a/app/Template/comment/edit.php +++ b/app/Template/comment/edit.php @@ -2,7 +2,7 @@ <h2><?= t('Edit a comment') ?></h2> </div> -<form method="post" action="<?= $this->url->href('comment', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('comment', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> @@ -29,8 +29,8 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/comment/forbidden.php b/app/Template/comment/forbidden.php deleted file mode 100644 index 1e306d45..00000000 --- a/app/Template/comment/forbidden.php +++ /dev/null @@ -1,7 +0,0 @@ -<div class="page-header"> - <h2><?= t('Forbidden') ?></h2> -</div> - -<p class="alert alert-error"> - <?= t('Only administrators or the creator of the comment can access to this page.') ?> -</p>
\ No newline at end of file diff --git a/app/Template/comment/remove.php b/app/Template/comment/remove.php index afc3346f..1b5004f4 100644 --- a/app/Template/comment/remove.php +++ b/app/Template/comment/remove.php @@ -12,6 +12,6 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'comment', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php index 44457653..873e1470 100644 --- a/app/Template/comment/show.php +++ b/app/Template/comment/show.php @@ -6,10 +6,10 @@ <?php endif ?> <?php if (! empty($comment['username'])): ?> - <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ + <span class="comment-username"><?= $this->text->e($comment['name'] ?: $comment['username']) ?></span> @ <?php endif ?> - <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date_creation']) ?></span> + <span class="comment-date"><?= $this->dt->datetime($comment['date_creation']) ?></span> </p> <div class="comment-inner"> @@ -18,10 +18,10 @@ <li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li> <?php if ($editable && ($this->user->isAdmin() || $this->user->isCurrentUser($comment['user_id']))): ?> <li> - <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?> + <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?> </li> <li> - <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?> + <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?> </li> <?php endif ?> </ul> diff --git a/app/Template/config/about.php b/app/Template/config/about.php index e0652a2f..968b109a 100644 --- a/app/Template/config/about.php +++ b/app/Template/config/about.php @@ -29,7 +29,7 @@ <ul> <li> <?= t('Database driver:') ?> - <strong><?= $this->e(DB_DRIVER) ?></strong> + <strong><?= $this->text->e(DB_DRIVER) ?></strong> </li> <?php if (DB_DRIVER === 'sqlite'): ?> <li> @@ -54,6 +54,7 @@ <div class="listing"> <h3><?= t('Board/Calendar/List view') ?></h3> <ul> + <li><?= t('Switch to the project overview') ?> = <strong>v o</strong></li> <li><?= t('Switch to the board view') ?> = <strong>v b</strong></li> <li><?= t('Switch to the calendar view') ?> = <strong>v c</strong></li> <li><?= t('Switch to the list view') ?> = <strong>v l</strong></li> diff --git a/app/Template/config/api.php b/app/Template/config/api.php index 489f1968..3ebbb956 100644 --- a/app/Template/config/api.php +++ b/app/Template/config/api.php @@ -5,7 +5,7 @@ <ul> <li> <?= t('API token:') ?> - <strong><?= $this->e($values['api_token']) ?></strong> + <strong><?= $this->text->e($values['api_token']) ?></strong> </li> <li> <?= t('API endpoint:') ?> diff --git a/app/Template/config/application.php b/app/Template/config/application.php index ec7d8462..259756bc 100644 --- a/app/Template/config/application.php +++ b/app/Template/config/application.php @@ -1,14 +1,13 @@ <div class="page-header"> <h2><?= t('Application settings') ?></h2> </div> -<section> <form method="post" action="<?= $this->url->href('config', 'application') ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->label(t('Application URL'), 'application_url') ?> <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?> - <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p> + <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used to generate absolute URLs)') ?></p> <?= $this->form->label(t('Language'), 'application_language') ?> <?= $this->form->select('application_language', $languages, $values, $errors) ?> @@ -20,13 +19,20 @@ <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?> <p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p> + <?= $this->form->label(t('Date and time format'), 'application_datetime_format') ?> + <?= $this->form->select('application_datetime_format', $datetime_formats, $values, $errors) ?> + + <?= $this->form->label(t('Time format'), 'application_time_format') ?> + <?= $this->form->select('application_time_format', $time_formats, $values, $errors) ?> + <?= $this->form->checkbox('password_reset', t('Enable "Forget Password"'), 1, $values['password_reset'] == 1) ?> <?= $this->form->label(t('Custom Stylesheet'), 'application_stylesheet') ?> - <?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/> + <?= $this->form->textarea('application_stylesheet', $values, $errors) ?> + + <?= $this->hook->render('template:config:application', array('values' => $values, 'errors' => $errors)) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> -</section>
\ No newline at end of file diff --git a/app/Template/config/board.php b/app/Template/config/board.php index 19a4bcd7..ba1bab59 100644 --- a/app/Template/config/board.php +++ b/app/Template/config/board.php @@ -1,25 +1,23 @@ <div class="page-header"> <h2><?= t('Board settings') ?></h2> </div> -<section> <form method="post" action="<?= $this->url->href('config', 'board') ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->label(t('Task highlight period'), 'board_highlight_period') ?> - <?= $this->form->number('board_highlight_period', $values, $errors) ?><br/> + <?= $this->form->number('board_highlight_period', $values, $errors) ?> <p class="form-help"><?= t('Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)') ?></p> <?= $this->form->label(t('Refresh interval for public board'), 'board_public_refresh_interval') ?> - <?= $this->form->number('board_public_refresh_interval', $values, $errors) ?><br/> + <?= $this->form->number('board_public_refresh_interval', $values, $errors) ?> <p class="form-help"><?= t('Frequency in second (60 seconds by default)') ?></p> <?= $this->form->label(t('Refresh interval for private board'), 'board_private_refresh_interval') ?> - <?= $this->form->number('board_private_refresh_interval', $values, $errors) ?><br/> + <?= $this->form->number('board_private_refresh_interval', $values, $errors) ?> <p class="form-help"><?= t('Frequency in second (0 to disable this feature, 10 seconds by default)') ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> -</section>
\ No newline at end of file diff --git a/app/Template/config/calendar.php b/app/Template/config/calendar.php index f5250132..b7b230df 100644 --- a/app/Template/config/calendar.php +++ b/app/Template/config/calendar.php @@ -28,7 +28,7 @@ </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> </section>
\ No newline at end of file diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php index bba85672..e404c52e 100644 --- a/app/Template/config/integrations.php +++ b/app/Template/config/integrations.php @@ -3,35 +3,15 @@ </div> <form method="post" action="<?= $this->url->href('config', 'integrations') ?>" autocomplete="off"> - <?= $this->form->csrf() ?> - <?= $this->hook->render('template:config:integrations', array('values' => $values)) ?> - <h3><i class="fa fa-google"></i> <?= t('Google Authentication') ?></h3> - <div class="listing"> - <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'google', array(), false, '', true) ?>"/><br/> - <p class="form-help"><?= $this->url->doc(t('Help on Google authentication'), 'google-authentication') ?></p> - </div> - - <h3><i class="fa fa-github"></i> <?= t('Github Authentication') ?></h3> - <div class="listing"> - <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'github', array(), false, '', true) ?>"/><br/> - <p class="form-help"><?= $this->url->doc(t('Help on Github authentication'), 'github-authentication') ?></p> - </div> - - <h3><img src="<?= $this->url->dir() ?>assets/img/gitlab-icon.png"/> <?= t('Gitlab Authentication') ?></h3> - <div class="listing"> - <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'gitlab', array(), false, '', true) ?>"/><br/> - <p class="form-help"><?= $this->url->doc(t('Help on Gitlab authentication'), 'gitlab-authentication') ?></p> - </div> - <h3><img src="<?= $this->url->dir() ?>assets/img/gravatar-icon.png"/> <?= t('Gravatar') ?></h3> <div class="listing"> <?= $this->form->checkbox('integration_gravatar', t('Enable Gravatar images'), 1, $values['integration_gravatar'] == 1) ?> </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/config/layout.php b/app/Template/config/layout.php index 028f138c..f34caaab 100644 --- a/app/Template/config/layout.php +++ b/app/Template/config/layout.php @@ -1,10 +1,10 @@ <section id="main"> <section class="sidebar-container" id="config-section"> - <?= $this->render('config/sidebar') ?> + <?= $this->render($sidebar_template) ?> <div class="sidebar-content"> - <?= $config_content_for_layout ?> + <?= $content_for_sublayout ?> </div> </section> </section>
\ No newline at end of file diff --git a/app/Template/config/plugins.php b/app/Template/config/plugins.php index 4a263ce7..04b3f095 100644 --- a/app/Template/config/plugins.php +++ b/app/Template/config/plugins.php @@ -17,14 +17,14 @@ <tr> <td> <?php if ($plugin->getPluginHomepage()): ?> - <a href="<?= $plugin->getPluginHomepage() ?>" target="_blank" rel="noreferrer"><?= $this->e($plugin->getPluginName()) ?></a> + <a href="<?= $plugin->getPluginHomepage() ?>" target="_blank" rel="noreferrer"><?= $this->text->e($plugin->getPluginName()) ?></a> <?php else: ?> - <?= $this->e($plugin->getPluginName()) ?> + <?= $this->text->e($plugin->getPluginName()) ?> <?php endif ?> </td> - <td><?= $this->e($plugin->getPluginAuthor()) ?></td> - <td><?= $this->e($plugin->getPluginVersion()) ?></td> - <td><?= $this->e($plugin->getPluginDescription()) ?></td> + <td><?= $this->text->e($plugin->getPluginAuthor()) ?></td> + <td><?= $this->text->e($plugin->getPluginVersion()) ?></td> + <td><?= $this->text->e($plugin->getPluginDescription()) ?></td> </tr> <?php endforeach ?> <?php endif ?>
\ No newline at end of file diff --git a/app/Template/config/project.php b/app/Template/config/project.php index c58a7bac..b6b7ec25 100644 --- a/app/Template/config/project.php +++ b/app/Template/config/project.php @@ -1,7 +1,6 @@ <div class="page-header"> <h2><?= t('Project settings') ?></h2> </div> -<section> <form method="post" action="<?= $this->url->href('config', 'project') ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -10,19 +9,19 @@ <?= $this->form->select('default_color', $colors, $values, $errors) ?> <?= $this->form->label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?> - <?= $this->form->text('board_columns', $values, $errors) ?><br/> + <?= $this->form->text('board_columns', $values, $errors) ?> <p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p> <?= $this->form->label(t('Default categories for new projects (Comma-separated)'), 'project_categories') ?> - <?= $this->form->text('project_categories', $values, $errors) ?><br/> + <?= $this->form->text('project_categories', $values, $errors) ?> <p class="form-help"><?= t('Example: "Bug, Feature Request, Improvement"') ?></p> + <?= $this->form->checkbox('disable_private_project', t('Disable private projects'), 1, isset($values['disable_private_project']) && $values['disable_private_project'] == 1) ?> <?= $this->form->checkbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?> <?= $this->form->checkbox('subtask_time_tracking', t('Trigger automatically subtask time tracking'), 1, $values['subtask_time_tracking'] == 1) ?> <?= $this->form->checkbox('cfd_include_closed_tasks', t('Include closed tasks in the cumulative flow diagram'), 1, $values['cfd_include_closed_tasks'] == 1) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> -</section>
\ No newline at end of file diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php index a8174505..dd51bc74 100644 --- a/app/Template/config/sidebar.php +++ b/app/Template/config/sidebar.php @@ -34,11 +34,6 @@ <li <?= $this->app->checkMenuSelection('config', 'api') ?>> <?= $this->url->link(t('API'), 'config', 'api') ?> </li> - <li> - <?= $this->url->link(t('Documentation'), 'doc', 'show') ?> - </li> <?= $this->hook->render('template:config:sidebar') ?> </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div>
\ No newline at end of file diff --git a/app/Template/config/webhook.php b/app/Template/config/webhook.php index f1a98f8b..b96979a0 100644 --- a/app/Template/config/webhook.php +++ b/app/Template/config/webhook.php @@ -10,7 +10,7 @@ <?= $this->form->text('webhook_url', $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> </section> @@ -22,7 +22,7 @@ <ul> <li> <?= t('Webhook token:') ?> - <strong><?= $this->e($values['webhook_token']) ?></strong> + <strong><?= $this->text->e($values['webhook_token']) ?></strong> </li> <li> <?= t('URL for task creation:') ?> diff --git a/app/Template/currency/index.php b/app/Template/currency/index.php index 1c78c47a..d35ac459 100644 --- a/app/Template/currency/index.php +++ b/app/Template/currency/index.php @@ -12,7 +12,7 @@ <?php foreach ($rates as $rate): ?> <tr> <td> - <strong><?= $this->e($rate['currency']) ?></strong> + <strong><?= $this->text->e($rate['currency']) ?></strong> </td> <td> <?= n($rate['rate']) ?> @@ -29,10 +29,10 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('Reference currency'), 'application_currency') ?> - <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?><br/> + <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> @@ -43,12 +43,12 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('Currency'), 'currency') ?> - <?= $this->form->select('currency', $currencies, $values, $errors) ?><br/> + <?= $this->form->select('currency', $currencies, $values, $errors) ?> <?= $this->form->label(t('Rate'), 'rate') ?> - <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?><br/> + <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> diff --git a/app/Template/custom_filter/add.php b/app/Template/custom_filter/add.php index 361083ee..e3e144ae 100644 --- a/app/Template/custom_filter/add.php +++ b/app/Template/custom_filter/add.php @@ -19,6 +19,6 @@ <?= $this->form->checkbox('append', t('Append filter (instead of replacement)'), 1) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/custom_filter/edit.php b/app/Template/custom_filter/edit.php index adae6b4f..5d07e8c2 100644 --- a/app/Template/custom_filter/edit.php +++ b/app/Template/custom_filter/edit.php @@ -2,7 +2,7 @@ <h2><?= t('Edit custom filter') ?></h2> </div> -<form method="post" action="<?= $this->url->href('customfilter', 'update', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?>" autocomplete="off"> +<form class="form-popover" method="post" action="<?= $this->url->href('customfilter', 'update', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -25,8 +25,8 @@ <?= $this->form->checkbox('append', t('Append filter (instead of replacement)'), 1, $values['append'] == 1) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/custom_filter/index.php b/app/Template/custom_filter/index.php index 7868384f..12a4eece 100644 --- a/app/Template/custom_filter/index.php +++ b/app/Template/custom_filter/index.php @@ -14,8 +14,8 @@ </tr> <?php foreach ($custom_filters as $filter): ?> <tr> - <td><?= $this->e($filter['name']) ?></td> - <td><?= $this->e($filter['filter']) ?></td> + <td><?= $this->text->e($filter['name']) ?></td> + <td><?= $this->text->e($filter['filter']) ?></td> <td> <?php if ($filter['is_shared'] == 1): ?> <?= t('Yes') ?> @@ -30,14 +30,14 @@ <?= t('Replace') ?> <?php endif ?> </td> - <td><?= $this->e($filter['owner_name'] ?: $filter['owner_username']) ?></td> + <td><?= $this->text->e($filter['owner_name'] ?: $filter['owner_username']) ?></td> <td> <?php if ($filter['user_id'] == $this->user->getId() || $this->user->hasProjectAccess('customfilter', 'edit', $project['id'])): ?> <div class="dropdown"> <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> - <li><?= $this->url->link(t('Remove'), 'customfilter', 'remove', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), true) ?></li> - <li><?= $this->url->link(t('Edit'), 'customfilter', 'edit', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?></li> + <li><?= $this->url->link(t('Remove'), 'customfilter', 'confirm', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), false, 'popover') ?></li> + <li><?= $this->url->link(t('Edit'), 'customfilter', 'edit', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), false, 'popover') ?></li> </ul> </div> <?php endif ?> diff --git a/app/Template/custom_filter/remove.php b/app/Template/custom_filter/remove.php new file mode 100644 index 00000000..d4c67a2b --- /dev/null +++ b/app/Template/custom_filter/remove.php @@ -0,0 +1,17 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a custom filter') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this custom filter: "%s"?', $filter['name']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'customfilter', 'remove', array('project_id' => $project['id'], 'filter_id' => $filter['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> + </div> +</section> diff --git a/app/Template/event/comment_create.php b/app/Template/event/comment_create.php index 462f15ca..063736b3 100644 --- a/app/Template/event/comment_create.php +++ b/app/Template/event/comment_create.php @@ -2,11 +2,11 @@ <p class="activity-title"> <?= e('%s commented the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <div class="activity-description"> - <em><?= $this->e($task['title']) ?></em><br/> + <em><?= $this->text->e($task['title']) ?></em><br/> <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> </div>
\ No newline at end of file diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php index 0cb10bf6..93f24d8a 100644 --- a/app/Template/event/comment_update.php +++ b/app/Template/event/comment_update.php @@ -2,10 +2,10 @@ <p class="activity-title"> <?= e('%s updated a comment on the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <div class="activity-description"> - <em><?= $this->e($task['title']) ?></em><br/> + <em><?= $this->text->e($task['title']) ?></em><br/> </div>
\ No newline at end of file diff --git a/app/Template/event/events.php b/app/Template/event/events.php index aec0b29e..bbb01be4 100644 --- a/app/Template/event/events.php +++ b/app/Template/event/events.php @@ -14,7 +14,7 @@ <?php elseif ($this->text->contains($event['event_name'], 'comment')): ?> <i class="fa fa-comments-o"></i> <?php endif ?> - <?= dt('%B %e, %Y at %k:%M %p', $event['date_creation']) ?> + <?= $this->dt->datetime($event['date_creation']) ?> </p> <div class="activity-content"><?= $event['event_content'] ?></div> </div> diff --git a/app/Template/event/subtask_create.php b/app/Template/event/subtask_create.php index ca23aa9c..532783d5 100644 --- a/app/Template/event/subtask_create.php +++ b/app/Template/event/subtask_create.php @@ -2,16 +2,16 @@ <p class="activity-title"> <?= e('%s created a subtask for the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <div class="activity-description"> - <p><em><?= $this->e($task['title']) ?></em></p> + <p><em><?= $this->text->e($task['title']) ?></em></p> <ul> <li> - <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>) + <?= $this->text->e($subtask['title']) ?> (<strong><?= $this->text->e($subtask['status_name']) ?></strong>) </li> <li> <?php if ($subtask['username']): ?> diff --git a/app/Template/event/subtask_update.php b/app/Template/event/subtask_update.php index 11a778de..e9ff6ac6 100644 --- a/app/Template/event/subtask_update.php +++ b/app/Template/event/subtask_update.php @@ -2,16 +2,16 @@ <p class="activity-title"> <?= e('%s updated a subtask for the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <div class="activity-description"> - <p><em><?= $this->e($task['title']) ?></em></p> + <p><em><?= $this->text->e($task['title']) ?></em></p> <ul> <li> - <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>) + <?= $this->text->e($subtask['title']) ?> (<strong><?= $this->text->e($subtask['status_name']) ?></strong>) </li> <li> <?php if ($subtask['username']): ?> diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php index cdec8743..580176c7 100644 --- a/app/Template/event/task_assignee_change.php +++ b/app/Template/event/task_assignee_change.php @@ -5,14 +5,14 @@ <?php if (! empty($assignee)): ?> <?= e('%s changed the assignee of the task %s to %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - $this->e($assignee) + $this->text->e($assignee) ) ?> <?php else: ?> - <?= e('%s remove the assignee of the task %s', $this->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + <?= e('%s remove the assignee of the task %s', $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> <?php endif ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_close.php b/app/Template/event/task_close.php index 3d8670a6..361458d3 100644 --- a/app/Template/event/task_close.php +++ b/app/Template/event/task_close.php @@ -2,10 +2,10 @@ <p class="activity-title"> <?= e('%s closed the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_create.php b/app/Template/event/task_create.php index 773f401c..655bf8f3 100644 --- a/app/Template/event/task_create.php +++ b/app/Template/event/task_create.php @@ -2,10 +2,10 @@ <p class="activity-title"> <?= e('%s created the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/file_create.php b/app/Template/event/task_file_create.php index 1a36bc8f..61bf3d61 100644 --- a/app/Template/event/file_create.php +++ b/app/Template/event/task_file_create.php @@ -2,10 +2,10 @@ <p class="activity-title"> <?= e('%s attached a new file to the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($file['name']) ?></em> + <em><?= $this->text->e($file['name']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_move_column.php b/app/Template/event/task_move_column.php index ca482e46..904c956c 100644 --- a/app/Template/event/task_move_column.php +++ b/app/Template/event/task_move_column.php @@ -2,11 +2,11 @@ <p class="activity-title"> <?= e('%s moved the task %s to the column "%s"', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - $this->e($task['column_title']) + $this->text->e($task['column_title']) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_move_position.php b/app/Template/event/task_move_position.php index dcdd3e1b..6580bb79 100644 --- a/app/Template/event/task_move_position.php +++ b/app/Template/event/task_move_position.php @@ -2,12 +2,12 @@ <p class="activity-title"> <?= e('%s moved the task %s to the position #%d in the column "%s"', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), $task['position'], - $this->e($task['column_title']) + $this->text->e($task['column_title']) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_move_swimlane.php b/app/Template/event/task_move_swimlane.php index ca440dbf..9ffa554c 100644 --- a/app/Template/event/task_move_swimlane.php +++ b/app/Template/event/task_move_swimlane.php @@ -3,17 +3,17 @@ <p class="activity-title"> <?php if ($task['swimlane_id'] == 0): ?> <?= e('%s moved the task %s to the first swimlane', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> <?php else: ?> <?= e('%s moved the task %s to the swimlane "%s"', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - $this->e($task['swimlane_name']) + $this->text->e($task['swimlane_name']) ) ?> <?php endif ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_open.php b/app/Template/event/task_open.php index 11fec64b..9db2e3c9 100644 --- a/app/Template/event/task_open.php +++ b/app/Template/event/task_open.php @@ -2,10 +2,10 @@ <p class="activity-title"> <?= e('%s opened the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> </p>
\ No newline at end of file diff --git a/app/Template/event/task_update.php b/app/Template/event/task_update.php index e8254bcb..72d70495 100644 --- a/app/Template/event/task_update.php +++ b/app/Template/event/task_update.php @@ -2,12 +2,12 @@ <p class="activity-title"> <?= e('%s updated the task %s', - $this->e($author), + $this->text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> </p> <p class="activity-description"> - <em><?= $this->e($task['title']) ?></em> + <em><?= $this->text->e($task['title']) ?></em> <?php if (isset($changes)): ?> <div class="activity-changes"> <?= $this->render('task/changes', array('changes' => $changes, 'task' => $task)) ?> diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php index 44448520..6a1de7e9 100644 --- a/app/Template/export/sidebar.php +++ b/app/Template/export/sidebar.php @@ -15,6 +15,4 @@ </li> <?= $this->hook->render('template:export:sidebar') ?> </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div>
\ No newline at end of file diff --git a/app/Template/export/subtasks.php b/app/Template/export/subtasks.php index 4aad2641..a82cb3d1 100644 --- a/app/Template/export/subtasks.php +++ b/app/Template/export/subtasks.php @@ -1,7 +1,5 @@ <div class="page-header"> - <h2> - <?= t('Subtasks exportation for "%s"', $project['name']) ?> - </h2> + <h2><?= t('Subtasks exportation for "%s"', $project['name']) ?></h2> </div> <p class="alert alert-info"><?= t('This report contains all subtasks information for the given date range.') ?></p> @@ -13,7 +11,7 @@ <?= $this->form->hidden('project_id', $values) ?> <?= $this->form->label(t('Start Date'), 'from') ?> - <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> <?= $this->form->label(t('End Date'), 'to') ?> <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> @@ -21,6 +19,6 @@ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/export/summary.php b/app/Template/export/summary.php index ffbd6ac2..60aa306f 100644 --- a/app/Template/export/summary.php +++ b/app/Template/export/summary.php @@ -1,7 +1,5 @@ <div class="page-header"> - <h2> - <?= t('Daily project summary export for "%s"', $project['name']) ?> - </h2> + <h2><?= t('Daily project summary export for "%s"', $project['name']) ?></h2> </div> <p class="alert alert-info"><?= t('This export contains the number of tasks per column grouped per day.') ?></p> @@ -13,7 +11,7 @@ <?= $this->form->hidden('project_id', $values) ?> <?= $this->form->label(t('Start Date'), 'from') ?> - <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> <?= $this->form->label(t('End Date'), 'to') ?> <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> @@ -21,6 +19,6 @@ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/export/tasks.php b/app/Template/export/tasks.php index c74c8f98..bed8ab90 100644 --- a/app/Template/export/tasks.php +++ b/app/Template/export/tasks.php @@ -1,7 +1,5 @@ <div class="page-header"> - <h2> - <?= t('Tasks exportation for "%s"', $project['name']) ?> - </h2> + <h2><?= t('Tasks exportation for "%s"', $project['name']) ?></h2> </div> <p class="alert alert-info"><?= t('This report contains all tasks information for the given date range.') ?></p> @@ -13,7 +11,7 @@ <?= $this->form->hidden('project_id', $values) ?> <?= $this->form->label(t('Start Date'), 'from') ?> - <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> <?= $this->form->label(t('End Date'), 'to') ?> <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> @@ -21,6 +19,6 @@ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/export/transitions.php b/app/Template/export/transitions.php index bf6ef249..093930a3 100644 --- a/app/Template/export/transitions.php +++ b/app/Template/export/transitions.php @@ -1,7 +1,5 @@ <div class="page-header"> - <h2> - <?= t('Task transitions export') ?> - </h2> + <h2><?= t('Task transitions export') ?></h2> </div> <p class="alert alert-info"><?= t('This report contains all column moves for each task with the date, the user and the time spent for each transition.') ?></p> @@ -13,7 +11,7 @@ <?= $this->form->hidden('project_id', $values) ?> <?= $this->form->label(t('Start Date'), 'from') ?> - <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> <?= $this->form->label(t('End Date'), 'to') ?> <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> @@ -21,6 +19,6 @@ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Execute') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/feed/project.php b/app/Template/feed/project.php index 76cf6cf0..1c6d1166 100644 --- a/app/Template/feed/project.php +++ b/app/Template/feed/project.php @@ -15,7 +15,7 @@ <published><?= date(DATE_ATOM, $e['date_creation']) ?></published> <updated><?= date(DATE_ATOM, $e['date_creation']) ?></updated> <author> - <name><?= $this->e($e['author']) ?></name> + <name><?= $this->text->e($e['author']) ?></name> </author> <content type="html"> <![CDATA[ diff --git a/app/Template/feed/user.php b/app/Template/feed/user.php index 3e9606c6..28847f1f 100644 --- a/app/Template/feed/user.php +++ b/app/Template/feed/user.php @@ -15,7 +15,7 @@ <published><?= date(DATE_ATOM, $e['date_creation']) ?></published> <updated><?= date(DATE_ATOM, $e['date_creation']) ?></updated> <author> - <name><?= $this->e($e['author']) ?></name> + <name><?= $this->text->e($e['author']) ?></name> </author> <content type="html"> <![CDATA[ diff --git a/app/Template/file/new.php b/app/Template/file/new.php deleted file mode 100644 index a1a59eae..00000000 --- a/app/Template/file/new.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Attach a document') ?></h2> -</div> - -<form action="<?= $this->url->href('file', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" enctype="multipart/form-data"> - <?= $this->form->csrf() ?> - <input type="file" name="files[]" multiple /> - <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></div> - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </div> -</form>
\ No newline at end of file diff --git a/app/Template/file/open.php b/app/Template/file/open.php deleted file mode 100644 index 3df012b6..00000000 --- a/app/Template/file/open.php +++ /dev/null @@ -1,6 +0,0 @@ -<div class="page-header"> - <h2><?= $this->e($file['name']) ?></h2> - <div class="task-file-viewer"> - <img src="<?= $this->url->href('file', 'image', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/> - </div> -</div>
\ No newline at end of file diff --git a/app/Template/file/show.php b/app/Template/file/show.php deleted file mode 100644 index b87739a8..00000000 --- a/app/Template/file/show.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php if (! empty($files) || ! empty($images)): ?> -<div id="attachments" class="task-show-section"> - - <div class="page-header"> - <h2><?= t('Attachments') ?></h2> - </div> - <?php if (! empty($images)): ?> - <h3><?= t('Images') ?></h3> - <ul class="task-show-images"> - <?php foreach ($images as $file): ?> - <li> - <div class="img_container"> - <img src="<?= $this->url->href('file', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/> - </div> - <p> - <?= $this->e($file['name']) ?> - <span class="tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'> - <i class="fa fa-info-circle"></i> - </span> - </p> - <span class="task-show-file-actions task-show-image-actions"> - <i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> - <?php if ($this->user->hasProjectAccess('file', 'remove', $task['project_id'])): ?> - <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> - <?php endif ?> - <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> - </span> - </li> - <?php endforeach ?> - </ul> - <?php endif ?> - - <?php if (! empty($files)): ?> - <h3><?= t('Files') ?></h3> - <table class="task-show-file-table"> - <?php foreach ($files as $file): ?> - <tr> - <td><i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i></td> - <td> - <?= $this->e($file['name']) ?> - <span class="tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'> - <i class="fa fa-info-circle"></i> - </span> - </td> - <td> - <span class="task-show-file-actions"> - <?php if ($this->user->hasProjectAccess('file', 'remove', $task['project_id'])): ?> - <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> - <?php endif ?> - <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> - </span> - </td> - </tr> - <?php endforeach ?> - </table> - <?php endif ?> -</div> -<?php endif ?>
\ No newline at end of file diff --git a/app/Template/file_viewer/show.php b/app/Template/file_viewer/show.php new file mode 100644 index 00000000..191aaa6c --- /dev/null +++ b/app/Template/file_viewer/show.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= $this->text->e($file['name']) ?></h2> +</div> +<div class="file-viewer"> + <?php if ($file['is_image']): ?> + <img src="<?= $this->url->href('FileViewer', 'image', $params) ?>" alt="<?= $this->text->e($file['name']) ?>"> + <?php elseif ($type === 'markdown'): ?> + <article class="markdown"> + <?= $this->text->markdown($content) ?> + </article> + <?php elseif ($type === 'text'): ?> + <pre><?= $content ?></pre> + <?php endif ?> +</div>
\ No newline at end of file diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php index 1face3b8..fe193c2b 100644 --- a/app/Template/gantt/project.php +++ b/app/Template/gantt/project.php @@ -1,5 +1,5 @@ <section id="main"> - <?= $this->render('project/filters', array( + <?= $this->render('project_header/header', array( 'project' => $project, 'filters' => $filters, 'users_list' => $users_list, diff --git a/app/Template/gantt/projects.php b/app/Template/gantt/projects.php index 46d2af91..84b260bb 100644 --- a/app/Template/gantt/projects.php +++ b/app/Template/gantt/projects.php @@ -1,12 +1,6 @@ <section id="main"> <div class="page-header"> <ul> - <?php if ($this->user->hasAccess('project', 'create')): ?> - <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> - <?php endif ?> - <li> - <i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?> - </li> <li> <i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('Projects list'), 'project', 'index') ?> </li> diff --git a/app/Template/gantt/task_creation.php b/app/Template/gantt/task_creation.php index 7997e231..ef1298a9 100644 --- a/app/Template/gantt/task_creation.php +++ b/app/Template/gantt/task_creation.php @@ -33,30 +33,17 @@ </div> <div class="form-column"> - <?= $this->form->label(t('Assignee'), 'owner_id') ?> - <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?><br/> - - <?= $this->form->label(t('Category'), 'category_id') ?> - <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?><br/> - - <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?> - <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> - <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?><br/> - <?php endif ?> - - <?= $this->form->label(t('Complexity'), 'score') ?> - <?= $this->form->number('score', $values, $errors, array('tabindex="6"')) ?><br/> - - <?= $this->form->label(t('Start Date'), 'date_started') ?> - <?= $this->form->text('date_started', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="7"'), 'form-date') ?> - - <?= $this->form->label(t('Due Date'), 'date_due') ?> - <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="8"'), 'form-date') ?><br/> - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + <?= $this->task->selectAssignee($users_list, $values, $errors) ?> + <?= $this->task->selectCategory($categories_list, $values, $errors) ?> + <?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?> + <?= $this->task->selectPriority($project, $values) ?> + <?= $this->task->selectScore($values, $errors) ?> + <?= $this->task->selectStartDate($values, $errors) ?> + <?= $this->task->selectDueDate($values, $errors) ?> </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="11"/> + <button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> </div> diff --git a/app/Template/group/associate.php b/app/Template/group/associate.php index dc665bb3..9de46f35 100644 --- a/app/Template/group/associate.php +++ b/app/Template/group/associate.php @@ -13,10 +13,10 @@ <?= $this->form->hidden('group_id', $values) ?> <?= $this->form->label(t('User'), 'user_id') ?> - <?= $this->form->select('user_id', $users, $values, $errors, array('required'), 'chosen-select') ?><br/> + <?= $this->form->select('user_id', $users, $values, $errors, array('required'), 'chosen-select') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'group', 'index') ?> </div> diff --git a/app/Template/group/create.php b/app/Template/group/create.php index 696e5013..4ce6b1f3 100644 --- a/app/Template/group/create.php +++ b/app/Template/group/create.php @@ -8,10 +8,10 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?><br/> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'group', 'index') ?> </div> diff --git a/app/Template/group/edit.php b/app/Template/group/edit.php index 4d7e5e81..e9d9dd5a 100644 --- a/app/Template/group/edit.php +++ b/app/Template/group/edit.php @@ -11,10 +11,10 @@ <?= $this->form->hidden('external_id', $values) ?> <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?><br/> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'group', 'index') ?> </div> diff --git a/app/Template/group/index.php b/app/Template/group/index.php index d111b5d9..8302e5a8 100644 --- a/app/Template/group/index.php +++ b/app/Template/group/index.php @@ -21,10 +21,10 @@ #<?= $group['id'] ?> </td> <td> - <?= $this->e($group['external_id']) ?> + <?= $this->text->e($group['external_id']) ?> </td> <td> - <?= $this->e($group['name']) ?> + <?= $this->text->e($group['name']) ?> </td> <td> <div class="dropdown"> diff --git a/app/Template/group/users.php b/app/Template/group/users.php index f79cb9ad..bbd41525 100644 --- a/app/Template/group/users.php +++ b/app/Template/group/users.php @@ -22,13 +22,13 @@ <?= $this->url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?> </td> <td> - <?= $this->url->link($this->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> + <?= $this->url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> </td> <td> - <?= $this->e($user['name']) ?> + <?= $this->text->e($user['name']) ?> </td> <td> - <a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a> + <a href="mailto:<?= $this->text->e($user['email']) ?>"><?= $this->text->e($user['email']) ?></a> </td> <td> <?= $this->url->link(t('Remove this user'), 'group', 'dissociate', array('group_id' => $group['id'], 'user_id' => $user['id'])) ?> diff --git a/app/Template/header.php b/app/Template/header.php index 405d07b3..d891118c 100644 --- a/app/Template/header.php +++ b/app/Template/header.php @@ -5,10 +5,10 @@ <?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, '', t('Dashboard')) ?> </span> <span class="title"> - <?= $this->e($title) ?> + <?= $this->text->e($title) ?> </span> <?php if (! empty($description)): ?> - <span class="tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($description)) ?>'> <i class="fa fa-info-circle"></i> </span> <?php endif ?> @@ -26,28 +26,80 @@ data-redirect-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>"> <option value=""></option> <?php foreach ($board_selector as $board_id => $board_name): ?> - <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option> + <option value="<?= $board_id ?>"><?= $this->text->e($board_name) ?></option> <?php endforeach ?> </select> </li> <?php endif ?> - <li> + <li class="user-links"> <?php if ($this->user->hasNotifications()): ?> <span class="notification"> <?= $this->url->link('<i class="fa fa-bell web-notification-icon"></i>', 'app', 'notifications', array('user_id' => $this->user->getId()), false, '', t('Unread notifications')) ?> </span> <?php endif ?> - <span class="dropdown"> + <?php $has_project_creation_access = $this->user->hasAccess('ProjectCreation', 'create'); ?> + <?php $is_private_project_enabled = $this->app->config('disable_private_project', 0) == 0; ?> + + <?php if ($has_project_creation_access || (!$has_project_creation_access && $is_private_project_enabled)): ?> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-plus fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <?php if ($has_project_creation_access): ?> + <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'ProjectCreation', 'create', array(), false, 'popover') ?></li> + <?php endif ?> + <?php if ($is_private_project_enabled): ?> + <li> + <i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'ProjectCreation', 'createPrivate', array(), false, 'popover') ?> + </li> + <?php endif ?> + </ul> + </div> + <?php endif ?> + + <div class="dropdown"> <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-user fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> - <li class="no-hover"><strong><?= $this->e($this->user->getFullname()) ?></strong></li> - <li><?= $this->url->link(t('My dashboard'), 'app', 'index', array('user_id' => $this->user->getId())) ?></li> - <li><?= $this->url->link(t('My profile'), 'user', 'show', array('user_id' => $this->user->getId())) ?></li> - <li><?= $this->url->link(t('Logout'), 'auth', 'logout') ?></li> + <li class="no-hover"><strong><?= $this->text->e($this->user->getFullname()) ?></strong></li> + <li> + <i class="fa fa-tachometer fa-fw"></i> + <?= $this->url->link(t('My dashboard'), 'app', 'index', array('user_id' => $this->user->getId())) ?> + </li> + <li> + <i class="fa fa-home fa-fw"></i> + <?= $this->url->link(t('My profile'), 'user', 'show', array('user_id' => $this->user->getId())) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('Projects management'), 'project', 'index') ?> + </li> + <?php if ($this->user->hasAccess('user', 'index')): ?> + <li> + <i class="fa fa-user fa-fw"></i> + <?= $this->url->link(t('Users management'), 'user', 'index') ?> + </li> + <li> + <i class="fa fa-group fa-fw"></i> + <?= $this->url->link(t('Groups management'), 'group', 'index') ?> + </li> + <li> + <i class="fa fa-cog fa-fw"></i> + <?= $this->url->link(t('Settings'), 'config', 'index') ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-life-ring fa-fw"></i> + <?= $this->url->link(t('Documentation'), 'doc', 'show') ?> + </li> + <?php if (! DISABLE_LOGOUT): ?> + <li> + <i class="fa fa-sign-out fa-fw"></i> + <?= $this->url->link(t('Logout'), 'auth', 'logout') ?> + </li> + <?php endif ?> </ul> - </span> + </div> </li> </ul> </nav> -</header>
\ No newline at end of file +</header> diff --git a/app/Template/layout.php b/app/Template/layout.php index 0c81aac2..a694a96a 100644 --- a/app/Template/layout.php +++ b/app/Template/layout.php @@ -30,7 +30,7 @@ <link rel="apple-touch-icon" sizes="114x114" href="<?= $this->url->dir() ?>assets/img/touch-icon-iphone-retina.png"> <link rel="apple-touch-icon" sizes="144x144" href="<?= $this->url->dir() ?>assets/img/touch-icon-ipad-retina.png"> - <title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title> + <title><?= isset($title) ? $this->text->e($title) : 'Kanboard' ?></title> <?= $this->hook->render('template:layout:head') ?> </head> diff --git a/app/Template/link/create.php b/app/Template/link/create.php index 2b4ac62c..3b36abee 100644 --- a/app/Template/link/create.php +++ b/app/Template/link/create.php @@ -13,6 +13,6 @@ <?= $this->form->text('opposite_label', $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/link/edit.php b/app/Template/link/edit.php index 516de464..e91422be 100644 --- a/app/Template/link/edit.php +++ b/app/Template/link/edit.php @@ -14,7 +14,7 @@ <?= $this->form->select('opposite_id', $labels, $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'link', 'index') ?> </div> diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php index aa17b228..10dcedcc 100644 --- a/app/Template/listing/show.php +++ b/app/Template/listing/show.php @@ -1,10 +1,13 @@ <section id="main"> - <?= $this->render('project/filters', array( + <?= $this->render('project_header/header', array( 'project' => $project, 'filters' => $filters, + 'custom_filters_list' => $custom_filters_list, + 'users_list' => $users_list, + 'categories_list' => $categories_list, )) ?> - <?php if (! empty($values['search']) && $paginator->isEmpty()): ?> + <?php if ($paginator->isEmpty()): ?> <p class="alert"><?= t('No tasks found.') ?></p> <?php elseif (! $paginator->isEmpty()): ?> <table class="table-fixed table-small"> @@ -21,29 +24,29 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->render('task/dropdown', array('task' => $task)) ?> </td> <td> - <?= $this->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> + <?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> </td> <td> - <?= $this->e($task['column_name']) ?> + <?= $this->text->e($task['column_name']) ?> </td> <td> - <?= $this->e($task['category_name']) ?> + <?= $this->text->e($task['category_name']) ?> </td> <td> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> <?php if ($task['assignee_username']): ?> - <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?> <?php else: ?> <?= t('Unassigned') ?> <?php endif ?> </td> <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> + <?= $this->dt->date($task['date_due']) ?> </td> <td> <?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?> diff --git a/app/Template/notification/comment_create.php b/app/Template/notification/comment_create.php index 2f2c0fa6..fefc8ba1 100644 --- a/app/Template/notification/comment_create.php +++ b/app/Template/notification/comment_create.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <?php if (! empty($comment['username'])): ?> <h3><?= t('New comment posted by %s', $comment['name'] ?: $comment['username']) ?></h3> diff --git a/app/Template/notification/comment_update.php b/app/Template/notification/comment_update.php index a15e5d6d..2477d8b3 100644 --- a/app/Template/notification/comment_update.php +++ b/app/Template/notification/comment_update.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <h3><?= t('Comment updated') ?></h3> diff --git a/app/Template/notification/comment_user_mention.php b/app/Template/notification/comment_user_mention.php index 59f5127e..372183df 100644 --- a/app/Template/notification/comment_user_mention.php +++ b/app/Template/notification/comment_user_mention.php @@ -1,6 +1,6 @@ <h2><?= t('You were mentioned in a comment on the task #%d', $task['id']) ?></h2> -<p><?= $this->e($task['title']) ?></p> +<p><?= $this->text->e($task['title']) ?></p> <?= $this->text->markdown($comment['comment']) ?> diff --git a/app/Template/notification/subtask_create.php b/app/Template/notification/subtask_create.php index e1c62b73..f6f8ba13 100644 --- a/app/Template/notification/subtask_create.php +++ b/app/Template/notification/subtask_create.php @@ -1,15 +1,15 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <h3><?= t('New sub-task') ?></h3> <ul> - <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li> - <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li> - <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li><?= t('Title:') ?> <?= $this->text->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->text->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->text->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> <li> <?= t('Time tracking:') ?> <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <strong><?= $this->text->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> <?php endif ?> </li> </ul> diff --git a/app/Template/notification/subtask_update.php b/app/Template/notification/subtask_update.php index cfde9db6..e6785f2c 100644 --- a/app/Template/notification/subtask_update.php +++ b/app/Template/notification/subtask_update.php @@ -1,19 +1,19 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <h3><?= t('Sub-task updated') ?></h3> <ul> - <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li> - <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li> - <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li><?= t('Title:') ?> <?= $this->text->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->text->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->text->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> <li> <?= t('Time tracking:') ?> <?php if (! empty($subtask['time_spent'])): ?> - <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <strong><?= $this->text->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> <?php endif ?> <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <strong><?= $this->text->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> <?php endif ?> </li> </ul> diff --git a/app/Template/notification/task_assignee_change.php b/app/Template/notification/task_assignee_change.php index c9729ac9..53f7c5c1 100644 --- a/app/Template/notification/task_assignee_change.php +++ b/app/Template/notification/task_assignee_change.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> diff --git a/app/Template/notification/task_close.php b/app/Template/notification/task_close.php index 463223a0..4202e092 100644 --- a/app/Template/notification/task_close.php +++ b/app/Template/notification/task_close.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <p><?= t('The task #%d have been closed.', $task['id']) ?></p> diff --git a/app/Template/notification/task_create.php b/app/Template/notification/task_create.php index 1d834d44..3cd68ac0 100644 --- a/app/Template/notification/task_create.php +++ b/app/Template/notification/task_create.php @@ -1,12 +1,12 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> - <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?> + <?= t('Created:').' '.$this->dt->datetime($task['date_creation']) ?> </li> <?php if ($task['date_due']): ?> <li> - <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong> + <strong><?= t('Due date:').' '.$this->dt->date($task['date_due']) ?></strong> </li> <?php endif ?> <?php if (! empty($task['creator_username'])): ?> @@ -25,12 +25,12 @@ </li> <li> <?= t('Column on the board:') ?> - <strong><?= $this->e($task['column_title']) ?></strong> + <strong><?= $this->text->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->text->e($task['position']) ?></li> <?php if (! empty($task['category_name'])): ?> <li> - <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong> + <?= t('Category:') ?> <strong><?= $this->text->e($task['category_name']) ?></strong> </li> <?php endif ?> </ul> diff --git a/app/Template/notification/file_create.php b/app/Template/notification/task_file_create.php index 63f7d1b8..feab8dd2 100644 --- a/app/Template/notification/file_create.php +++ b/app/Template/notification/task_file_create.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <p><?= t('New attachment added "%s"', $file['name']) ?></p> diff --git a/app/Template/notification/task_move_column.php b/app/Template/notification/task_move_column.php index 88ab8ab5..263adebf 100644 --- a/app/Template/notification/task_move_column.php +++ b/app/Template/notification/task_move_column.php @@ -1,11 +1,11 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> <?= t('Column on the board:') ?> - <strong><?= $this->e($task['column_title']) ?></strong> + <strong><?= $this->text->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->text->e($task['position']) ?></li> </ul> <?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_move_position.php b/app/Template/notification/task_move_position.php index 88ab8ab5..263adebf 100644 --- a/app/Template/notification/task_move_position.php +++ b/app/Template/notification/task_move_position.php @@ -1,11 +1,11 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> <?= t('Column on the board:') ?> - <strong><?= $this->e($task['column_title']) ?></strong> + <strong><?= $this->text->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->text->e($task['position']) ?></li> </ul> <?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_move_swimlane.php b/app/Template/notification/task_move_swimlane.php index 04de7cc6..cbbd620d 100644 --- a/app/Template/notification/task_move_swimlane.php +++ b/app/Template/notification/task_move_swimlane.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> @@ -6,14 +6,14 @@ <?= t('The task have been moved to the first swimlane') ?> <?php else: ?> <?= t('The task have been moved to another swimlane:') ?> - <strong><?= $this->e($task['swimlane_name']) ?></strong> + <strong><?= $this->text->e($task['swimlane_name']) ?></strong> <?php endif ?> </li> <li> <?= t('Column on the board:') ?> - <strong><?= $this->e($task['column_title']) ?></strong> + <strong><?= $this->text->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->text->e($task['position']) ?></li> </ul> <?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_open.php b/app/Template/notification/task_open.php index cb02a79c..6eb4ec08 100644 --- a/app/Template/notification/task_open.php +++ b/app/Template/notification/task_open.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <p><?= t('The task #%d have been opened.', $task['id']) ?></p> diff --git a/app/Template/notification/task_overdue.php b/app/Template/notification/task_overdue.php index a231937b..ac0665a2 100644 --- a/app/Template/notification/task_overdue.php +++ b/app/Template/notification/task_overdue.php @@ -5,11 +5,11 @@ <li> (<strong>#<?= $task['id'] ?></strong>) <?php if ($application_url): ?> - <a href="<?= $this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', true) ?>"><?= $this->e($task['title']) ?></a> + <a href="<?= $this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', true) ?>"><?= $this->text->e($task['title']) ?></a> <?php else: ?> - <?= $this->e($task['title']) ?> + <?= $this->text->e($task['title']) ?> <?php endif ?> - (<?= dt('%B %e, %Y', $task['date_due']) ?>) + (<?= $this->dt->date($task['date_due']) ?>) <?php if ($task['assignee_username']): ?> (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>) <?php endif ?> diff --git a/app/Template/notification/task_update.php b/app/Template/notification/task_update.php index 54f5c6d1..8adb2553 100644 --- a/app/Template/notification/task_update.php +++ b/app/Template/notification/task_update.php @@ -1,4 +1,4 @@ -<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->text->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <?= $this->render('task/changes', array('changes' => $changes, 'task' => $task)) ?> <?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_user_mention.php b/app/Template/notification/task_user_mention.php index 40ddddca..3d8c8e95 100644 --- a/app/Template/notification/task_user_mention.php +++ b/app/Template/notification/task_user_mention.php @@ -1,5 +1,5 @@ <h2><?= t('You were mentioned in the task #%d', $task['id']) ?></h2> -<p><?= $this->e($task['title']) ?></p> +<p><?= $this->text->e($task['title']) ?></p> <h2><?= t('Description') ?></h2> <?= $this->text->markdown($task['description']) ?> diff --git a/app/Template/password_reset/change.php b/app/Template/password_reset/change.php index 310f0f97..0a1d8de4 100644 --- a/app/Template/password_reset/change.php +++ b/app/Template/password_reset/change.php @@ -4,13 +4,13 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('New password'), 'password') ?> - <?= $this->form->password('password', $values, $errors) ?><br/> + <?= $this->form->password('password', $values, $errors) ?> <?= $this->form->label(t('Confirmation'), 'confirmation') ?> <?= $this->form->password('confirmation', $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Change Password') ?></button> </div> </form> </div>
\ No newline at end of file diff --git a/app/Template/password_reset/create.php b/app/Template/password_reset/create.php index ef958011..918a0eb4 100644 --- a/app/Template/password_reset/create.php +++ b/app/Template/password_reset/create.php @@ -11,7 +11,7 @@ <?= $this->form->text('captcha', array(), $errors, array('required')) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Change Password') ?></button> </div> </form> </div>
\ No newline at end of file diff --git a/app/Template/project/duplicate.php b/app/Template/project/duplicate.php index 8967c306..7cfa8969 100644 --- a/app/Template/project/duplicate.php +++ b/app/Template/project/duplicate.php @@ -10,13 +10,17 @@ <?= $this->form->csrf() ?> + <?php if ($project['is_private'] == 0): ?> + <?= $this->form->checkbox('projectPermission', t('Permissions'), 1, true) ?> + <?php endif ?> + <?= $this->form->checkbox('category', t('Categories'), 1, true) ?> <?= $this->form->checkbox('action', t('Actions'), 1, true) ?> <?= $this->form->checkbox('swimlane', t('Swimlanes'), 1, false) ?> <?= $this->form->checkbox('task', t('Tasks'), 1, false) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Duplicate') ?>" class="btn btn-red"/> + <button type="submit" class="btn btn-red"><?= t('Duplicate') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> </div> </form> diff --git a/app/Template/project/filters.php b/app/Template/project/filters.php deleted file mode 100644 index 78794e1c..00000000 --- a/app/Template/project/filters.php +++ /dev/null @@ -1,106 +0,0 @@ -<div class="page-header"> - <?= $this->hook->render('template:project:header:before', array('project' => $project)) ?> - - <div class="dropdown"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> - <ul> - <?php if (isset($is_board)): ?> - <li> - <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>> - <i class="fa fa-expand fa-fw"></i> - <?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> - </span> - <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? 'style="display: none;"' : '' ?>> - <i class="fa fa-compress fa-fw"></i> - <?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> - </span> - </li> - <li> - <span class="filter-compact"> - <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Compact view') ?></a> - </span> - <span class="filter-wide" style="display: none"> - <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a> - </span> - </li> - <li> - <span class="filter-max-height" style="display: none"> - <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Set maximum column height') ?></a> - </span> - <span class="filter-min-height"> - <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Remove maximum column height') ?></a> - </span> - </li> - <?php endif ?> - <?= $this->render('project/dropdown', array('project' => $project)) ?> - </ul> - </div> - <ul class="views"> - <li <?= $filters['controller'] === 'board' ? 'class="active"' : '' ?>> - <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Board'), 'board', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?> - </li> - <li <?= $filters['controller'] === 'calendar' ? 'class="active"' : '' ?>> - <i class="fa fa-calendar fa-fw"></i> - <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?> - </li> - <li <?= $filters['controller'] === 'listing' ? 'class="active"' : '' ?>> - <i class="fa fa-list fa-fw"></i> - <?= $this->url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?> - </li> - <?php if ($this->user->hasProjectAccess('gantt', 'project', $project['id'])): ?> - <li <?= $filters['controller'] === 'gantt' ? 'class="active"' : '' ?>> - <i class="fa fa-sliders fa-fw"></i> - <?= $this->url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?> - </li> - <?php endif ?> - </ul> - <form method="get" action="<?= $this->url->dir() ?>" class="search"> - <?= $this->form->hidden('controller', $filters) ?> - <?= $this->form->hidden('action', $filters) ?> - <?= $this->form->hidden('project_id', $filters) ?> - <?= $this->form->text('search', $filters, array(), array('placeholder="'.t('Filter').'"'), 'form-input-large') ?> - </form> - - <div class="filter-dropdowns"> - - <?= $this->render('app/filters_helper', array('reset' => 'status:open', 'project' => $project)) ?> - - <?php if (isset($custom_filters_list) && ! empty($custom_filters_list)): ?> - <div class="dropdown filters"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('My filters') ?></a> - <ul> - <?php foreach ($custom_filters_list as $filter): ?> - <li><a href="#" class="filter-helper" data-<?php if ($filter['append']): ?><?= 'append-' ?><?php endif ?>filter='<?= $this->e($filter['filter']) ?>'><?= $this->e($filter['name']) ?></a></li> - <?php endforeach ?> - </ul> - </div> - <?php endif ?> - - <?php if (isset($users_list)): ?> - <div class="dropdown filters"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Users') ?></a> - <ul> - <li><a href="#" class="filter-helper" data-append-filter="assignee:nobody"><?= t('Not assigned') ?></a></li> - <?php foreach ($users_list as $user): ?> - <li><a href="#" class="filter-helper" data-append-filter='assignee:"<?= $this->e($user) ?>"'><?= $this->e($user) ?></a></li> - <?php endforeach ?> - </ul> - </div> - <?php endif ?> - - <?php if (isset($categories_list) && ! empty($categories_list)): ?> - <div class="dropdown filters"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Categories') ?></a> - <ul> - <li><a href="#" class="filter-helper" data-append-filter="category:none"><?= t('No category') ?></a></li> - <?php foreach ($categories_list as $category): ?> - <li><a href="#" class="filter-helper" data-append-filter='category:"<?= $this->e($category) ?>"'><?= $this->e($category) ?></a></li> - <?php endforeach ?> - </ul> - </div> - <?php endif ?> - </div> - - <?= $this->hook->render('template:project:header:after', array('project' => $project)) ?> -</div>
\ No newline at end of file diff --git a/app/Template/project/index.php b/app/Template/project/index.php index 3d2a33ea..10d4aaa2 100644 --- a/app/Template/project/index.php +++ b/app/Template/project/index.php @@ -1,10 +1,6 @@ <section id="main"> <div class="page-header"> <ul> - <?php if ($this->user->hasAccess('project', 'create')): ?> - <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> - <?php endif ?> - <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'createPrivate') ?></li> <?php if ($this->user->hasAccess('projectuser', 'managers')): ?> <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'projectuser', 'managers') ?></li> <?php endif ?> @@ -53,22 +49,22 @@ <?php endif ?> <?php if (! empty($project['description'])): ?> - <span class="tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($project['description'])) ?>'> <i class="fa fa-info-circle"></i> </span> <?php endif ?> - <?= $this->url->link($this->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> + <?= $this->url->link($this->text->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> </td> <td> - <?= $project['start_date'] ?> + <?= $this->dt->date($project['start_date']) ?> </td> <td> - <?= $project['end_date'] ?> + <?= $this->dt->date($project['end_date']) ?> </td> <td> <?php if ($project['owner_id'] > 0): ?> - <?= $this->e($project['owner_name'] ?: $project['owner_username']) ?> + <?= $this->text->e($project['owner_name'] ?: $project['owner_username']) ?> <?php endif ?> </td> <?php if ($this->user->hasAccess('projectuser', 'managers')): ?> @@ -80,7 +76,7 @@ <td class="dashboard-project-stats"> <?php foreach ($project['columns'] as $column): ?> <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong> - <span><?= $this->e($column['title']) ?></span> + <span><?= $this->text->e($column['title']) ?></span> <?php endforeach ?> </td> </tr> diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php index 8ba92ef9..eb391ae5 100644 --- a/app/Template/project/layout.php +++ b/app/Template/project/layout.php @@ -30,7 +30,7 @@ <?= $this->render($sidebar_template, array('project' => $project)) ?> <div class="sidebar-content"> - <?= $project_content_for_layout ?> + <?= $content_for_sublayout ?> </div> </section> </section>
\ No newline at end of file diff --git a/app/Template/project/new.php b/app/Template/project/new.php deleted file mode 100644 index 8e4ccfec..00000000 --- a/app/Template/project/new.php +++ /dev/null @@ -1,24 +0,0 @@ -<section id="main"> - <div class="page-header"> - <ul> - <li><i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('All projects'), 'project', 'index') ?></li> - </ul> - </div> - <form method="post" action="<?= $this->url->href('project', 'save') ?>" autocomplete="off"> - - <?= $this->form->csrf() ?> - <?= $this->form->hidden('is_private', $values) ?> - <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'index') ?> - </div> - </form> - <?php if (isset($is_private) && $is_private): ?> - <div class="alert alert-info"> - <p><?= t('There is no user management for private projects.') ?></p> - </div> - <?php endif ?> -</section>
\ No newline at end of file diff --git a/app/Template/project/notifications.php b/app/Template/project/notifications.php index b39d6c05..494a322a 100644 --- a/app/Template/project/notifications.php +++ b/app/Template/project/notifications.php @@ -12,7 +12,7 @@ <?= $this->form->checkboxes('notification_types', $types, $notifications) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> </div> diff --git a/app/Template/project/show.php b/app/Template/project/show.php index 5f1aefc1..42eeec4d 100644 --- a/app/Template/project/show.php +++ b/app/Template/project/show.php @@ -5,7 +5,7 @@ <li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li> <?php if ($project['owner_id'] > 0): ?> - <li><?= t('Project owner: ') ?><strong><?= $this->e($project['owner_name'] ?: $project['owner_username']) ?></strong></li> + <li><?= t('Project owner: ') ?><strong><?= $this->text->e($project['owner_name'] ?: $project['owner_username']) ?></strong></li> <?php endif ?> <?php if ($project['is_private']): ?> @@ -21,15 +21,15 @@ <?php endif ?> <?php if ($project['last_modified']): ?> - <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li> + <li><?= t('Modified:').' '.$this->dt->datetime($project['last_modified']) ?></li> <?php endif ?> <?php if ($project['start_date']): ?> - <li><?= t('Start date: %s', $project['start_date']) ?></li> + <li><?= t('Start date: ').$this->dt->date($project['start_date']) ?></li> <?php endif ?> <?php if ($project['end_date']): ?> - <li><?= t('End date: %s', $project['end_date']) ?></li> + <li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li> <?php endif ?> <?php if ($stats['nb_tasks'] > 0): ?> @@ -61,9 +61,9 @@ <?php foreach ($stats['columns'] as $column): ?> <tr> <td> - <?= $this->e($column['title']) ?> + <?= $this->text->e($column['title']) ?> <?php if (! empty($column['description'])): ?> - <span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($column['description'])) ?>'> <i class="fa fa-info-circle"></i> </span> <?php endif ?> diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php index 2f2ce3ce..304b4aee 100644 --- a/app/Template/project/sidebar.php +++ b/app/Template/project/sidebar.php @@ -63,6 +63,4 @@ <?= $this->hook->render('template:project:sidebar', array('project' => $project)) ?> </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div> diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php new file mode 100644 index 00000000..c34173a9 --- /dev/null +++ b/app/Template/project_creation/create.php @@ -0,0 +1,42 @@ +<section id="main"> + <div class="page-header"> + <h2><?= $title ?></h2> + </div> + <form class="popover-form" id="project-creation-form" method="post" action="<?= $this->url->href('ProjectCreation', 'save') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('is_private', $values) ?> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <?php if (count($projects_list) > 1): ?> + <?= $this->form->label(t('Create from another project'), 'src_project_id') ?> + <?= $this->form->select('src_project_id', $projects_list, $values) ?> + <?php endif ?> + + <div class="project-creation-options" <?= isset($values['src_project_id']) && $values['src_project_id'] > 0 ? '' : 'style="display: none"' ?>> + <p class="alert"><?= t('Which parts of the project do you want to duplicate?') ?></p> + + <?php if (! $is_private): ?> + <?= $this->form->checkbox('projectPermission', t('Permissions'), 1, true) ?> + <?php endif ?> + + <?= $this->form->checkbox('category', t('Categories'), 1, true) ?> + <?= $this->form->checkbox('action', t('Actions'), 1, true) ?> + <?= $this->form->checkbox('swimlane', t('Swimlanes'), 1, true) ?> + <?= $this->form->checkbox('task', t('Tasks'), 1, false) ?> + </div> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'project', 'index', array(), false, 'close-popover') ?> + </div> + </form> + <?php if ($is_private): ?> + <div class="alert alert-info"> + <p><?= t('There is no user management for private projects.') ?></p> + </div> + <?php endif ?> +</section>
\ No newline at end of file diff --git a/app/Template/project_edit/dates.php b/app/Template/project_edit/dates.php index cb585c6a..83651592 100644 --- a/app/Template/project_edit/dates.php +++ b/app/Template/project_edit/dates.php @@ -19,7 +19,7 @@ <?= $this->form->text('end_date', $values, $errors, array('maxlength="10"'), 'form-date') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> diff --git a/app/Template/project_edit/description.php b/app/Template/project_edit/description.php index dce8ab10..6bfe8b8d 100644 --- a/app/Template/project_edit/description.php +++ b/app/Template/project_edit/description.php @@ -32,6 +32,6 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> diff --git a/app/Template/project_edit/general.php b/app/Template/project_edit/general.php index 5caefa2d..5e6d32b4 100644 --- a/app/Template/project_edit/general.php +++ b/app/Template/project_edit/general.php @@ -24,13 +24,13 @@ <?= $this->form->select('owner_id', $owners, $values, $errors) ?> </div> - <?php if ($this->user->hasProjectAccess('project', 'create', $project['id'])): ?> + <?php if ($this->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])): ?> <hr> <?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?> <p class="form-help"><?= t('Private projects do not have users and groups management.') ?></p> <?php endif ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> diff --git a/app/Template/project_edit/task_priority.php b/app/Template/project_edit/task_priority.php index e54215b2..faf49f5d 100644 --- a/app/Template/project_edit/task_priority.php +++ b/app/Template/project_edit/task_priority.php @@ -22,7 +22,7 @@ <?= $this->form->number('priority_end', $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> diff --git a/app/Template/project_file/create.php b/app/Template/project_file/create.php new file mode 100644 index 00000000..67315285 --- /dev/null +++ b/app/Template/project_file/create.php @@ -0,0 +1,33 @@ +<div class="page-header"> + <h2><?= t('Attach a document') ?></h2> +</div> +<div id="file-done" style="display:none"> + <p class="alert alert-success"> + <?= t('All files have been uploaded successfully.') ?> + <?= $this->url->link(t('View uploaded files'), 'ProjectOverview', 'show', array('project_id' => $project['id'])) ?> + </p> +</div> + +<div id="file-error-max-size" style="display:none"> + <p class="alert alert-error"> + <?= t('The maximum allowed file size is %sB.', $this->text->bytes($max_size)) ?> + <a href="#" id="file-browser"><?= t('Choose files again') ?></a> + </p> +</div> + +<div + id="file-dropzone" + data-max-size="<?= $max_size ?>" + data-url="<?= $this->url->href('ProjectFile', 'save', array('project_id' => $project['id'])) ?>"> + <div id="file-dropzone-inner"> + <?= t('Drag and drop your files here') ?> <?= t('or') ?> <a href="#" id="file-browser"><?= t('choose files') ?></a> + </div> +</div> + +<input type="file" name="files[]" multiple style="display:none" id="file-form-element"> + +<div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" id="file-upload-button" disabled> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +</div> diff --git a/app/Template/project_file/remove.php b/app/Template/project_file/remove.php new file mode 100644 index 00000000..ba834288 --- /dev/null +++ b/app/Template/project_file/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a file') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this file: "%s"?', $this->text->e($file['name'])) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'ProjectFile', 'remove', array('project_id' => $project['id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php new file mode 100644 index 00000000..bbc033bf --- /dev/null +++ b/app/Template/project_header/dropdown.php @@ -0,0 +1,34 @@ +<div class="dropdown"> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?php if ($is_board): ?> + <li> + <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>> + <i class="fa fa-expand fa-fw"></i> + <?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + </span> + <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? 'style="display: none;"' : '' ?>> + <i class="fa fa-compress fa-fw"></i> + <?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + </span> + </li> + <li> + <span class="filter-compact"> + <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Compact view') ?></a> + </span> + <span class="filter-wide" style="display: none"> + <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a> + </span> + </li> + <li> + <span class="filter-max-height" style="display: none"> + <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Set maximum column height') ?></a> + </span> + <span class="filter-min-height"> + <i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Remove maximum column height') ?></a> + </span> + </li> + <?php endif ?> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/project_header/header.php b/app/Template/project_header/header.php new file mode 100644 index 00000000..f6e5af9e --- /dev/null +++ b/app/Template/project_header/header.php @@ -0,0 +1,15 @@ +<div class="project-header"> + <?= $this->hook->render('template:project:header:before', array('project' => $project)) ?> + + <?= $this->render('project_header/dropdown', array('project' => $project, 'is_board' => isset($is_board))) ?> + <?= $this->render('project_header/views', array('project' => $project, 'filters' => $filters)) ?> + <?= $this->render('project_header/search', array( + 'project' => $project, + 'filters' => $filters, + 'custom_filters_list' => isset($custom_filters_list) ? $custom_filters_list : array(), + 'users_list' => isset($users_list) ? $users_list : array(), + 'categories_list' => isset($categories_list) ? $categories_list : array(), + )) ?> + + <?= $this->hook->render('template:project:header:after', array('project' => $project)) ?> +</div>
\ No newline at end of file diff --git a/app/Template/project_header/search.php b/app/Template/project_header/search.php new file mode 100644 index 00000000..42216352 --- /dev/null +++ b/app/Template/project_header/search.php @@ -0,0 +1,45 @@ +<div class="filter-box"> + <form method="get" action="<?= $this->url->dir() ?>" class="search"> + <?= $this->form->hidden('controller', $filters) ?> + <?= $this->form->hidden('action', $filters) ?> + <?= $this->form->hidden('project_id', $filters) ?> + <?= $this->form->text('search', $filters, array(), array('placeholder="'.t('Filter').'"')) ?> + + <?= $this->render('app/filters_helper', array('reset' => 'status:open', 'project' => $project)) ?> + + <?php if (isset($custom_filters_list) && ! empty($custom_filters_list)): ?> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Custom filters') ?>"><i class="fa fa-bookmark fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <?php foreach ($custom_filters_list as $filter): ?> + <li><a href="#" class="filter-helper" data-<?php if ($filter['append']): ?><?= 'append-' ?><?php endif ?>filter='<?= $this->text->e($filter['filter']) ?>'><?= $this->text->e($filter['name']) ?></a></li> + <?php endforeach ?> + </ul> + </div> + <?php endif ?> + + <?php if (isset($users_list)): ?> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('User filters') ?>"><i class="fa fa-users fa-fw"></i> <i class="fa fa-caret-down"></i></a> + <ul> + <li><a href="#" class="filter-helper" data-append-filter="assignee:nobody"><?= t('Not assigned') ?></a></li> + <?php foreach ($users_list as $user): ?> + <li><a href="#" class="filter-helper" data-append-filter='assignee:"<?= $this->text->e($user) ?>"'><?= $this->text->e($user) ?></a></li> + <?php endforeach ?> + </ul> + </div> + <?php endif ?> + + <?php if (isset($categories_list) && ! empty($categories_list)): ?> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Category filters') ?>"><i class="fa fa-tags fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li><a href="#" class="filter-helper" data-append-filter="category:none"><?= t('No category') ?></a></li> + <?php foreach ($categories_list as $category): ?> + <li><a href="#" class="filter-helper" data-append-filter='category:"<?= $this->text->e($category) ?>"'><?= $this->text->e($category) ?></a></li> + <?php endforeach ?> + </ul> + </div> + <?php endif ?> + </form> +</div> diff --git a/app/Template/project_header/views.php b/app/Template/project_header/views.php new file mode 100644 index 00000000..f8fdbb02 --- /dev/null +++ b/app/Template/project_header/views.php @@ -0,0 +1,24 @@ +<ul class="views"> + <li <?= $this->app->getRouterController() === 'ProjectOverview' ? 'class="active"' : '' ?>> + <i class="fa fa-eye fa-fw"></i> + <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?> + </li> + <li <?= $this->app->getRouterController() === 'Board' ? 'class="active"' : '' ?>> + <i class="fa fa-th fa-fw"></i> + <?= $this->url->link(t('Board'), 'board', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?> + </li> + <li <?= $this->app->getRouterController() === 'Calendar' ? 'class="active"' : '' ?>> + <i class="fa fa-calendar fa-fw"></i> + <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?> + </li> + <li <?= $this->app->getRouterController() === 'Listing' ? 'class="active"' : '' ?>> + <i class="fa fa-list fa-fw"></i> + <?= $this->url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?> + </li> + <?php if ($this->user->hasProjectAccess('gantt', 'project', $project['id'])): ?> + <li <?= $this->app->getRouterController() === 'Gantt' ? 'class="active"' : '' ?>> + <i class="fa fa-sliders fa-fw"></i> + <?= $this->url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?> + </li> + <?php endif ?> +</ul>
\ No newline at end of file diff --git a/app/Template/project_overview/columns.php b/app/Template/project_overview/columns.php new file mode 100644 index 00000000..cc5782bd --- /dev/null +++ b/app/Template/project_overview/columns.php @@ -0,0 +1,8 @@ +<div class="project-overview-columns"> + <?php foreach ($project['columns'] as $column): ?> + <div class="project-overview-column"> + <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong><br> + <span><?= $this->text->e($column['title']) ?></span> + </div> + <?php endforeach ?> +</div> diff --git a/app/Template/project_overview/description.php b/app/Template/project_overview/description.php new file mode 100644 index 00000000..cd6e2450 --- /dev/null +++ b/app/Template/project_overview/description.php @@ -0,0 +1,8 @@ +<?php if (! empty($project['description'])): ?> + <div class="page-header"> + <h2><?= $this->text->e($project['name']) ?></h2> + </div> + <article class="markdown"> + <?= $this->text->markdown($project['description']) ?> + </article> +<?php endif ?> diff --git a/app/Template/project_overview/files.php b/app/Template/project_overview/files.php new file mode 100644 index 00000000..605431ed --- /dev/null +++ b/app/Template/project_overview/files.php @@ -0,0 +1,98 @@ +<div class="page-header"> + <h2><?= t('Attachments') ?></h2> + <?php if ($this->user->hasProjectAccess('ProjectFile', 'create', $project['id'])): ?> + <ul> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Upload a file'), 'ProjectFile', 'create', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> + <?php endif ?> +</div> + +<?php if (empty($files) && empty($images)): ?> + <p class="alert"><?= t('There is no attachment at the moment.') ?></p> +<?php endif ?> + +<?php if (! empty($images)): ?> +<div class="file-thumbnails"> + <?php foreach ($images as $file): ?> + <div class="file-thumbnail"> + <a href="<?= $this->url->href('FileViewer', 'show', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?>" class="popover"><img src="<?= $this->url->href('FileViewer', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $project['id'])) ?>" title="<?= $this->text->e($file['name']) ?>" alt="<?= $this->text->e($file['name']) ?>"></a> + <div class="file-thumbnail-content"> + <div class="file-thumbnail-title"> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->text->e($file['name']) ?> <i class="fa fa-caret-down"></i></a> + <ul> + <li> + <i class="fa fa-download fa-fw"></i> + <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?> + </li> + <?php if ($this->user->hasProjectAccess('ProjectFile', 'remove', $project['id'])): ?> + <li> + <i class="fa fa-trash fa-fw"></i> + <?= $this->url->link(t('Remove'), 'ProjectFile', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + </ul> + </div> + </div> + <div class="file-thumbnail-description"> + <span class="tooltip" title='<?= t('Uploaded: %s', $this->dt->datetime($file['date'])).'<br>'.t('Size: %s', $this->text->bytes($file['size'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?= t('Uploaded by %s', $file['user_name'] ?: $file['username']) ?> + </div> + </div> + </div> + <?php endforeach ?> +</div> +<?php endif ?> + +<?php if (! empty($files)): ?> +<table class="table-stripped"> + <tr> + <th><?= t('Filename') ?></th> + <th><?= t('Creator') ?></th> + <th><?= t('Date') ?></th> + <th><?= t('Size') ?></th> + </tr> + <?php foreach ($files as $file): ?> + <tr> + <td> + <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->text->e($file['name']) ?> <i class="fa fa-caret-down"></i></a> + <ul> + <?php if ($this->file->getPreviewType($file['name']) !== null): ?> + <li> + <i class="fa fa-eye fa-fw"></i> + <?= $this->url->link(t('View file'), 'FileViewer', 'show', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-download fa-fw"></i> + <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?> + </li> + <?php if ($this->user->hasProjectAccess('ProjectFile', 'remove', $project['id'])): ?> + <li> + <i class="fa fa-trash fa-fw"></i> + <?= $this->url->link(t('Remove'), 'ProjectFile', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + </ul> + </div> + </td> + <td> + <?= $this->text->e($file['user_name'] ?: $file['username']) ?> + </td> + <td> + <?= $this->dt->date($file['date']) ?> + </td> + <td> + <?= $this->text->bytes($file['size']) ?> + </td> + </tr> + <?php endforeach ?> +</table> +<?php endif ?> diff --git a/app/Template/project_overview/information.php b/app/Template/project_overview/information.php new file mode 100644 index 00000000..95508d98 --- /dev/null +++ b/app/Template/project_overview/information.php @@ -0,0 +1,35 @@ +<div class="page-header"> + <h2><?= t('Information') ?></h2> +</div> +<div class="listing"> +<ul> + <?php if ($project['owner_id'] > 0): ?> + <li><?= t('Project owner: ') ?><strong><?= $this->text->e($project['owner_name'] ?: $project['owner_username']) ?></strong></li> + <?php endif ?> + + <?php if (! empty($users)): ?> + <?php foreach ($roles as $role => $role_name): ?> + <?php if (isset($users[$role])): ?> + <li> + <?= $role_name ?>: + <strong><?= implode(', ', $users[$role]) ?></strong> + </li> + <?php endif ?> + <?php endforeach ?> + <?php endif ?> + + <?php if ($project['start_date']): ?> + <li><?= t('Start date: ').$this->dt->date($project['start_date']) ?></li> + <?php endif ?> + + <?php if ($project['end_date']): ?> + <li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li> + <?php endif ?> + + <?php if ($project['is_public']): ?> + <li><i class="fa fa-share-alt"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-rss-square"></i> <?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li> + <?php endif ?> +</ul> +</div> diff --git a/app/Template/project_overview/show.php b/app/Template/project_overview/show.php new file mode 100644 index 00000000..0038d952 --- /dev/null +++ b/app/Template/project_overview/show.php @@ -0,0 +1,16 @@ +<section id="main"> + <?= $this->render('project_header/header', array( + 'project' => $project, + 'filters' => $filters, + )) ?> + + <?= $this->render('project_overview/columns', array('project' => $project)) ?> + <?= $this->render('project_overview/description', array('project' => $project)) ?> + <?= $this->render('project_overview/files', array('project' => $project, 'images' => $images, 'files' => $files)) ?> + <?= $this->render('project_overview/information', array('project' => $project, 'users' => $users, 'roles' => $roles)) ?> + + <div class="page-header"> + <h2><?= t('Last activity') ?></h2> + </div> + <?= $this->render('event/events', array('events' => $events)) ?> +</section> diff --git a/app/Template/project_permission/index.php b/app/Template/project_permission/index.php index 5f0edc2b..a7d666a6 100644 --- a/app/Template/project_permission/index.php +++ b/app/Template/project_permission/index.php @@ -19,7 +19,7 @@ </tr> <?php foreach ($users as $user): ?> <tr> - <td><?= $this->e($user['name'] ?: $user['username']) ?></td> + <td><?= $this->text->e($user['name'] ?: $user['username']) ?></td> <td> <?= $this->form->select( 'role-'.$user['id'], @@ -57,7 +57,7 @@ <?= $this->form->select('role', $roles, $values, $errors) ?> - <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Add') ?></button> </form> </div> <?php endif ?> @@ -79,7 +79,7 @@ </tr> <?php foreach ($groups as $group): ?> <tr> - <td><?= $this->e($group['name']) ?></td> + <td><?= $this->text->e($group['name']) ?></td> <td> <?= $this->form->select( 'role-'.$group['id'], @@ -119,7 +119,7 @@ <?= $this->form->select('role', $roles, $values, $errors) ?> - <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Add') ?></button> </form> </div> <?php endif ?> @@ -135,7 +135,7 @@ <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> </div> </form> <?php endif ?> diff --git a/app/Template/project_user/layout.php b/app/Template/project_user/layout.php index 3a569da4..1103e9bc 100644 --- a/app/Template/project_user/layout.php +++ b/app/Template/project_user/layout.php @@ -1,13 +1,6 @@ <section id="main"> <div class="page-header"> <ul> - <?php if ($this->user->hasAccess('project', 'create')): ?> - <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> - <?php endif ?> - <li> - <i class="fa fa-lock fa-fw"></i> - <?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?> - </li> <li> <i class="fa fa-folder fa-fw"></i> <?= $this->url->link(t('Projects list'), 'project', 'index') ?> @@ -22,11 +15,11 @@ </div> <section class="sidebar-container"> - <?= $this->render('project_user/sidebar', array('users' => $users, 'filter' => $filter)) ?> + <?= $this->render($sidebar_template, array('users' => $users, 'filter' => $filter)) ?> <div class="sidebar-content"> <div class="page-header"> - <h2><?= $this->e($title) ?></h2> + <h2><?= $this->text->e($title) ?></h2> </div> <?= $content_for_sublayout ?> </div> diff --git a/app/Template/project_user/roles.php b/app/Template/project_user/roles.php index 35d16241..17fb709b 100644 --- a/app/Template/project_user/roles.php +++ b/app/Template/project_user/roles.php @@ -10,19 +10,19 @@ <?php foreach ($paginator->getCollection() as $project): ?> <tr> <td> - <?= $this->e($this->user->getFullname($project)) ?> + <?= $this->text->e($this->user->getFullname($project)) ?> </td> <td> <?= $this->url->link('<i class="fa fa-th"></i>', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> <?= $this->url->link('<i class="fa fa-sliders fa-fw"></i>', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> <?= $this->url->link('<i class="fa fa-cog fa-fw"></i>', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Project settings')) ?> - <?= $this->e($project['project_name']) ?> + <?= $this->text->e($project['project_name']) ?> </td> <td class="dashboard-project-stats"> <?php foreach ($project['columns'] as $column): ?> <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong> - <span><?= $this->e($column['title']) ?></span> + <span><?= $this->text->e($column['title']) ?></span> <?php endforeach ?> </td> </tr> diff --git a/app/Template/project_user/sidebar.php b/app/Template/project_user/sidebar.php index 27f1094c..ff113ebb 100644 --- a/app/Template/project_user/sidebar.php +++ b/app/Template/project_user/sidebar.php @@ -10,7 +10,7 @@ 'chosen-select select-auto-redirect' ) ?> - <br/><br/> + <br><br> <ul> <li <?= $this->app->checkMenuSelection('projectuser', 'managers') ?>> <?= $this->url->link(t('Project managers'), 'projectuser', 'managers', $filter) ?> diff --git a/app/Template/project_user/tasks.php b/app/Template/project_user/tasks.php index f4fc2723..108d3b33 100644 --- a/app/Template/project_user/tasks.php +++ b/app/Template/project_user/tasks.php @@ -14,29 +14,29 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> - <?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?= $this->url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> </td> <td> - <?= $this->e($task['column_name']) ?> + <?= $this->text->e($task['column_name']) ?> </td> <td> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> <?php if ($task['assignee_username']): ?> - <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?> <?php else: ?> <?= t('Unassigned') ?> <?php endif ?> </td> <td> - <?= dt('%B %e, %Y', $task['date_started']) ?> + <?= $this->dt->date($task['date_started']) ?> </td> <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> + <?= $this->dt->date($task['date_due']) ?> </td> </tr> <?php endforeach ?> diff --git a/app/Template/project_user/tooltip_users.php b/app/Template/project_user/tooltip_users.php index 7a07caad..f75d964b 100644 --- a/app/Template/project_user/tooltip_users.php +++ b/app/Template/project_user/tooltip_users.php @@ -6,7 +6,7 @@ <strong><?= $role_name ?></strong> <ul> <?php foreach ($users[$role] as $user_id => $user): ?> - <li><?= $this->url->link($this->e($user), 'Projectuser', 'opens', array('user_id' => $user_id)) ?></li> + <li><?= $this->url->link($this->text->e($user), 'Projectuser', 'opens', array('user_id' => $user_id)) ?></li> <?php endforeach ?> </ul> <?php endif ?> diff --git a/app/Template/search/index.php b/app/Template/search/index.php index 329c072a..9231a6f3 100644 --- a/app/Template/search/index.php +++ b/app/Template/search/index.php @@ -8,14 +8,13 @@ </ul> </div> - <div class="search"> + <div class="filter-box"> <form method="get" action="<?= $this->url->dir() ?>" class="search"> <?= $this->form->hidden('controller', $values) ?> <?= $this->form->hidden('action', $values) ?> <?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?> + <?= $this->render('app/filters_helper') ?> </form> - - <?= $this->render('app/filters_helper') ?> </div> <?php if (empty($values['search'])): ?> diff --git a/app/Template/search/results.php b/app/Template/search/results.php index 88eed87c..79df3544 100644 --- a/app/Template/search/results.php +++ b/app/Template/search/results.php @@ -13,32 +13,32 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td> - <?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?= $this->url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> </td> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> - <?= $this->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> + <?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> </td> <td> - <?= $this->e($task['column_name']) ?> + <?= $this->text->e($task['column_name']) ?> </td> <td> - <?= $this->e($task['category_name']) ?> + <?= $this->text->e($task['category_name']) ?> </td> <td> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> </td> <td> <?php if ($task['assignee_username']): ?> - <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?> <?php else: ?> <?= t('Unassigned') ?> <?php endif ?> </td> <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> + <?= $this->dt->date($task['date_due']) ?> </td> <td> <?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?> diff --git a/app/Template/subtask/create.php b/app/Template/subtask/create.php index 82e378f5..029fddf5 100644 --- a/app/Template/subtask/create.php +++ b/app/Template/subtask/create.php @@ -2,26 +2,19 @@ <h2><?= t('Add a sub-task') ?></h2> </div> -<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> - <?= $this->form->hidden('task_id', $values) ?> - - <?= $this->form->label(t('Title'), 'title') ?> - <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/> - - <?= $this->form->label(t('Assignee'), 'user_id') ?> - <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/> - - <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> - <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + <?= $this->subtask->selectTitle($values, $errors, array('autofocus')) ?> + <?= $this->subtask->selectAssignee($users_list, $values, $errors) ?> + <?= $this->subtask->selectTimeEstimated($values, $errors) ?> <?= $this->form->checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/subtask/edit.php b/app/Template/subtask/edit.php index 2e583069..3c210f60 100644 --- a/app/Template/subtask/edit.php +++ b/app/Template/subtask/edit.php @@ -2,28 +2,19 @@ <h2><?= t('Edit a sub-task') ?></h2> </div> -<form method="post" action="<?= $this->url->href('subtask', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('subtask', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> - <?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('task_id', $values) ?> - - <?= $this->form->label(t('Title'), 'title') ?> - <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/> - - <?= $this->form->label(t('Assignee'), 'user_id') ?> - <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/> - - <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> - <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> - - <?= $this->form->label(t('Time spent'), 'time_spent') ?> - <?= $this->form->numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/> + <?= $this->subtask->selectTitle($values, $errors, array('autofocus')) ?> + <?= $this->subtask->selectAssignee($users_list, $values, $errors) ?> + <?= $this->subtask->selectTimeEstimated($values, $errors) ?> + <?= $this->subtask->selectTimeSpent($values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/subtask/icons.php b/app/Template/subtask/icons.php deleted file mode 100644 index 1f31d51f..00000000 --- a/app/Template/subtask/icons.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php if ($subtask['status'] == 0): ?> - <i class="fa fa-square-o fa-fw"></i> -<?php elseif ($subtask['status'] == 1): ?> - <i class="fa fa-gears fa-fw"></i> -<?php else: ?> - <i class="fa fa-check-square-o fa-fw"></i> -<?php endif ?>
\ No newline at end of file diff --git a/app/Template/subtask/menu.php b/app/Template/subtask/menu.php new file mode 100644 index 00000000..6c98b951 --- /dev/null +++ b/app/Template/subtask/menu.php @@ -0,0 +1,11 @@ +<div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li> + <?= $this->url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?> + </li> + <li> + <?= $this->url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?> + </li> + </ul> +</div> diff --git a/app/Template/subtask/remove.php b/app/Template/subtask/remove.php index 65ade31d..374256fd 100644 --- a/app/Template/subtask/remove.php +++ b/app/Template/subtask/remove.php @@ -7,11 +7,11 @@ <?= t('Do you really want to remove this sub-task?') ?> </p> - <p><strong><?= $this->e($subtask['title']) ?></strong></p> + <p><strong><?= $this->text->e($subtask['title']) ?></strong></p> <div class="form-actions"> <?= $this->url->link(t('Yes'), 'subtask', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php index 6945840f..2eda357b 100644 --- a/app/Template/subtask/show.php +++ b/app/Template/subtask/show.php @@ -1,107 +1,25 @@ -<div id="subtasks" class="task-show-section"> +<?php if (isset($show_title)): ?> +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> +<?php endif ?> - <?php if (! empty($subtasks)): ?> - <div class="page-header"> - <h2><?= t('Sub-Tasks') ?></h2> - </div> +<div class="page-header"> + <h2><?= t('Sub-Tasks') ?></h2> +</div> - <?php $first_position = $subtasks[0]['position']; ?> - <?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?> - <table class="subtasks-table"> - <tr> - <th class="column-40"><?= t('Title') ?></th> - <th><?= t('Assignee') ?></th> - <th><?= t('Time tracking') ?></th> - <?php if ($editable): ?> - <th class="column-5"></th> - <?php endif ?> - </tr> - <?php foreach ($subtasks as $subtask): ?> - <tr> - <td> - <?php if ($editable): ?> - <?= $this->subtask->toggleStatus($subtask, 'task') ?> - <?php else: ?> - <?= $this->render('subtask/icons', array('subtask' => $subtask)) . $this->e($subtask['title']) ?> - <?php endif ?> - </td> - <td> - <?php if (! empty($subtask['username'])): ?> - <?php if ($editable): ?> - <?= $this->url->link($this->e($subtask['name'] ?: $subtask['username']), 'user', 'show', array('user_id' => $subtask['user_id'])) ?> - <?php else: ?> - <?= $this->e($subtask['name'] ?: $subtask['username']) ?> - <?php endif ?> - <?php endif ?> - </td> - <td> - <ul class="no-bullet"> - <li> - <?php if (! empty($subtask['time_spent'])): ?> - <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> - <?php endif ?> +<div id="subtasks"> - <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> - <?php endif ?> - </li> - <?php if ($editable && $subtask['user_id'] == $this->user->getId()): ?> - <li> - <?php if ($subtask['is_timer_started']): ?> - <i class="fa fa-pause"></i> - <?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?> - (<?= $this->dt->age($subtask['timer_start_date']) ?>) - <?php else: ?> - <i class="fa fa-play-circle-o"></i> - <?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?> - <?php endif ?> - </li> - <?php endif ?> - </ul> - </td> - <?php if ($editable): ?> - <td> - <div class="dropdown"> - <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> - <ul> - <?php if ($subtask['position'] != $first_position): ?> - <li> - <?= $this->url->link(t('Move Up'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'up'), true) ?> - </li> - <?php endif ?> - <?php if ($subtask['position'] != $last_position): ?> - <li> - <?= $this->url->link(t('Move Down'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'down'), true) ?> - </li> - <?php endif ?> - <li> - <?= $this->url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?> - </li> - <li> - <?= $this->url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?> - </li> - </ul> - </div> - </td> - <?php endif ?> - </tr> - <?php endforeach ?> - </table> - <?php endif ?> + <?= $this->render('subtask/table', array('subtasks' => $subtasks, 'task' => $task, 'editable' => $editable)) ?> <?php if ($editable && $this->user->hasProjectAccess('subtask', 'save', $task['project_id'])): ?> - <?php if (empty($subtasks)): ?> - <div class="page-header"> - <h2><?= t('Sub-Tasks') ?></h2> - </div> - <?php endif ?> <form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> <?= $this->form->text('title', array(), array(), array('required', 'placeholder="'.t('Type here to create a new sub-task').'"')) ?> <?= $this->form->numeric('time_estimated', array(), array(), array('placeholder="'.t('Original estimate').'"')) ?> <?= $this->form->select('user_id', $users_list, array(), array(), array('placeholder="'.t('Assignee').'"')) ?> - <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Add') ?></button> </form> <?php endif ?> diff --git a/app/Template/subtask/table.php b/app/Template/subtask/table.php new file mode 100644 index 00000000..0af02dcf --- /dev/null +++ b/app/Template/subtask/table.php @@ -0,0 +1,71 @@ +<?php if (! empty($subtasks)): ?> + <table + class="subtasks-table table-stripped" + data-save-position-url="<?= $this->url->href('Subtask', 'movePosition', array('project_id' => $task['project_id'], 'task_id' => $task['id'])) ?>" + > + <thead> + <tr> + <th class="column-40"><?= t('Title') ?></th> + <th><?= t('Assignee') ?></th> + <th><?= t('Time tracking') ?></th> + <?php if ($editable): ?> + <th class="column-5"></th> + <?php endif ?> + </tr> + </thead> + <tbody> + <?php foreach ($subtasks as $subtask): ?> + <tr data-subtask-id="<?= $subtask['id'] ?>"> + <td> + <?php if ($editable): ?> + <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change subtask position') ?>"></i> + <?= $this->subtask->toggleStatus($subtask, $task['project_id'], true) ?> + <?php else: ?> + <?= $this->subtask->getTitle($subtask) ?> + <?php endif ?> + </td> + <td> + <?php if (! empty($subtask['username'])): ?> + <?= $this->text->e($subtask['name'] ?: $subtask['username']) ?> + <?php endif ?> + </td> + <td> + <ul class="no-bullet"> + <li> + <?php if (! empty($subtask['time_spent'])): ?> + <strong><?= $this->text->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= $this->text->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </li> + <?php if ($editable && $subtask['user_id'] == $this->user->getId()): ?> + <li> + <?php if ($subtask['is_timer_started']): ?> + <i class="fa fa-pause"></i> + <?= $this->url->link(t('Stop timer'), 'SubtaskStatus', 'timer', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?> + (<?= $this->dt->age($subtask['timer_start_date']) ?>) + <?php else: ?> + <i class="fa fa-play-circle-o"></i> + <?= $this->url->link(t('Start timer'), 'SubtaskStatus', 'timer', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?> + <?php endif ?> + </li> + <?php endif ?> + </ul> + </td> + <?php if ($editable): ?> + <td> + <?= $this->render('subtask/menu', array( + 'task' => $task, + 'subtask' => $subtask, + )) ?> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php else: ?> + <p class="alert"><?= t('There is no subtask at the moment.') ?></p> +<?php endif ?> diff --git a/app/Template/subtask/restriction_change_status.php b/app/Template/subtask_restriction/popover.php index 88e91d82..916a664e 100644 --- a/app/Template/subtask/restriction_change_status.php +++ b/app/Template/subtask_restriction/popover.php @@ -1,18 +1,16 @@ <div class="page-header"> <h2><?= t('You already have one subtask in progress') ?></h2> </div> - - <form action="<?= $this->url->href('subtask', 'changeRestrictionStatus', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post"> +<form class="popover-form" action="<?= $this->url->href('SubtaskRestriction', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post"> <?= $this->form->csrf() ?> - <?= $this->form->hidden('redirect', array('redirect' => $redirect)) ?> <p><?= t('Select the new status of the subtask: "%s"', $subtask_inprogress['title']) ?></p> <?= $this->form->radios('status', $status_list) ?> <?= $this->form->hidden('id', $subtask_inprogress) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-red"/> + <button type="submit" class="btn btn-red"><?= t('Save') ?></button> <?= t('or') ?> <a href="#" class="close-popover"><?= t('cancel') ?></a> </div> diff --git a/app/Template/swimlane/create.php b/app/Template/swimlane/create.php new file mode 100644 index 00000000..aa92a930 --- /dev/null +++ b/app/Template/swimlane/create.php @@ -0,0 +1,37 @@ +<div class="page-header"> + <h2><?= t('Add a new swimlane') ?></h2> +</div> +<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</form> diff --git a/app/Template/swimlane/edit.php b/app/Template/swimlane/edit.php index dfc5cf0b..232570e7 100644 --- a/app/Template/swimlane/edit.php +++ b/app/Template/swimlane/edit.php @@ -2,7 +2,7 @@ <h2><?= t('Swimlane modification for the project "%s"', $project['name']) ?></h2> </div> -<form method="post" action="<?= $this->url->href('swimlane', 'update', array('project_id' => $project['id'], 'swimlane_id' => $values['id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'update', array('project_id' => $project['id'], 'swimlane_id' => $values['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -34,8 +34,8 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/swimlane/edit_default.php b/app/Template/swimlane/edit_default.php new file mode 100644 index 00000000..3bf82568 --- /dev/null +++ b/app/Template/swimlane/edit_default.php @@ -0,0 +1,18 @@ +<div class="page-header"> + <h2><?= t('Change default swimlane') ?></h2> +</div> +<form class="popover-form" method="post" action="<?= $this->url->href('swimlane', 'updateDefault', array('project_id' => $project['id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Name'), 'default_swimlane') ?> + <?= $this->form->text('default_swimlane', $values, $errors, array('required', 'maxlength="50"')) ?> + + <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, $values['show_default_swimlane'] == 1) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</form> diff --git a/app/Template/swimlane/index.php b/app/Template/swimlane/index.php index 9502cffd..fad35306 100644 --- a/app/Template/swimlane/index.php +++ b/app/Template/swimlane/index.php @@ -1,71 +1,28 @@ <div class="page-header"> - <h2><?= t('Change default swimlane') ?></h2> + <h2><?= t('Swimlanes') ?></h2> + <ul> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add a new swimlane'), 'Swimlane', 'create', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> </div> -<form method="post" action="<?= $this->url->href('swimlane', 'change', array('project_id' => $project['id'])) ?>" autocomplete="off"> - <?= $this->form->csrf() ?> - <?= $this->form->hidden('id', $default_swimlane) ?> - - <?= $this->form->label(t('Rename'), 'default_swimlane') ?> - <?= $this->form->text('default_swimlane', $default_swimlane, array(), array('required', 'maxlength="50"')) ?><br/> - - <?php if (! empty($active_swimlanes) || $default_swimlane['show_default_swimlane'] == 0): ?> - <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, $default_swimlane['show_default_swimlane'] == 1) ?> - <?php else: ?> - <?= $this->form->hidden('show_default_swimlane', $default_swimlane) ?> - <?php endif ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> - -<?php if (! empty($active_swimlanes)): ?> -<div class="page-header"> - <h2><?= t('Active swimlanes') ?></h2> -</div> -<?= $this->render('swimlane/table', array('swimlanes' => $active_swimlanes, 'project' => $project)) ?> +<?php if (! empty($active_swimlanes) || $default_swimlane['show_default_swimlane'] == 1): ?> +<h3><?= t('Active swimlanes') ?></h3> + <?= $this->render('swimlane/table', array( + 'swimlanes' => $active_swimlanes, + 'project' => $project, + 'default_swimlane' => $default_swimlane['show_default_swimlane'] == 1 ? $default_swimlane : array() + )) ?> <?php endif ?> -<?php if (! empty($inactive_swimlanes)): ?> -<div class="page-header"> - <h2><?= t('Inactive swimlanes') ?></h2> -</div> -<?= $this->render('swimlane/table', array('swimlanes' => $inactive_swimlanes, 'project' => $project, 'hide_position' => true)) ?> +<?php if (! empty($inactive_swimlanes) || $default_swimlane['show_default_swimlane'] == 0): ?> + <h3><?= t('Inactive swimlanes') ?></h3> + <?= $this->render('swimlane/table', array( + 'swimlanes' => $inactive_swimlanes, + 'project' => $project, + 'default_swimlane' => $default_swimlane['show_default_swimlane'] == 0 ? $default_swimlane : array(), + 'disable_handler' => true + )) ?> <?php endif ?> - -<div class="page-header"> - <h2><?= t('Add a new swimlane') ?></h2> -</div> -<form method="post" action="<?= $this->url->href('swimlane', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> - - <?= $this->form->csrf() ?> - <?= $this->form->hidden('project_id', $values) ?> - - <?= $this->form->label(t('Name'), 'name') ?> - <?= $this->form->text('name', $values, $errors, array('required', 'maxlength="50"')) ?> - - <?= $this->form->label(t('Description'), 'description') ?> - - <div class="form-tabs"> - <div class="write-area"> - <?= $this->form->textarea('description', $values, $errors) ?> - </div> - <div class="preview-area"> - <div class="markdown"></div> - </div> - <ul class="form-tabs-nav"> - <li class="form-tab form-tab-selected"> - <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> - </li> - <li class="form-tab"> - <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> - </li> - </ul> - </div> - <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> diff --git a/app/Template/swimlane/remove.php b/app/Template/swimlane/remove.php index 1d7c2b7a..9be39ff8 100644 --- a/app/Template/swimlane/remove.php +++ b/app/Template/swimlane/remove.php @@ -11,7 +11,7 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'swimlane', 'remove', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </div> </section>
\ No newline at end of file diff --git a/app/Template/swimlane/table.php b/app/Template/swimlane/table.php index 89915d56..17be6924 100644 --- a/app/Template/swimlane/table.php +++ b/app/Template/swimlane/table.php @@ -1,47 +1,76 @@ -<table> - <tr> - <?php if (! isset($hide_position)): ?> - <th class="column-10"><?= t('Position') ?></th> - <?php endif ?> - <th><?= t('Name') ?></th> - <th class="column-8"><?= t('Actions') ?></th> - </tr> - <?php foreach ($swimlanes as $swimlane): ?> - <tr> - <?php if (! isset($hide_position)): ?> - <td>#<?= $swimlane['position'] ?></td> - <?php endif ?> - <td><?= $this->e($swimlane['name']) ?></td> - <td> - <div class="dropdown"> - <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> - <ul> - <?php if ($swimlane['position'] != 0 && $swimlane['position'] != 1): ?> +<table + class="swimlanes-table table-stripped" + data-save-position-url="<?= $this->url->href('Swimlane', 'move', array('project_id' => $project['id'])) ?>"> + <thead> + <tr> + <th><?= t('Name') ?></th> + <th class="column-8"><?= t('Actions') ?></th> + </tr> + + <?php if (! empty($default_swimlane)): ?> + <tr> + <td> + <?= $this->text->e($default_swimlane['default_swimlane']) ?> + <?php if ($default_swimlane['default_swimlane'] !== t('Default swimlane')): ?> + (<?= t('Default swimlane') ?>) + <?php endif ?> + </td> + <td> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> <li> - <?= $this->url->link(t('Move Up'), 'swimlane', 'moveup', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?= $this->url->link(t('Edit'), 'Swimlane', 'editDefault', array('project_id' => $project['id']), false, 'popover') ?> </li> - <?php endif ?> - <?php if ($swimlane['position'] != 0 && $swimlane['position'] != count($swimlanes)): ?> <li> - <?= $this->url->link(t('Move Down'), 'swimlane', 'movedown', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?php if ($default_swimlane['show_default_swimlane'] == 1): ?> + <?= $this->url->link(t('Disable'), 'Swimlane', 'disableDefault', array('project_id' => $project['id']), true) ?> + <?php else: ?> + <?= $this->url->link(t('Enable'), 'Swimlane', 'enableDefault', array('project_id' => $project['id']), true) ?> + <?php endif ?> </li> + </ul> + </td> + </tr> + <?php endif ?> + </thead> + <tbody> + <?php foreach ($swimlanes as $swimlane): ?> + <tr data-swimlane-id="<?= $swimlane['id'] ?>"> + <td> + <?php if (! isset($disable_handler)): ?> + <i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Change column position') ?>"></i> + <?php endif ?> + + <?= $this->text->e($swimlane['name']) ?> + + <?php if (! empty($swimlane['description'])): ?> + <span class="tooltip" title='<?= $this->text->e($this->text->markdown($swimlane['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> <?php endif ?> - <li> - <?= $this->url->link(t('Edit'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?> - </li> - <li> - <?php if ($swimlane['is_active']): ?> - <?= $this->url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> - <?php else: ?> - <?= $this->url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> - <?php endif ?> - </li> - <li> - <?= $this->url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?> - </li> - </ul> - </div> - </td> - </tr> - <?php endforeach ?> -</table>
\ No newline at end of file + </td> + <td> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li> + <?= $this->url->link(t('Edit'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> + </li> + <li> + <?php if ($swimlane['is_active']): ?> + <?= $this->url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?php else: ?> + <?= $this->url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?php endif ?> + </li> + <li> + <?= $this->url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> + </li> + </ul> + </div> + </td> + </tr> + <?php endforeach ?> + </tbody> +</table> diff --git a/app/Template/task/analytics.php b/app/Template/task/analytics.php index 306dd021..54eac2b7 100644 --- a/app/Template/task/analytics.php +++ b/app/Template/task/analytics.php @@ -1,3 +1,6 @@ +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> <div class="page-header"> <h2><?= t('Analytics') ?></h2> </div> @@ -18,7 +21,7 @@ </tr> <?php foreach ($time_spent_columns as $column): ?> <tr> - <td><?= $this->e($column['title']) ?></td> + <td><?= $this->text->e($column['title']) ?></td> <td><?= $this->dt->duration($column['time_spent']) ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/task/changes.php b/app/Template/task/changes.php index f288a8f4..779bdfa9 100644 --- a/app/Template/task/changes.php +++ b/app/Template/task/changes.php @@ -31,7 +31,7 @@ if (empty($task['date_due'])) { echo '<li>'.t('The due date have been removed').'</li>'; } else { - echo '<li>'.dt('New due date: %B %e, %Y', $task['date_due']).'</li>'; + echo '<li>'.t('New due date: ').$this->dt->date($task['date_due']).'</li>'; } break; case 'description': @@ -56,7 +56,7 @@ break; case 'date_started': if ($value != 0) { - echo '<li>'.dt('Start date changed: %B %e, %Y', $task['date_started']).'</li>'; + echo '<li>'.t('Start date changed: ').$this->dt->datetime($task['date_started']).'</li>'; } break; default: @@ -68,7 +68,7 @@ </ul> <?php if (! empty($changes['description'])): ?> - <p><?= t('The description have been modified') ?></p> + <p><?= t('The description has been modified') ?></p> <div class="markdown"><?= $this->text->markdown($task['description']) ?></div> <?php endif ?> <?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/color_picker.php b/app/Template/task/color_picker.php index a849b9ce..0c62fa70 100644 --- a/app/Template/task/color_picker.php +++ b/app/Template/task/color_picker.php @@ -3,7 +3,7 @@ <div data-color-id="<?= $color_id ?>" class="color-square color-<?= $color_id ?> <?= isset($values['color_id']) && $values['color_id'] === $color_id ? 'color-square-selected' : '' ?>" - title="<?= $this->e($color_name) ?>"> + title="<?= $this->text->e($color_name) ?>"> </div> <?php endforeach ?> </div> diff --git a/app/Template/task/comments.php b/app/Template/task/comments.php index 57fb305f..c22e39ec 100644 --- a/app/Template/task/comments.php +++ b/app/Template/task/comments.php @@ -28,7 +28,7 @@ 'task_id' => $task['id'], ), 'errors' => array(), - 'task' => $task + 'task' => $task, )) ?> <?php endif ?> </div> diff --git a/app/Template/task/details.php b/app/Template/task/details.php index d885ca9c..61f6c848 100644 --- a/app/Template/task/details.php +++ b/app/Template/task/details.php @@ -1,104 +1,130 @@ -<div class="color-<?= $task['color_id'] ?> task-show-details"> - <h2><?= $this->e('#'.$task['id'].' '.$task['title']) ?></h2> - <?php if ($task['score']): ?> - <span class="task-score"><?= $this->e($task['score']) ?></span> - <?php endif ?> - <ul> - <li> - <strong><?= t('Priority: %d', $task['priority']) ?></strong> - </li> - <?php if ($task['reference']): ?> - <li> - <strong><?= t('Reference: %s', $task['reference']) ?></strong> - </li> - <?php endif ?> - <?php if (! empty($task['swimlane_name'])): ?> - <li> - <?= t('Swimlane: %s', $task['swimlane_name']) ?> - </li> - <?php endif ?> - <li> - <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?> - </li> - <?php if ($task['date_modification']): ?> - <li> - <?= dt('Last modified on %B %e, %Y at %k:%M %p', $task['date_modification']) ?> - </li> - <?php endif ?> - <?php if ($task['date_completed']): ?> - <li> - <?= dt('Completed on %B %e, %Y at %k:%M %p', $task['date_completed']) ?> - </li> - <?php endif ?> - <?php if ($task['date_started']): ?> - <li> - <?= dt('Started on %B %e, %Y', $task['date_started']) ?> - </li> - <?php endif ?> - <?php if ($task['date_due']): ?> - <li> - <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong> - </li> - <?php endif ?> - <?php if ($task['time_estimated']): ?> - <li> - <?= t('Estimated time: %s hours', $task['time_estimated']) ?> - </li> - <?php endif ?> - <?php if ($task['time_spent']): ?> - <li> - <?= t('Time spent: %s hours', $task['time_spent']) ?> - </li> - <?php endif ?> - <?php if ($task['creator_username']): ?> - <li> - <?= t('Created by %s', $task['creator_name'] ?: $task['creator_username']) ?> - </li> - <?php endif ?> - <li> - <strong> - <?php if ($task['assignee_username']): ?> - <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?> - <?php else: ?> - <?= t('There is nobody assigned') ?> - <?php endif ?> - </strong> - </li> - <li> - <?= t('Column on the board:') ?> - <strong><?= $this->e($task['column_title']) ?></strong> - (<?= $this->e($task['project_name']) ?>) - <?= dt('since %B %e, %Y at %k:%M %p', $task['date_moved']) ?> - </li> - <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> - <?php if ($task['category_name']): ?> - <li> - <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong> - </li> - <?php endif ?> - <li> - <?php if ($task['is_active'] == 1): ?> - <?= t('Status is open') ?> - <?php else: ?> - <?= t('Status is closed') ?> - <?php endif ?> - </li> - <?php if ($project['is_public']): ?> - <li> - <?= $this->url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> - </li> - <?php endif ?> - - <?php if ($editable && $task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_NONE): ?> - <li> - <strong><?= t('Recurring information') ?></strong> - <?= $this->render('task/recurring_info', array( - 'task' => $task, - 'recurrence_trigger_list' => $recurrence_trigger_list, - 'recurrence_timeframe_list' => $recurrence_timeframe_list, - 'recurrence_basedate_list' => $recurrence_basedate_list, - )) ?> - </li> - <?php endif ?> - </ul> -</div> +<section id="task-summary"> + <h2><?= $this->text->e($task['title']) ?></h2> + <div class="task-summary-container color-<?= $task['color_id'] ?>"> + <div class="task-summary-column"> + <ul class="no-bullet"> + <li> + <strong><?= t('Status:') ?></strong> + <span> + <?php if ($task['is_active'] == 1): ?> + <?= t('open') ?> + <?php else: ?> + <?= t('closed') ?> + <?php endif ?> + </span> + </li> + <li> + <strong><?= t('Priority:') ?></strong> <span><?= $task['priority'] ?></span> + </li> + <?php if (! empty($task['reference'])): ?> + <li> + <strong><?= t('Reference:') ?></strong> <span><?= $this->text->e($task['reference']) ?></span> + </li> + <?php endif ?> + <?php if (! empty($task['score'])): ?> + <li> + <strong><?= t('Complexity:') ?></strong> <span><?= $this->text->e($task['score']) ?></span> + </li> + <?php endif ?> + <?php if ($project['is_public']): ?> + <li class="smaller"> + <i class="fa fa-external-link fa-fw"></i> + <?= $this->url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> + </li> + <?php endif ?> + </ul> + </div> + <div class="task-summary-column"> + <ul class="no-bullet"> + <?php if (! empty($task['category_name'])): ?> + <li> + <strong><?= t('Category:') ?></strong> + <span><?= $this->text->e($task['category_name']) ?></span> + </li> + <?php endif ?> + <?php if (! empty($task['swimlane_name'])): ?> + <li> + <strong><?= t('Swimlane:') ?></strong> + <span><?= $this->text->e($task['swimlane_name']) ?></span> + </li> + <?php endif ?> + <li> + <strong><?= t('Column:') ?></strong> + <span><?= $this->text->e($task['column_title']) ?></span> + </li> + <li> + <strong><?= t('Position:') ?></strong> + <span><?= $task['position'] ?></span> + </li> + </ul> + </div> + <div class="task-summary-column"> + <ul class="no-bullet"> + <li> + <strong><?= t('Assignee:') ?></strong> + <span> + <?php if ($task['assignee_username']): ?> + <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?php else: ?> + <?= t('not assigned') ?> + <?php endif ?> + </span> + </li> + <?php if ($task['creator_username']): ?> + <li> + <strong><?= t('Creator:') ?></strong> + <span><?= $this->text->e($task['creator_name'] ?: $task['creator_username']) ?></span> + </li> + <?php endif ?> + <?php if ($task['date_due']): ?> + <li> + <strong><?= t('Due date:') ?></strong> + <span><?= $this->dt->date($task['date_due']) ?></span> + </li> + <?php endif ?> + <?php if ($task['time_estimated']): ?> + <li> + <strong><?= t('Time estimated:') ?></strong> + <span><?= t('%s hours', $task['time_estimated']) ?></span> + </li> + <?php endif ?> + <?php if ($task['time_spent']): ?> + <li> + <strong><?= t('Time spent:') ?></strong> + <span><?= t('%s hours', $task['time_spent']) ?></span> + </li> + <?php endif ?> + </ul> + </div> + <div class="task-summary-column"> + <ul class="no-bullet"> + <li> + <strong><?= t('Created:') ?></strong> + <span><?= $this->dt->datetime($task['date_creation']) ?></span> + </li> + <li> + <strong><?= t('Modified:') ?></strong> + <span><?= $this->dt->datetime($task['date_modification']) ?></span> + </li> + <?php if ($task['date_completed']): ?> + <li> + <strong><?= t('Completed:') ?></strong> + <span><?= $this->dt->datetime($task['date_completed']) ?></span> + </li> + <?php endif ?> + <?php if ($task['date_started']): ?> + <li> + <strong><?= t('Started:') ?></strong> + <span><?= $this->dt->datetime($task['date_started']) ?></span> + </li> + <?php endif ?> + <?php if ($task['date_moved']): ?> + <li> + <strong><?= t('Moved:') ?></strong> + <span><?= $this->dt->datetime($task['date_moved']) ?></span> + </li> + <?php endif ?> + </ul> + </div> + </div> +</section> diff --git a/app/Template/task/dropdown.php b/app/Template/task/dropdown.php new file mode 100644 index 00000000..3300ccf0 --- /dev/null +++ b/app/Template/task/dropdown.php @@ -0,0 +1,60 @@ +<div class="dropdown"> + <a href="#" class="dropdown-menu">#<?= $task['id'] ?></a> + <ul> + <?php if (isset($task['date_started']) && empty($task['date_started'])): ?> + <li> + <i class="fa fa-play fa-fw"></i> + <?= $this->url->link(t('Set automatically the start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-pencil-square-o fa-fw"></i> + <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-align-left fa-fw"></i> + <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-code-fork fa-fw"></i> + <?= $this->url->link(t('Add internal link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-external-link fa-fw"></i> + <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-comment-o fa-fw"></i> + <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-files-o fa-fw"></i> + <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-clipboard fa-fw"></i> + <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-clone fa-fw"></i> + <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <?php if (isset($task['is_active'])): ?> + <li> + <?php if ($task['is_active'] == 1): ?> + <i class="fa fa-times fa-fw"></i> + <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + <?php else: ?> + <i class="fa fa-check-square-o fa-fw"></i> + <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + <?php endif ?> + </li> + <?php endif ?> + + <?= $this->hook->render('template:task:dropdown') ?> + </ul> +</div> diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php index 9fe1a716..9cbbfec9 100644 --- a/app/Template/task/layout.php +++ b/app/Template/task/layout.php @@ -2,6 +2,9 @@ <div class="page-header"> <ul> <li> + <?= $this->render('task/menu', array('task' => $task)) ?> + </li> + <li> <i class="fa fa-th fa-fw"></i> <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?> </li> @@ -17,12 +20,12 @@ <?php endif ?> </ul> </div> - <section class="sidebar-container" id="task-section"> + <section class="sidebar-container"> - <?= $this->render('task/sidebar', array('task' => $task)) ?> + <?= $this->render($sidebar_template, array('task' => $task)) ?> <div class="sidebar-content"> - <?= $task_content_for_layout ?> + <?= $content_for_sublayout ?> </div> </section> </section>
\ No newline at end of file diff --git a/app/Template/task/menu.php b/app/Template/task/menu.php new file mode 100644 index 00000000..cddd930a --- /dev/null +++ b/app/Template/task/menu.php @@ -0,0 +1,78 @@ +<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> +<div class="dropdown"> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?php if (empty($task['date_started'])): ?> + <li> + <i class="fa fa-play fa-fw"></i> + <?= $this->url->link(t('Set automatically the start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-pencil-square-o fa-fw"></i> + <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-align-left fa-fw"></i> + <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-refresh fa-rotate-90 fa-fw"></i> + <?= $this->url->link(t('Edit recurrence'), 'TaskRecurrence', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-code-fork fa-fw"></i> + <?= $this->url->link(t('Add internal link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-external-link fa-fw"></i> + <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-comment-o fa-fw"></i> + <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-file fa-fw"></i> + <?= $this->url->link(t('Attach a document'), 'TaskFile', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-camera fa-fw"></i> + <?= $this->url->link(t('Add a screenshot'), 'TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-files-o fa-fw"></i> + <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-clipboard fa-fw"></i> + <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-clone fa-fw"></i> + <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <li> + <?php if ($task['is_active'] == 1): ?> + <i class="fa fa-times fa-fw"></i> + <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + <?php else: ?> + <i class="fa fa-check-square-o fa-fw"></i> + <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + <?php endif ?> + </li> + <?php if ($this->task->canRemove($task)): ?> + <li> + <i class="fa fa-trash-o fa-fw"></i> + <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> + </li> + <?php endif ?> + + <?= $this->hook->render('template:task:menu') ?> + </ul> +</div> +<?php endif ?> diff --git a/app/Template/task/public.php b/app/Template/task/public.php index 7edf097c..0052214a 100644 --- a/app/Template/task/public.php +++ b/app/Template/task/public.php @@ -10,6 +10,12 @@ 'is_public' => true, )) ?> + <?= $this->render('subtask/show', array( + 'task' => $task, + 'subtasks' => $subtasks, + 'editable' => false + )) ?> + <?= $this->render('tasklink/show', array( 'task' => $task, 'links' => $links, @@ -18,12 +24,6 @@ 'is_public' => true, )) ?> - <?= $this->render('subtask/show', array( - 'task' => $task, - 'subtasks' => $subtasks, - 'editable' => false - )) ?> - <?= $this->render('task/comments', array( 'task' => $task, 'comments' => $comments, diff --git a/app/Template/task/remove.php b/app/Template/task/remove.php index 2f6edc22..eb0809b1 100644 --- a/app/Template/task/remove.php +++ b/app/Template/task/remove.php @@ -4,12 +4,12 @@ <div class="confirm"> <p class="alert alert-info"> - <?= t('Do you really want to remove this task: "%s"?', $this->e($task['title'])) ?> + <?= t('Do you really want to remove this task: "%s"?', $this->text->e($task['title'])) ?> </p> <div class="form-actions"> <?= $this->url->link(t('Yes'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/task/show.php b/app/Template/task/show.php index f6d47e53..5dc27ca1 100644 --- a/app/Template/task/show.php +++ b/app/Template/task/show.php @@ -1,45 +1,55 @@ +<?= $this->hook->render('template:task:show:top', array('task' => $task, 'project' => $project)) ?> + <?= $this->render('task/details', array( 'task' => $task, 'project' => $project, - 'recurrence_trigger_list' => $this->task->recurrenceTriggers(), - 'recurrence_timeframe_list' => $this->task->recurrenceTimeframes(), - 'recurrence_basedate_list' => $this->task->recurrenceBasedates(), 'editable' => $this->user->hasProjectAccess('taskmodification', 'edit', $project['id']), )) ?> -<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $project['id'])): ?> - <?= $this->render('task_modification/edit_time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?> -<?php endif ?> +<?= $this->hook->render('template:task:show:before-description', array('task' => $task, 'project' => $project)) ?> <?= $this->render('task/description', array('task' => $task)) ?> -<?= $this->render('tasklink/show', array( - 'task' => $task, - 'links' => $links, - 'link_label_list' => $link_label_list, - 'editable' => $this->user->hasProjectAccess('tasklink', 'edit', $project['id']), - 'is_public' => false, -)) ?> +<?= $this->hook->render('template:task:show:before-subtasks', array('task' => $task, 'project' => $project)) ?> <?= $this->render('subtask/show', array( 'task' => $task, 'subtasks' => $subtasks, 'project' => $project, 'users_list' => isset($users_list) ? $users_list : array(), - 'editable' => $this->user->hasProjectAccess('subtask', 'edit', $project['id']), + 'editable' => true, +)) ?> + +<?= $this->hook->render('template:task:show:before-tasklinks', array('task' => $task, 'project' => $project)) ?> + +<?= $this->render('tasklink/show', array( + 'task' => $task, + 'links' => $links, + 'project' => $project, + 'link_label_list' => $link_label_list, + 'editable' => true, + 'is_public' => false, )) ?> +<?= $this->hook->render('template:task:show:before-timetracking', array('task' => $task, 'project' => $project)) ?> + <?= $this->render('task/time_tracking_summary', array('task' => $task)) ?> -<?= $this->render('file/show', array( +<?= $this->hook->render('template:task:show:before-attachements', array('task' => $task, 'project' => $project)) ?> + +<?= $this->render('task_file/show', array( 'task' => $task, 'files' => $files, 'images' => $images )) ?> +<?= $this->hook->render('template:task:show:before-comments', array('task' => $task, 'project' => $project)) ?> + <?= $this->render('task/comments', array( 'task' => $task, 'comments' => $comments, 'project' => $project, 'editable' => $this->user->hasProjectAccess('comment', 'edit', $project['id']), )) ?> + +<?= $this->hook->render('template:task:show:bottom', array('task' => $task, 'project' => $project)) ?> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index f522c1c4..951c5095 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -1,5 +1,5 @@ <div class="sidebar"> - <h2><?= t('Information') ?></h2> + <h2><?= t('Task #%d', $task['id']) ?></h2> <ul> <li <?= $this->app->checkMenuSelection('task', 'show') ?>> <?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> @@ -18,61 +18,16 @@ <?= $this->url->link(t('Time tracking'), 'task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <?php endif ?> - - <?= $this->hook->render('template:task:sidebar:information') ?> - </ul> - <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> - <h2><?= t('Actions') ?></h2> - <ul> - <li <?= $this->app->checkMenuSelection('taskmodification', 'edit') ?>> - <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskmodification', 'description') ?>> - <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskmodification', 'recurrence') ?>> - <?= $this->url->link(t('Edit recurrence'), 'taskmodification', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('subtask', 'create') ?>> - <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('tasklink', 'create') ?>> - <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <li <?= $this->app->checkMenuSelection('subtask', 'show') ?>> + <?= $this->url->link(t('Sub-tasks'), 'subtask', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> - <li <?= $this->app->checkMenuSelection('comment', 'create') ?>> - <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <li <?= $this->app->checkMenuSelection('tasklink', 'show') ?>> + <?= $this->url->link(t('Internal links'), 'tasklink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> - <li <?= $this->app->checkMenuSelection('file', 'create') ?>> - <?= $this->url->link(t('Attach a document'), 'file', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <li <?= $this->app->checkMenuSelection('TaskExternalLink', 'show') ?>> + <?= $this->url->link(t('External links'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> - <li <?= $this->app->checkMenuSelection('file', 'screenshot') ?>> - <?= $this->url->link(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskduplication', 'duplicate') ?>> - <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskduplication', 'copy') ?>> - <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskduplication', 'move') ?>> - <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <li <?= $this->app->checkMenuSelection('taskstatus') ?>> - <?php if ($task['is_active'] == 1): ?> - <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php else: ?> - <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php endif ?> - </li> - <?php if ($this->task->canRemove($task)): ?> - <li <?= $this->app->checkMenuSelection('task', 'remove') ?>> - <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <?php endif ?> - <?= $this->hook->render('template:task:sidebar:actions') ?> + <?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?> </ul> - <?php endif ?> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div> diff --git a/app/Template/task/time_tracking_details.php b/app/Template/task/time_tracking_details.php index faa07cb8..c51b8f5a 100644 --- a/app/Template/task/time_tracking_details.php +++ b/app/Template/task/time_tracking_details.php @@ -1,3 +1,7 @@ +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> + <?= $this->render('task/time_tracking_summary', array('task' => $task)) ?> <h3><?= t('Subtask timesheet') ?></h3> @@ -10,14 +14,14 @@ <th><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th> <th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th> <th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th> - <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th> + <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), \Kanboard\Model\SubtaskTimeTracking::TABLE.'.time_spent') ?></th> </tr> <?php foreach ($subtask_paginator->getCollection() as $record): ?> <tr> - <td><?= $this->url->link($this->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td> + <td><?= $this->url->link($this->text->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td> <td><?= t($record['subtask_title']) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td> + <td><?= $this->dt->datetime($record['start']) ?></td> + <td><?= $this->dt->datetime($record['end']) ?></td> <td><?= n($record['time_spent']).' '.t('hours') ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/task/time_tracking_summary.php b/app/Template/task/time_tracking_summary.php index 0210be7e..9886ccfa 100644 --- a/app/Template/task/time_tracking_summary.php +++ b/app/Template/task/time_tracking_summary.php @@ -5,9 +5,9 @@ </div> <ul class="listing"> - <li><?= t('Estimate:') ?> <strong><?= $this->e($task['time_estimated']) ?></strong> <?= t('hours') ?></li> - <li><?= t('Spent:') ?> <strong><?= $this->e($task['time_spent']) ?></strong> <?= t('hours') ?></li> - <li><?= t('Remaining:') ?> <strong><?= $this->e($task['time_estimated'] - $task['time_spent']) ?></strong> <?= t('hours') ?></li> + <li><?= t('Estimate:') ?> <strong><?= $this->text->e($task['time_estimated']) ?></strong> <?= t('hours') ?></li> + <li><?= t('Spent:') ?> <strong><?= $this->text->e($task['time_spent']) ?></strong> <?= t('hours') ?></li> + <li><?= t('Remaining:') ?> <strong><?= $this->text->e($task['time_estimated'] - $task['time_spent']) ?></strong> <?= t('hours') ?></li> </ul> <?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php index 2ca2387f..83040177 100644 --- a/app/Template/task/transitions.php +++ b/app/Template/task/transitions.php @@ -1,3 +1,7 @@ +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> + <div class="page-header"> <h2><?= t('Transitions') ?></h2> </div> @@ -15,10 +19,10 @@ </tr> <?php foreach ($transitions as $transition): ?> <tr> - <td><?= dt('%B %e, %Y at %k:%M %p', $transition['date']) ?></td> - <td><?= $this->e($transition['src_column']) ?></td> - <td><?= $this->e($transition['dst_column']) ?></td> - <td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td> + <td><?= $this->dt->datetime($transition['date']) ?></td> + <td><?= $this->text->e($transition['src_column']) ?></td> + <td><?= $this->text->e($transition['dst_column']) ?></td> + <td><?= $this->url->link($this->text->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td> <td><?= $this->dt->duration($transition['time_spent']) ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php index eaf9024d..0af2eb1e 100644 --- a/app/Template/task_creation/form.php +++ b/app/Template/task_creation/form.php @@ -1,16 +1,8 @@ -<?php if (! $ajax): ?> -<div class="page-header"> - <ul> - <li><i class="fa fa-th fa-fw"></i><?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $values['project_id'])) ?></li> - </ul> -</div> -<?php else: ?> <div class="page-header"> <h2><?= t('New task') ?></h2> </div> -<?php endif ?> -<form id="task-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -55,36 +47,20 @@ <div class="form-column"> <?= $this->form->hidden('project_id', $values) ?> - - <?= $this->form->label(t('Assignee'), 'owner_id') ?> - <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?> - - <?= $this->form->label(t('Category'), 'category_id') ?> - <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?> - - <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?> - <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> - <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?> - <?php endif ?> - - <?= $this->form->label(t('Column'), 'column_id') ?> - <?= $this->form->select('column_id', $columns_list, $values, $errors, array('tabindex="6"')) ?> - + <?= $this->task->selectAssignee($users_list, $values, $errors) ?> + <?= $this->task->selectCategory($categories_list, $values, $errors) ?> + <?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?> + <?= $this->task->selectColumn($columns_list, $values, $errors) ?> <?= $this->task->selectPriority($project, $values) ?> + <?= $this->task->selectScore($values, $errors) ?> + <?= $this->task->selectTimeEstimated($values, $errors) ?> + <?= $this->task->selectDueDate($values, $errors) ?> - <?= $this->form->label(t('Complexity'), 'score') ?> - <?= $this->form->number('score', $values, $errors, array('tabindex="9"')) ?> - - <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> - <?= $this->form->numeric('time_estimated', $values, $errors, array('tabindex="10"')) ?> <?= t('hours') ?> - - <?= $this->form->label(t('Due Date'), 'date_due') ?> - <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="11"'), 'form-date') ?> - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + <?= $this->hook->render('template:task:form:right-column', array('values'=>$values, 'errors'=>$errors)) ?> </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="12"/> + <button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/task_duplication/copy.php b/app/Template/task_duplication/copy.php index 415b8610..b7337a1e 100644 --- a/app/Template/task_duplication/copy.php +++ b/app/Template/task_duplication/copy.php @@ -6,7 +6,7 @@ <p class="alert"><?= t('There is no destination project available.') ?></p> <?php else: ?> - <form method="post" action="<?= $this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + <form class="popover-form" method="post" action="<?= $this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> @@ -39,9 +39,9 @@ <p class="form-help"><?= t('Current assignee: %s', ($task['assignee_name'] ?: $task['assignee_username']) ?: e('not assigned')) ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/task_duplication/duplicate.php b/app/Template/task_duplication/duplicate.php index 4b50d9ca..376f6b3b 100644 --- a/app/Template/task_duplication/duplicate.php +++ b/app/Template/task_duplication/duplicate.php @@ -10,6 +10,6 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/task_duplication/move.php b/app/Template/task_duplication/move.php index d8d1ba05..beebf9eb 100644 --- a/app/Template/task_duplication/move.php +++ b/app/Template/task_duplication/move.php @@ -6,7 +6,7 @@ <p class="alert"><?= t('There is no destination project available.') ?></p> <?php else: ?> - <form method="post" action="<?= $this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + <form class="popover-form" method="post" action="<?= $this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> @@ -39,9 +39,9 @@ <p class="form-help"><?= t('Current assignee: %s', ($task['assignee_name'] ?: $task['assignee_username']) ?: e('not assigned')) ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/task_external_link/create.php b/app/Template/task_external_link/create.php new file mode 100644 index 00000000..5d49eef0 --- /dev/null +++ b/app/Template/task_external_link/create.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Add a new external link') ?></h2> +</div> + +<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/task_external_link/edit.php b/app/Template/task_external_link/edit.php new file mode 100644 index 00000000..dcbc2633 --- /dev/null +++ b/app/Template/task_external_link/edit.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Edit external link') ?></h2> +</div> + +<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/task_external_link/find.php b/app/Template/task_external_link/find.php new file mode 100644 index 00000000..3977a66c --- /dev/null +++ b/app/Template/task_external_link/find.php @@ -0,0 +1,28 @@ +<div class="page-header"> + <h2><?= t('Add a new external link') ?></h2> +</div> + +<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> + + <?= $this->form->label(t('External link'), 'text') ?> + <?= $this->form->text( + 'text', + $values, + $errors, + array( + 'required', + 'autofocus', + 'placeholder="'.t('Copy and paste your link here...').'"', + )) ?> + + <?= $this->form->label(t('Link type'), 'type') ?> + <?= $this->form->select('type', $types, $values) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Next') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/task_external_link/form.php b/app/Template/task_external_link/form.php new file mode 100644 index 00000000..932ca521 --- /dev/null +++ b/app/Template/task_external_link/form.php @@ -0,0 +1,13 @@ +<?= $this->form->csrf() ?> +<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> +<?= $this->form->hidden('id', $values) ?> +<?= $this->form->hidden('link_type', $values) ?> + +<?= $this->form->label(t('URL'), 'url') ?> +<?= $this->form->text('url', $values, $errors, array('required')) ?> + +<?= $this->form->label(t('Title'), 'title') ?> +<?= $this->form->text('title', $values, $errors, array('required')) ?> + +<?= $this->form->label(t('Dependency'), 'dependency') ?> +<?= $this->form->select('dependency', $dependencies, $values, $errors) ?> diff --git a/app/Template/task_external_link/remove.php b/app/Template/task_external_link/remove.php new file mode 100644 index 00000000..01535255 --- /dev/null +++ b/app/Template/task_external_link/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a link') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this link: "%s"?', $link['title']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'TaskExternalLink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/task_external_link/show.php b/app/Template/task_external_link/show.php new file mode 100644 index 00000000..0b9567ba --- /dev/null +++ b/app/Template/task_external_link/show.php @@ -0,0 +1,54 @@ +<div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> +</div> + +<div class="page-header"> + <h2><?= t('External links') ?></h2> +</div> + +<?php if (empty($links)): ?> + <p class="alert"><?= t('There is no external link for the moment.') ?></p> +<?php else: ?> + <table class="table-stripped table-small"> + <tr> + <th class="column-10"><?= t('Type') ?></th> + <th><?= t('Title') ?></th> + <th class="column-10"><?= t('Dependency') ?></th> + <th class="column-15"><?= t('Creator') ?></th> + <th class="column-15"><?= t('Date') ?></th> + <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?> + <th class="column-5"><?= t('Action') ?></th> + <?php endif ?> + </tr> + <?php foreach ($links as $link): ?> + <tr> + <td> + <?= $link['type'] ?> + </td> + <td> + <a href="<?= $link['url'] ?>" target="_blank"><?= $this->text->e($link['title']) ?></a> + </td> + <td> + <?= $this->text->e($link['dependency_label']) ?> + </td> + <td> + <?= $this->text->e($link['creator_name'] ?: $link['creator_username']) ?> + </td> + <td> + <?= $this->dt->date($link['date_creation']) ?> + </td> + <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?> + <td> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li><?= $this->url->link(t('Edit'), 'TaskExternalLink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> + <li><?= $this->url->link(t('Remove'), 'TaskExternalLink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> + </ul> + </div> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </table> +<?php endif ?> diff --git a/app/Template/task_file/create.php b/app/Template/task_file/create.php new file mode 100644 index 00000000..f03ce8dc --- /dev/null +++ b/app/Template/task_file/create.php @@ -0,0 +1,33 @@ +<div class="page-header"> + <h2><?= t('Attach a document') ?></h2> +</div> +<div id="file-done" style="display:none"> + <p class="alert alert-success"> + <?= t('All files have been uploaded successfully.') ?> + <?= $this->url->link(t('View uploaded files'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </p> +</div> + +<div id="file-error-max-size" style="display:none"> + <p class="alert alert-error"> + <?= t('The maximum allowed file size is %sB.', $this->text->bytes($max_size)) ?> + <a href="#" id="file-browser"><?= t('Choose files again') ?></a> + </p> +</div> + +<div + id="file-dropzone" + data-max-size="<?= $max_size ?>" + data-url="<?= $this->url->href('TaskFile', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> + <div id="file-dropzone-inner"> + <?= t('Drag and drop your files here') ?> <?= t('or') ?> <a href="#" id="file-browser"><?= t('choose files') ?></a> + </div> +</div> + +<input type="file" name="files[]" multiple style="display:none" id="file-form-element"> + +<div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" id="file-upload-button" disabled> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +</div> diff --git a/app/Template/file/remove.php b/app/Template/task_file/remove.php index 37f648eb..fe601f6f 100644 --- a/app/Template/file/remove.php +++ b/app/Template/task_file/remove.php @@ -4,12 +4,12 @@ <div class="confirm"> <p class="alert alert-info"> - <?= t('Do you really want to remove this file: "%s"?', $this->e($file['name'])) ?> + <?= t('Do you really want to remove this file: "%s"?', $this->text->e($file['name'])) ?> </p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'file', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'TaskFile', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/file/screenshot.php b/app/Template/task_file/screenshot.php index 73b72eae..2880478d 100644 --- a/app/Template/file/screenshot.php +++ b/app/Template/task_file/screenshot.php @@ -6,11 +6,11 @@ <p id="screenshot-inner"><?= t('Take a screenshot and press CTRL+V or ⌘+V to paste here.') ?></p> </div> -<form action="<?= $this->url->href('file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => $redirect)) ?>" method="post"> +<form class="popover-form" action="<?= $this->url->href('TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post"> <input type="hidden" name="screenshot"/> <?= $this->form->csrf() ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> diff --git a/app/Template/task_file/show.php b/app/Template/task_file/show.php new file mode 100644 index 00000000..21bf79ef --- /dev/null +++ b/app/Template/task_file/show.php @@ -0,0 +1,90 @@ +<?php if (! empty($files) || ! empty($images)): ?> +<div id="attachments" class="task-show-section"> + + <div class="page-header"> + <h2><?= t('Attachments') ?></h2> + </div> + <?php if (! empty($images)): ?> + <div class="file-thumbnails"> + <?php foreach ($images as $file): ?> + <div class="file-thumbnail"> + <a href="<?= $this->url->href('FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>" class="popover"><img src="<?= $this->url->href('FileViewer', 'thumbnail', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" title="<?= $this->text->e($file['name']) ?>" alt="<?= $this->text->e($file['name']) ?>"></a> + <div class="file-thumbnail-content"> + <div class="file-thumbnail-title"> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->text->e($file['name']) ?> <i class="fa fa-caret-down"></i></a> + <ul> + <li> + <i class="fa fa-download fa-fw"></i> + <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + </li> + <?php if ($this->user->hasProjectAccess('TaskFile', 'remove', $task['project_id'])): ?> + <li> + <i class="fa fa-trash fa-fw"></i> + <?= $this->url->link(t('Remove'), 'TaskFile', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + </ul> + </div> + </div> + <div class="file-thumbnail-description"> + <span class="tooltip" title='<?= t('Uploaded: %s', $this->dt->datetime($file['date'])).'<br>'.t('Size: %s', $this->text->bytes($file['size'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?= t('Uploaded by %s', $file['user_name'] ?: $file['username']) ?> + </div> + </div> + </div> + <?php endforeach ?> + </div> + <?php endif ?> + + <?php if (! empty($files)): ?> + <table class="table-stripped"> + <tr> + <th><?= t('Filename') ?></th> + <th><?= t('Creator') ?></th> + <th><?= t('Date') ?></th> + <th><?= t('Size') ?></th> + </tr> + <?php foreach ($files as $file): ?> + <tr> + <td> + <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-text"><?= $this->text->e($file['name']) ?> <i class="fa fa-caret-down"></i></a> + <ul> + <?php if ($this->file->getPreviewType($file['name']) !== null): ?> + <li> + <i class="fa fa-eye fa-fw"></i> + <?= $this->url->link(t('View file'), 'FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-download fa-fw"></i> + <?= $this->url->link(t('Download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + </li> + <?php if ($this->user->hasProjectAccess('TaskFile', 'remove', $task['project_id'])): ?> + <li> + <i class="fa fa-trash fa-fw"></i> + <?= $this->url->link(t('Remove'), 'TaskFile', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + </li> + <?php endif ?> + </ul> + </div> + </td> + <td> + <?= $this->text->e($file['user_name'] ?: $file['username']) ?> + </td> + <td> + <?= $this->dt->date($file['date']) ?> + </td> + <td> + <?= $this->text->bytes($file['size']) ?> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> +</div> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task_import/step1.php b/app/Template/task_import/step1.php index 7619216a..abb43505 100644 --- a/app/Template/task_import/step1.php +++ b/app/Template/task_import/step1.php @@ -16,7 +16,7 @@ <p class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Import') ?>" class="btn btn-blue"> + <button type="submit" class="btn btn-blue"><?= t('Import') ?></button> </div> </form> <div class="page-header"> diff --git a/app/Template/task_modification/edit_description.php b/app/Template/task_modification/edit_description.php index c38e885d..cb74b250 100644 --- a/app/Template/task_modification/edit_description.php +++ b/app/Template/task_modification/edit_description.php @@ -2,7 +2,7 @@ <h2><?= t('Edit the description') ?></h2> </div> -<form method="post" action="<?= $this->url->href('taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('taskmodification', 'updateDescription', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> @@ -37,12 +37,8 @@ <div class="form-help"><?= $this->url->doc(t('Write your text in Markdown'), 'syntax-guide') ?></div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?php if ($ajax): ?> - <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> - <?php else: ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php endif ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/task_modification/edit_task.php b/app/Template/task_modification/edit_task.php index 2701dd8f..b5891c15 100644 --- a/app/Template/task_modification/edit_task.php +++ b/app/Template/task_modification/edit_task.php @@ -1,72 +1,35 @@ <div class="page-header"> <h2><?= t('Edit a task') ?></h2> </div> -<form id="task-form" method="post" action="<?= $this->url->href('taskmodification', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> +<form class="popover-form" method="post" action="<?= $this->url->href('taskmodification', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> <div class="form-column"> - <?= $this->form->label(t('Title'), 'title') ?> <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?> - - <?= $this->form->label(t('Description'), 'description') ?> - <div class="form-tabs"> - <div class="write-area"> - <?= $this->form->textarea( - 'description', - $values, - $errors, - array( - 'placeholder="'.t('Leave a description').'"', - 'tabindex="2"', - 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"' - ) - ) ?> - </div> - <div class="preview-area"> - <div class="markdown"></div> - </div> - <ul class="form-tabs-nav"> - <li class="form-tab form-tab-selected"> - <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> - </li> - <li class="form-tab"> - <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> - </li> - </ul> - </div> - - <?= $this->render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> + <?= $this->task->selectAssignee($users_list, $values, $errors) ?> + <?= $this->task->selectCategory($categories_list, $values, $errors) ?> + <?= $this->task->selectPriority($project, $values) ?> + <?= $this->task->selectScore($values, $errors) ?> </div> <div class="form-column"> - <?= $this->form->hidden('id', $values) ?> - <?= $this->form->hidden('project_id', $values) ?> - - <?= $this->form->label(t('Assignee'), 'owner_id') ?> - <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?> - - <?= $this->form->label(t('Category'), 'category_id') ?> - <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?> - - <?= $this->form->label(t('Complexity'), 'score') ?> - <?= $this->form->number('score', $values, $errors, array('tabindex="6"')) ?> - - <?= $this->task->selectPriority($project, $values) ?> + <?= $this->task->selectTimeEstimated($values, $errors) ?> + <?= $this->task->selectTimeSpent($values, $errors) ?> + <?= $this->task->selectStartDate($values, $errors) ?> + <?= $this->task->selectDueDate($values, $errors) ?> + </div> - <?= $this->form->label(t('Due Date'), 'date_due') ?> - <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="8"'), 'form-date') ?> - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + <div class="form-clear"> + <?= $this->render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="10"> + <button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button> <?= t('or') ?> - <?php if ($ajax): ?> - <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?> - <?php else: ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php endif ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/task_modification/edit_time.php b/app/Template/task_modification/edit_time.php deleted file mode 100644 index 8e7f9b42..00000000 --- a/app/Template/task_modification/edit_time.php +++ /dev/null @@ -1,20 +0,0 @@ -<form method="post" action="<?= $this->url->href('taskmodification', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off"> - - <?php if (empty($values['date_started'])): ?> - <?= $this->url->link('<i class="fa fa-play"></i>', 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-show-start-link', t('Set automatically the start date')) ?> - <?php endif ?> - - <?= $this->form->csrf() ?> - <?= $this->form->hidden('id', $values) ?> - - <?= $this->form->label(t('Start date'), 'date_started') ?> - <?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-datetime') ?> - - <?= $this->form->label(t('Time estimated'), 'time_estimated') ?> - <?= $this->form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?> - - <?= $this->form->label(t('Time spent'), 'time_spent') ?> - <?= $this->form->numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?> - - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> -</form>
\ No newline at end of file diff --git a/app/Template/task_modification/edit_recurrence.php b/app/Template/task_recurrence/edit.php index dc4faa7a..0f5d611a 100644 --- a/app/Template/task_modification/edit_recurrence.php +++ b/app/Template/task_recurrence/edit.php @@ -4,7 +4,7 @@ <?php if ($task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_NONE): ?> <div class="listing"> - <?= $this->render('task/recurring_info', array( + <?= $this->render('task_recurrence/info', array( 'task' => $task, 'recurrence_trigger_list' => $recurrence_trigger_list, 'recurrence_timeframe_list' => $recurrence_timeframe_list, @@ -15,7 +15,7 @@ <?php if ($task['recurrence_status'] != \Kanboard\Model\Task::RECURRING_STATUS_PROCESSED): ?> - <form method="post" action="<?= $this->url->href('taskmodification', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + <form class="popover-form" method="post" action="<?= $this->url->href('TaskRecurrence', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> @@ -38,9 +38,9 @@ <?= $this->form->select('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form> diff --git a/app/Template/task/recurring_info.php b/app/Template/task_recurrence/info.php index 83ca0960..1a6574df 100644 --- a/app/Template/task/recurring_info.php +++ b/app/Template/task_recurrence/info.php @@ -5,16 +5,16 @@ <li><?= t('Recurrent task has been generated:') ?> <ul> <li> - <?= t('Trigger to generate recurrent task: ') ?><strong><?= $this->e($recurrence_trigger_list[$task['recurrence_trigger']]) ?></strong> + <?= t('Trigger to generate recurrent task: ') ?><strong><?= $this->text->e($recurrence_trigger_list[$task['recurrence_trigger']]) ?></strong> </li> <li> - <?= t('Factor to calculate new due date: ') ?><strong><?= $this->e($task['recurrence_factor']) ?></strong> + <?= t('Factor to calculate new due date: ') ?><strong><?= $this->text->e($task['recurrence_factor']) ?></strong> </li> <li> - <?= t('Timeframe to calculate new due date: ') ?><strong><?= $this->e($recurrence_timeframe_list[$task['recurrence_timeframe']]) ?></strong> + <?= t('Timeframe to calculate new due date: ') ?><strong><?= $this->text->e($recurrence_timeframe_list[$task['recurrence_timeframe']]) ?></strong> </li> <li> - <?= t('Base date to calculate new due date: ') ?><strong><?= $this->e($recurrence_basedate_list[$task['recurrence_basedate']]) ?></strong> + <?= t('Base date to calculate new due date: ') ?><strong><?= $this->text->e($recurrence_basedate_list[$task['recurrence_basedate']]) ?></strong> </li> </ul> </li> diff --git a/app/Template/task_status/close.php b/app/Template/task_status/close.php index d32863bd..7d200544 100644 --- a/app/Template/task_status/close.php +++ b/app/Template/task_status/close.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> diff --git a/app/Template/task_status/open.php b/app/Template/task_status/open.php index 615b2464..5d19bfbe 100644 --- a/app/Template/task_status/open.php +++ b/app/Template/task_status/open.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php index 2832bdc7..02d38a9b 100644 --- a/app/Template/tasklink/create.php +++ b/app/Template/tasklink/create.php @@ -2,7 +2,7 @@ <h2><?= t('Add a new link') ?></h2> </div> -<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" method="post" autocomplete="off"> +<form class="popover-form" action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> <?= $this->form->csrf() ?> <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> @@ -26,12 +26,8 @@ 'autocomplete') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?php if (isset($ajax)): ?> - <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?> - <?php else: ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - <?php endif ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/tasklink/edit.php b/app/Template/tasklink/edit.php index 896f84c0..b174c348 100644 --- a/app/Template/tasklink/edit.php +++ b/app/Template/tasklink/edit.php @@ -27,8 +27,8 @@ 'autocomplete') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </form>
\ No newline at end of file diff --git a/app/Template/tasklink/remove.php b/app/Template/tasklink/remove.php index 262fb488..42bf3012 100644 --- a/app/Template/tasklink/remove.php +++ b/app/Template/tasklink/remove.php @@ -10,6 +10,6 @@ <div class="form-actions"> <?= $this->url->link(t('Yes'), 'tasklink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php index 5843da17..1aa141a4 100644 --- a/app/Template/tasklink/show.php +++ b/app/Template/tasklink/show.php @@ -1,109 +1,37 @@ -<?php if (! empty($links)): ?> +<?php if (isset($show_title)): ?> + <div class="task-show-title color-<?= $task['color_id'] ?>"> + <h2><?= $this->text->e($task['title']) ?></h2> + </div> +<?php endif ?> + <div class="page-header"> - <h2><?= t('Links') ?></h2> + <h2><?= t('Internal links') ?></h2> </div> -<table id="links"> - <tr> - <th class="column-20"><?= t('Label') ?></th> - <th class="column-30"><?= t('Task') ?></th> - <th class="column-20"><?= t('Project') ?></th> - <th><?= t('Column') ?></th> - <th><?= t('Assignee') ?></th> - <?php if ($editable): ?> - <th class="column-5"><?= t('Action') ?></th> - <?php endif ?> - </tr> - <?php foreach ($links as $label => $grouped_links): ?> - <?php $hide_td = false ?> - <?php foreach ($grouped_links as $link): ?> - <tr> - <?php if (! $hide_td): ?> - <td rowspan="<?= count($grouped_links) ?>"><?= t('This task') ?> <strong><?= t($label) ?></strong></td> - <?php $hide_td = true ?> - <?php endif ?> - - <td> - <?php if ($is_public): ?> - <?= $this->url->link( - $this->e('#'.$link['task_id'].' '.$link['title']), - 'task', - 'readonly', - array('task_id' => $link['task_id'], 'token' => $project['token']), - false, - $link['is_active'] ? '' : 'task-link-closed' - ) ?> - <?php else: ?> - <?= $this->url->link( - $this->e('#'.$link['task_id'].' '.$link['title']), - 'task', - 'show', - array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), - false, - $link['is_active'] ? '' : 'task-link-closed' - ) ?> - <?php endif ?> - - <br/> - - <?php if (! empty($link['task_time_spent'])): ?> - <strong><?= $this->e($link['task_time_spent']).'h' ?></strong> <?= t('spent') ?> - <?php endif ?> - - <?php if (! empty($link['task_time_estimated'])): ?> - <strong><?= $this->e($link['task_time_estimated']).'h' ?></strong> <?= t('estimated') ?> - <?php endif ?> - </td> - <td><?= $this->e($link['project_name']) ?></td> - <td><?= $this->e($link['column_title']) ?></td> - <td> - <?php if (! empty($link['task_assignee_username'])): ?> - <?php if ($editable): ?> - <?= $this->url->link($this->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'user', 'show', array('user_id' => $link['task_assignee_id'])) ?> - <?php else: ?> - <?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?> - <?php endif ?> - <?php endif ?> - </td> - <?php if ($editable): ?> - <td> - <div class="dropdown"> - <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> - <ul> - <li><?= $this->url->link(t('Edit'), 'tasklink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li> - <li><?= $this->url->link(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li> - </ul> - </div> - </td> - <?php endif ?> - </tr> - <?php endforeach ?> - <?php endforeach ?> -</table> - -<?php if ($editable && isset($link_label_list)): ?> - <form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> - <?= $this->form->csrf() ?> - <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> - <?= $this->form->hidden('opposite_task_id', array()) ?> +<div id="link"> + + <?= $this->render('tasklink/table', array('links' => $links, 'task' => $task, 'project' => $project, 'editable' => $editable, 'is_public' => $is_public)) ?> + + <?php if ($editable && isset($link_label_list)): ?> + <form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> + <?= $this->form->hidden('opposite_task_id', array()) ?> + <?= $this->form->select('link_id', $link_label_list, array(), array()) ?> + <?= $this->form->text( + 'title', + array(), + array(), + array( + 'required', + 'placeholder="'.t('Start to type task title...').'"', + 'title="'.t('Start to type task title...').'"', + 'data-dst-field="opposite_task_id"', + 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + ), + 'autocomplete') ?> + <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + </form> + <?php endif ?> - <?= $this->form->select('link_id', $link_label_list, array(), array()) ?> - - <?= $this->form->text( - 'title', - array(), - array(), - array( - 'required', - 'placeholder="'.t('Start to type task title...').'"', - 'title="'.t('Start to type task title...').'"', - 'data-dst-field="opposite_task_id"', - 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', - ), - 'autocomplete') ?> - - <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> - </form> -<?php endif ?> - -<?php endif ?> +</div> diff --git a/app/Template/tasklink/table.php b/app/Template/tasklink/table.php new file mode 100644 index 00000000..62531cc7 --- /dev/null +++ b/app/Template/tasklink/table.php @@ -0,0 +1,87 @@ +<?php if (empty($links)): ?> + <p class="alert"><?= t('There is no internal link for the moment.') ?></p> +<?php else: ?> +<table class="task-links-table table-stripped"> + <?php foreach ($links as $label => $grouped_links): ?> + <?php $hide_td = false ?> + <?php foreach ($grouped_links as $link): ?> + <?php if (! $hide_td): ?> + <tr> + <td class="column-40" colspan="2"> + <?= t('This task') ?> + <strong><?= t($label) ?></strong> + <span class="task-links-task-count">(<?= count($grouped_links) ?>)</span> + </td> + <th><?= t('Assignee') ?></th> + <th><?= t('Time tracking') ?></th> + <?php if ($editable): ?> + <th class="column-5"></th> + <?php endif ?> + </tr> + <?php $hide_td = true ?> + <?php endif ?> + + <tr> + <td> + <?php if ($is_public): ?> + <?= $this->url->link( + $this->text->e('#'.$link['task_id'].' '.$link['title']), + 'task', + 'readonly', + array('task_id' => $link['task_id'], 'token' => $project['token']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php else: ?> + <?= $this->url->link( + $this->text->e('#'.$link['task_id'].' '.$link['title']), + 'task', + 'show', + array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php endif ?> + + <?php if ($link['project_id'] != $project['id']): ?> + <br> + <?= $this->text->e($link['project_name']) ?> + <?php endif ?> + </td> + <td> + <?= $this->text->e($link['column_title']) ?> + </td> + <td> + <?php if (! empty($link['task_assignee_username'])): ?> + <?php if ($editable): ?> + <?= $this->url->link($this->text->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'user', 'show', array('user_id' => $link['task_assignee_id'])) ?> + <?php else: ?> + <?= $this->text->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?> + <?php endif ?> + <?php endif ?> + </td> + <td> + <?php if (! empty($link['task_time_spent'])): ?> + <strong><?= $this->text->e($link['task_time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($link['task_time_estimated'])): ?> + <strong><?= $this->text->e($link['task_time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + <?php if ($editable && $this->user->hasProjectAccess('Tasklink', 'edit', $task['project_id'])): ?> + <td> + <div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li><?= $this->url->link(t('Edit'), 'tasklink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> + <li><?= $this->url->link(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li> + </ul> + </div> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + <?php endforeach ?> +</table> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/twofactor/check.php b/app/Template/twofactor/check.php index 68a58a6c..b0cb4825 100644 --- a/app/Template/twofactor/check.php +++ b/app/Template/twofactor/check.php @@ -5,6 +5,6 @@ <?= $this->form->text('code', array(), array(), array('placeholder="123456"', 'autofocus'), 'form-numeric') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Check my code') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php index b9ee4b49..6de36514 100644 --- a/app/Template/twofactor/index.php +++ b/app/Template/twofactor/index.php @@ -4,12 +4,12 @@ <form method="post" action="<?= $this->url->href('twofactor', $user['twofactor_activated'] == 1 ? 'deactivate' : 'show', array('user_id' => $user['id'])) ?>" autocomplete="off"> <?= $this->form->csrf() ?> - <p><?= t('Two-Factor Provider: ') ?><strong><?= $this->e($provider) ?></strong></p> + <p><?= t('Two-Factor Provider: ') ?><strong><?= $this->text->e($provider) ?></strong></p> <div class="form-actions"> <?php if ($user['twofactor_activated'] == 1): ?> - <input type="submit" value="<?= t('Disable two-factor authentication') ?>" class="btn btn-red"/> + <button type="submit" class="btn btn-red"><?= t('Disable two-factor authentication') ?></button> <?php else: ?> - <input type="submit" value="<?= t('Enable two-factor authentication') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Enable two-factor authentication') ?></button> <?php endif ?> </div> </form> diff --git a/app/Template/twofactor/show.php b/app/Template/twofactor/show.php index dd72965a..59897e20 100644 --- a/app/Template/twofactor/show.php +++ b/app/Template/twofactor/show.php @@ -5,15 +5,15 @@ <?php if (! empty($secret) || ! empty($qrcode_url) || ! empty($key_url)): ?> <div class="listing"> <?php if (! empty($secret)): ?> - <p><?= t('Secret key: ') ?><strong><?= $this->e($secret) ?></strong></p> + <p><?= t('Secret key: ') ?><strong><?= $this->text->e($secret) ?></strong></p> <?php endif ?> <?php if (! empty($qrcode_url)): ?> - <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p> + <p><br><img src="<?= $qrcode_url ?>"/><br><br></p> <?php endif ?> <?php if (! empty($key_url)): ?> - <p><?= t('This QR code contains the key URI: ') ?><a href="<?= $this->e($key_url) ?>"><?= $this->e($key_url) ?></a></p> + <p><?= t('This QR code contains the key URI: ') ?><a href="<?= $this->text->e($key_url) ?>"><?= $this->text->e($key_url) ?></a></p> <?php endif ?> </div> <?php endif ?> @@ -26,6 +26,6 @@ <?= $this->form->text('code', array(), array(), array('placeholder="123456"', 'autofocus'), 'form-numeric') ?> <div class="form-actions"> - <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Check my code') ?></button> </div> </form>
\ No newline at end of file diff --git a/app/Template/user/authentication.php b/app/Template/user/authentication.php index 20c3d372..6cfd4e57 100644 --- a/app/Template/user/authentication.php +++ b/app/Template/user/authentication.php @@ -8,20 +8,13 @@ <?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('username', $values) ?> - <?= $this->form->label(t('Google Id'), 'google_id') ?> - <?= $this->form->text('google_id', $values, $errors) ?> - - <?= $this->form->label(t('Github Id'), 'github_id') ?> - <?= $this->form->text('github_id', $values, $errors) ?> - - <?= $this->form->label(t('Gitlab Id'), 'gitlab_id') ?> - <?= $this->form->text('gitlab_id', $values, $errors) ?> + <?= $this->hook->render('template:user:authentication:form', array('values' => $values, 'errors' => $errors, 'user' => $user)) ?> <?= $this->form->checkbox('is_ldap_user', t('Remote user'), 1, isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) ?> <?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> </div> diff --git a/app/Template/user/create_local.php b/app/Template/user/create_local.php index 38bd7836..7257456d 100644 --- a/app/Template/user/create_local.php +++ b/app/Template/user/create_local.php @@ -44,7 +44,7 @@ </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'index') ?> </div> diff --git a/app/Template/user/create_remote.php b/app/Template/user/create_remote.php index 1cc560cd..05acbba1 100644 --- a/app/Template/user/create_remote.php +++ b/app/Template/user/create_remote.php @@ -20,14 +20,7 @@ <?= $this->form->label(t('Email'), 'email') ?> <?= $this->form->email('email', $values, $errors) ?> - <?= $this->form->label(t('Google Id'), 'google_id') ?> - <?= $this->form->text('google_id', $values, $errors) ?> - - <?= $this->form->label(t('Github Id'), 'github_id') ?> - <?= $this->form->text('github_id', $values, $errors) ?> - - <?= $this->form->label(t('Gitlab Id'), 'gitlab_id') ?> - <?= $this->form->text('gitlab_id', $values, $errors) ?> + <?= $this->hook->render('template:user:create-remote:form', array('values' => $values, 'errors' => $errors)) ?> </div> <div class="form-column"> @@ -48,7 +41,7 @@ </div> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'index') ?> </div> diff --git a/app/Template/user/dropdown.php b/app/Template/user/dropdown.php new file mode 100644 index 00000000..b74ed6e0 --- /dev/null +++ b/app/Template/user/dropdown.php @@ -0,0 +1,27 @@ +<div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li> + <i class="fa fa-user fa-fw"></i> + <?= $this->url->link(t('View profile'), 'user', 'show', array('user_id' => $user['id'])) ?> + </li> + <?php if ($user['is_active'] == 1 && $this->user->hasAccess('UserStatus', 'disable') && ! $this->user->isCurrentUser($user['id'])): ?> + <li> + <i class="fa fa-times fa-fw"></i> + <?= $this->url->link(t('Disable'), 'UserStatus', 'confirmDisable', array('user_id' => $user['id']), false, 'popover') ?> + </li> + <?php endif ?> + <?php if ($user['is_active'] == 0 && $this->user->hasAccess('UserStatus', 'enable') && ! $this->user->isCurrentUser($user['id'])): ?> + <li> + <i class="fa fa-check-square-o fa-fw"></i> + <?= $this->url->link(t('Enable'), 'UserStatus', 'confirmEnable', array('user_id' => $user['id']), false, 'popover') ?> + </li> + <?php endif ?> + <?php if ($this->user->hasAccess('UserStatus', 'remove') && ! $this->user->isCurrentUser($user['id'])): ?> + <li> + <i class="fa fa-trash-o fa-fw"></i> + <?= $this->url->link(t('Remove'), 'UserStatus', 'confirmRemove', array('user_id' => $user['id']), false, 'popover') ?> + </li> + <?php endif ?> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php index f7f67fb7..7b51eb73 100644 --- a/app/Template/user/edit.php +++ b/app/Template/user/edit.php @@ -28,7 +28,7 @@ <?php endif ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> </div> diff --git a/app/Template/user/external.php b/app/Template/user/external.php index 8b1d3c46..22c25af2 100644 --- a/app/Template/user/external.php +++ b/app/Template/user/external.php @@ -2,54 +2,10 @@ <h2><?= t('External authentications') ?></h2> </div> -<?php if (GOOGLE_AUTH): ?> - <h3><i class="fa fa-google"></i> <?= t('Google Account') ?></h3> +<?php $html = $this->hook->render('template:user:external', array('user' => $user)) ?> - <p class="listing"> - <?php if ($this->user->isCurrentUser($user['id'])): ?> - <?php if (empty($user['google_id'])): ?> - <?= $this->url->link(t('Link my Google Account'), 'oauth', 'google', array(), true) ?> - <?php else: ?> - <?= $this->url->link(t('Unlink my Google Account'), 'oauth', 'unlink', array('backend' => 'Google'), true) ?> - <?php endif ?> - <?php else: ?> - <?= empty($user['google_id']) ? t('No account linked.') : t('Account linked.') ?> - <?php endif ?> - </p> -<?php endif ?> - -<?php if (GITHUB_AUTH): ?> - <h3><i class="fa fa-github"></i> <?= t('Github Account') ?></h3> - - <p class="listing"> - <?php if ($this->user->isCurrentUser($user['id'])): ?> - <?php if (empty($user['github_id'])): ?> - <?= $this->url->link(t('Link my Github Account'), 'oauth', 'github', array(), true) ?> - <?php else: ?> - <?= $this->url->link(t('Unlink my Github Account'), 'oauth', 'unlink', array('backend' => 'Github'), true) ?> - <?php endif ?> - <?php else: ?> - <?= empty($user['github_id']) ? t('No account linked.') : t('Account linked.') ?> - <?php endif ?> - </p> -<?php endif ?> - -<?php if (GITLAB_AUTH): ?> - <h3><img src="<?= $this->url->dir() ?>assets/img/gitlab-icon.png"/> <?= t('Gitlab Account') ?></h3> - - <p class="listing"> - <?php if ($this->user->isCurrentUser($user['id'])): ?> - <?php if (empty($user['gitlab_id'])): ?> - <?= $this->url->link(t('Link my Gitlab Account'), 'oauth', 'gitlab', array(), true) ?> - <?php else: ?> - <?= $this->url->link(t('Unlink my Gitlab Account'), 'oauth', 'unlink', array('backend' => 'Gitlab'), true) ?> - <?php endif ?> - <?php else: ?> - <?= empty($user['gitlab_id']) ? t('No account linked.') : t('Account linked.') ?> - <?php endif ?> - </p> -<?php endif ?> - -<?php if (! GOOGLE_AUTH && ! GITHUB_AUTH && ! GITLAB_AUTH): ?> +<?php if (empty($html)): ?> <p class="alert"><?= t('No external authentication enabled.') ?></p> +<?php else: ?> + <?= $html ?> <?php endif ?> diff --git a/app/Template/user/index.php b/app/Template/user/index.php index cb7416d6..364fd965 100644 --- a/app/Template/user/index.php +++ b/app/Template/user/index.php @@ -12,30 +12,28 @@ <?php if ($paginator->isEmpty()): ?> <p class="alert"><?= t('No user') ?></p> <?php else: ?> - <table> + <table class="table-stripped"> <tr> - <th><?= $paginator->order(t('Id'), 'id') ?></th> - <th><?= $paginator->order(t('Username'), 'username') ?></th> - <th><?= $paginator->order(t('Name'), 'name') ?></th> - <th><?= $paginator->order(t('Email'), 'email') ?></th> - <th><?= $paginator->order(t('Role'), 'role') ?></th> - <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th> - <th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th> - <th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th> + <th class="column-18"><?= $paginator->order(t('Username'), 'username') ?></th> + <th class="column-18"><?= $paginator->order(t('Name'), 'name') ?></th> + <th class="column-15"><?= $paginator->order(t('Email'), 'email') ?></th> + <th class="column-15"><?= $paginator->order(t('Role'), 'role') ?></th> + <th class="column-10"><?= $paginator->order(t('Two Factor'), 'twofactor_activated') ?></th> + <th class="column-10"><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th> + <th class="column-10"><?= $paginator->order(t('Status'), 'is_active') ?></th> + <th class="column-5"><?= t('Actions') ?></th> </tr> <?php foreach ($paginator->getCollection() as $user): ?> <tr> <td> - <?= $this->url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?> + <?= '#'.$user['id'] ?> + <?= $this->url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> </td> <td> - <?= $this->url->link($this->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> + <?= $this->text->e($user['name']) ?> </td> <td> - <?= $this->e($user['name']) ?> - </td> - <td> - <a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a> + <a href="mailto:<?= $this->text->e($user['email']) ?>"><?= $this->text->e($user['email']) ?></a> </td> <td> <?= $this->user->getRoleName($user['role']) ?> @@ -44,14 +42,17 @@ <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?> </td> <td> - <?php if ($user['notifications_enabled'] == 1): ?> - <?= t('Enabled') ?> + <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?> + </td> + <td> + <?php if ($user['is_active'] == 1): ?> + <?= t('Active') ?> <?php else: ?> - <?= t('Disabled') ?> + <?= t('Inactive') ?> <?php endif ?> </td> <td> - <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?> + <?= $this->render('user/dropdown', array('user' => $user)) ?> </td> </tr> <?php endforeach ?> diff --git a/app/Template/user/last.php b/app/Template/user/last.php index 8879466e..3de4d5e2 100644 --- a/app/Template/user/last.php +++ b/app/Template/user/last.php @@ -14,10 +14,10 @@ </tr> <?php foreach ($last_logins as $login): ?> <tr> - <td><?= dt('%B %e, %Y at %k:%M %p', $login['date_creation']) ?></td> - <td><?= $this->e($login['auth_type']) ?></td> - <td><?= $this->e($login['ip']) ?></td> - <td><?= $this->e($login['user_agent']) ?></td> + <td><?= $this->dt->datetime($login['date_creation']) ?></td> + <td><?= $this->text->e($login['auth_type']) ?></td> + <td><?= $this->text->e($login['ip']) ?></td> + <td><?= $this->text->e($login['user_agent']) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php index 1e456348..3a0a5ba6 100644 --- a/app/Template/user/layout.php +++ b/app/Template/user/layout.php @@ -13,7 +13,7 @@ <?= $this->render('user/sidebar', array('user' => $user)) ?> <div class="sidebar-content"> - <?= $user_content_for_layout ?> + <?= $content_for_sublayout ?> </div> </section> </section>
\ No newline at end of file diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php index 7223013c..2a5c8152 100644 --- a/app/Template/user/notifications.php +++ b/app/Template/user/notifications.php @@ -22,7 +22,7 @@ <?php endif ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> </div> diff --git a/app/Template/user/password.php b/app/Template/user/password.php index 3ef28d33..ea6e997d 100644 --- a/app/Template/user/password.php +++ b/app/Template/user/password.php @@ -9,17 +9,17 @@ <div class="alert alert-error"> <?= $this->form->label(t('Current password for the user "%s"', $this->user->getFullname()), 'current_password') ?> - <?= $this->form->password('current_password', $values, $errors) ?><br/> + <?= $this->form->password('current_password', $values, $errors) ?> </div> <?= $this->form->label(t('New password for the user "%s"', $this->user->getFullname($user)), 'password') ?> - <?= $this->form->password('password', $values, $errors) ?><br/> + <?= $this->form->password('password', $values, $errors) ?> <?= $this->form->label(t('Confirmation'), 'confirmation') ?> - <?= $this->form->password('confirmation', $values, $errors) ?><br/> + <?= $this->form->password('confirmation', $values, $errors) ?> <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> </div> diff --git a/app/Template/user/password_reset.php b/app/Template/user/password_reset.php index b4c9a0c4..1371ce11 100644 --- a/app/Template/user/password_reset.php +++ b/app/Template/user/password_reset.php @@ -15,11 +15,11 @@ </tr> <?php foreach ($tokens as $token): ?> <tr> - <td><?= dt('%B %e, %Y at %k:%M %p', $token['date_creation']) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $token['date_expiration']) ?></td> + <td><?= $this->dt->datetime($token['date_creation']) ?></td> + <td><?= $this->dt->datetime($token['date_expiration']) ?></td> <td><?= $token['is_active'] == 0 ? t('No') : t('Yes') ?></td> - <td><?= $this->e($token['ip']) ?></td> - <td><?= $this->e($token['user_agent']) ?></td> + <td><?= $this->text->e($token['ip']) ?></td> + <td><?= $this->text->e($token['user_agent']) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php index 176a1491..80a633e3 100644 --- a/app/Template/user/profile.php +++ b/app/Template/user/profile.php @@ -1,8 +1,8 @@ <section id="main"> <br> <ul class="listing"> - <li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li> - <li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li> - <li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li> + <li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li> + <li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li> + <li><?= t('Email:') ?> <strong><?= $this->text->e($user['email']) ?: t('None') ?></strong></li> </ul> </section>
\ No newline at end of file diff --git a/app/Template/user/sessions.php b/app/Template/user/sessions.php index 7a66c5ad..d7fe895d 100644 --- a/app/Template/user/sessions.php +++ b/app/Template/user/sessions.php @@ -15,10 +15,10 @@ </tr> <?php foreach ($sessions as $session): ?> <tr> - <td><?= dt('%B %e, %Y at %k:%M %p', $session['date_creation']) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $session['expiration']) ?></td> - <td><?= $this->e($session['ip']) ?></td> - <td><?= $this->e($session['user_agent']) ?></td> + <td><?= $this->dt->datetime($session['date_creation']) ?></td> + <td><?= $this->dt->datetime($session['expiration']) ?></td> + <td><?= $this->text->e($session['ip']) ?></td> + <td><?= $this->text->e($session['user_agent']) ?></td> <td><?= $this->url->link(t('Remove'), 'User', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/user/show.php b/app/Template/user/show.php index 89c6b36b..df0affb8 100644 --- a/app/Template/user/show.php +++ b/app/Template/user/show.php @@ -2,9 +2,10 @@ <h2><?= t('Summary') ?></h2> </div> <ul class="listing"> - <li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li> - <li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li> - <li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li> + <li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li> + <li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li> + <li><?= t('Email:') ?> <strong><?= $this->text->e($user['email']) ?: t('None') ?></strong></li> + <li><?= t('Status:') ?> <strong><?= $user['is_active'] ? t('Active') : t('Inactive') ?></strong></li> </ul> <div class="page-header"> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 9f745568..20fd2ad2 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -1,9 +1,11 @@ <div class="sidebar"> <h2><?= t('Information') ?></h2> <ul> - <li <?= $this->app->checkMenuSelection('user', 'show') ?>> - <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?> - </li> + <?php if ($this->user->hasAccess('user', 'show')): ?> + <li <?= $this->app->checkMenuSelection('user', 'show') ?>> + <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php if ($this->user->isAdmin()): ?> <li> <?= $this->url->link(t('User dashboard'), 'app', 'index', array('user_id' => $user['id'])) ?> @@ -24,15 +26,18 @@ </li> <?php endif ?> - <?= $this->hook->render('template:user:sidebar:information') ?> + <?= $this->hook->render('template:user:sidebar:information', array('user' => $user)) ?> </ul> <h2><?= t('Actions') ?></h2> <ul> <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?> - <li <?= $this->app->checkMenuSelection('user', 'edit') ?>> - <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?> - </li> + + <?php if ($this->user->hasAccess('user', 'edit')): ?> + <li <?= $this->app->checkMenuSelection('user', 'edit') ?>> + <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> <?php if ($user['is_ldap_user'] == 0): ?> <li <?= $this->app->checkMenuSelection('user', 'password') ?>> @@ -71,13 +76,5 @@ <?php endif ?> <?= $this->hook->render('template:user:sidebar:actions', array('user' => $user)) ?> - - <?php if ($this->user->hasAccess('user', 'remove') && ! $this->user->isCurrentUser($user['id'])): ?> - <li <?= $this->app->checkMenuSelection('user', 'remove') ?>> - <?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?> - </li> - <?php endif ?> </ul> - <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> - <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> </div>
\ No newline at end of file diff --git a/app/Template/user/timesheet.php b/app/Template/user/timesheet.php index 5c0d3af8..92ebafb5 100644 --- a/app/Template/user/timesheet.php +++ b/app/Template/user/timesheet.php @@ -16,10 +16,10 @@ </tr> <?php foreach ($subtask_paginator->getCollection() as $record): ?> <tr> - <td><?= $this->url->link($this->e($record['task_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> - <td><?= $this->url->link($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td> - <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td> + <td><?= $this->url->link($this->text->e($record['task_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= $this->url->link($this->text->e($record['subtask_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= $this->dt->datetime($record['start']) ?></td> + <td><?= $this->dt->datetime($record['end']) ?></td> <td><?= n($record['time_spent']).' '.t('hours') ?></td> </tr> <?php endforeach ?> diff --git a/app/Template/user_import/step1.php b/app/Template/user_import/step1.php index 69643d6d..592587a3 100644 --- a/app/Template/user_import/step1.php +++ b/app/Template/user_import/step1.php @@ -26,7 +26,7 @@ <p class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></p> <div class="form-actions"> - <input type="submit" value="<?= t('Import') ?>" class="btn btn-blue"> + <button type="submit" class="btn btn-blue"><?= t('Import') ?></button> </div> </form> <div class="page-header"> diff --git a/app/Template/user_status/disable.php b/app/Template/user_status/disable.php new file mode 100644 index 00000000..90d8c757 --- /dev/null +++ b/app/Template/user_status/disable.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Disable user') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to disable this user: "%s"?', $user['name'] ?: $user['username']) ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'UserStatus', 'disable', array('user_id' => $user['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> + </div> +</div> diff --git a/app/Template/user_status/enable.php b/app/Template/user_status/enable.php new file mode 100644 index 00000000..cd3d4947 --- /dev/null +++ b/app/Template/user_status/enable.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Enable user') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to enable this user: "%s"?', $user['name'] ?: $user['username']) ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'UserStatus', 'enable', array('user_id' => $user['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> + </div> +</div> diff --git a/app/Template/user/remove.php b/app/Template/user_status/remove.php index 810a3a3f..cd5c09a6 100644 --- a/app/Template/user/remove.php +++ b/app/Template/user_status/remove.php @@ -6,8 +6,8 @@ <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['name'] ?: $user['username']) ?></p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'user', 'remove', array('user_id' => $user['id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'UserStatus', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + <?= $this->url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> </div> -</div>
\ No newline at end of file +</div> diff --git a/app/User/GithubUserProvider.php b/app/User/GithubUserProvider.php deleted file mode 100644 index ae3d7477..00000000 --- a/app/User/GithubUserProvider.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -namespace Kanboard\User; - -/** - * Github OAuth User Provider - * - * @package user - * @author Frederic Guillot - */ -class GithubUserProvider extends OAuthUserProvider -{ - /** - * Get external id column name - * - * @access public - * @return string - */ - public function getExternalIdColumn() - { - return 'github_id'; - } -} diff --git a/app/User/GitlabUserProvider.php b/app/User/GitlabUserProvider.php deleted file mode 100644 index a73472c8..00000000 --- a/app/User/GitlabUserProvider.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -namespace Kanboard\User; - -/** - * Gitlab OAuth User Provider - * - * @package user - * @author Frederic Guillot - */ -class GitlabUserProvider extends OAuthUserProvider -{ - /** - * Get external id column name - * - * @access public - * @return string - */ - public function getExternalIdColumn() - { - return 'gitlab_id'; - } -} diff --git a/app/User/GoogleUserProvider.php b/app/User/GoogleUserProvider.php deleted file mode 100644 index baa55e03..00000000 --- a/app/User/GoogleUserProvider.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -namespace Kanboard\User; - -/** - * Google OAuth User Provider - * - * @package user - * @author Frederic Guillot - */ -class GoogleUserProvider extends OAuthUserProvider -{ - /** - * Get external id column name - * - * @access public - * @return string - */ - public function getExternalIdColumn() - { - return 'google_id'; - } -} diff --git a/app/User/LdapUserProvider.php b/app/User/LdapUserProvider.php index 3a84bfea..153450d9 100644 --- a/app/User/LdapUserProvider.php +++ b/app/User/LdapUserProvider.php @@ -69,7 +69,7 @@ class LdapUserProvider implements UserProviderInterface * @param string $name * @param string $email * @param string $role - * @param string[] + * @param string[] $groupIds */ public function __construct($dn, $username, $name, $email, $role, array $groupIds) { diff --git a/app/Validator/AuthValidator.php b/app/Validator/AuthValidator.php index 36ccdff0..cd6e04d5 100644 --- a/app/Validator/AuthValidator.php +++ b/app/Validator/AuthValidator.php @@ -114,6 +114,6 @@ class AuthValidator extends Base } } - return array($result, $errors);; + return array($result, $errors); } } diff --git a/app/Validator/ColumnValidator.php b/app/Validator/ColumnValidator.php index 4c644e8a..f0f1659b 100644 --- a/app/Validator/ColumnValidator.php +++ b/app/Validator/ColumnValidator.php @@ -22,11 +22,12 @@ class ColumnValidator extends Base */ public function validateModification(array $values) { - $v = new Validator($values, array( - new Validators\Integer('task_limit', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); + $rules = array( + new Validators\Required('id', t('This value is required')), + new Validators\Integer('id', t('This value must be an integer')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -43,16 +44,32 @@ class ColumnValidator extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('project_id', t('The project id is required')), new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), $v->getErrors() ); } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('task_limit', t('This value must be an integer')), + new Validators\GreaterThan('task_limit', t('This value must be greater than %d', -1), -1), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), + ); + } } diff --git a/app/Validator/ExternalLinkValidator.php b/app/Validator/ExternalLinkValidator.php new file mode 100644 index 00000000..fff4133b --- /dev/null +++ b/app/Validator/ExternalLinkValidator.php @@ -0,0 +1,76 @@ +<?php + +namespace Kanboard\Validator; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * External Link Validator + * + * @package validator + * @author Frederic Guillot + */ +class ExternalLinkValidator extends Base +{ + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, $this->commonValidationRules()); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Required('url', t('Field required')), + new Validators\MaxLength('url', t('The maximum length is %d characters', 255), 255), + new Validators\Required('title', t('Field required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255), + new Validators\Required('link_type', t('Field required')), + new Validators\MaxLength('link_type', t('The maximum length is %d characters', 100), 100), + new Validators\Required('dependency', t('Field required')), + new Validators\MaxLength('dependency', t('The maximum length is %d characters', 100), 100), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('task_id', t('Field required')), + new Validators\Integer('task_id', t('This value must be an integer')), + ); + } +} diff --git a/app/Validator/TaskValidator.php b/app/Validator/TaskValidator.php index 7b73aeba..1a77dd32 100644 --- a/app/Validator/TaskValidator.php +++ b/app/Validator/TaskValidator.php @@ -40,8 +40,8 @@ class TaskValidator extends Base new Validators\Integer('priority', t('This value must be an integer')), new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50), - new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()), - new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getAllFormats()), + new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats(true)), + new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateTimeFormats(true)), new Validators\Numeric('time_spent', t('This value must be numeric')), new Validators\Numeric('time_estimated', t('This value must be numeric')), ); diff --git a/app/Validator/UserValidator.php b/app/Validator/UserValidator.php index d85d335f..e5953f30 100644 --- a/app/Validator/UserValidator.php +++ b/app/Validator/UserValidator.php @@ -17,10 +17,10 @@ class UserValidator extends Base /** * Common validation rules * - * @access private + * @access protected * @return array */ - private function commonValidationRules() + protected function commonValidationRules() { return array( new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25), diff --git a/app/check_setup.php b/app/check_setup.php index eec63ed8..af830de7 100644 --- a/app/check_setup.php +++ b/app/check_setup.php @@ -12,11 +12,6 @@ if (version_compare(PHP_VERSION, '5.4.0', '<')) { if (! ini_get('short_open_tag')) { throw new Exception('This software require to have short tags enabled if you have PHP < 5.4 ("short_open_tag = On")'); } - - // Magic quotes are deprecated since PHP 5.4 - if (get_magic_quotes_gpc()) { - throw new Exception('This software require to have "Magic quotes" disabled, it\'s deprecated since PHP 5.4 ("magic_quotes_gpc = Off")'); - } } // Check data folder if sqlite diff --git a/app/common.php b/app/common.php index fe287811..71f80c75 100644 --- a/app/common.php +++ b/app/common.php @@ -16,7 +16,9 @@ if (getenv('DATABASE_URL')) { if (file_exists('config.php')) { require 'config.php'; -} elseif (file_exists('data'.DIRECTORY_SEPARATOR.'config.php')) { +} + +if (file_exists('data'.DIRECTORY_SEPARATOR.'config.php')) { require 'data'.DIRECTORY_SEPARATOR.'config.php'; } @@ -24,6 +26,7 @@ require __DIR__.'/constants.php'; require __DIR__.'/check_setup.php'; $container = new Pimple\Container; +$container->register(new Kanboard\ServiceProvider\HelperProvider); $container->register(new Kanboard\ServiceProvider\SessionProvider); $container->register(new Kanboard\ServiceProvider\LoggingProvider); $container->register(new Kanboard\ServiceProvider\DatabaseProvider); @@ -34,4 +37,5 @@ $container->register(new Kanboard\ServiceProvider\EventDispatcherProvider); $container->register(new Kanboard\ServiceProvider\GroupProvider); $container->register(new Kanboard\ServiceProvider\RouteProvider); $container->register(new Kanboard\ServiceProvider\ActionProvider); +$container->register(new Kanboard\ServiceProvider\ExternalLinkProvider); $container->register(new Kanboard\ServiceProvider\PluginProvider); diff --git a/app/constants.php b/app/constants.php index da3de840..4201e6e4 100644 --- a/app/constants.php +++ b/app/constants.php @@ -1,8 +1,8 @@ <?php // Enable/disable debug -defined('DEBUG') or define('DEBUG', false); -defined('DEBUG_FILE') or define('DEBUG_FILE', __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); +defined('DEBUG') or define('DEBUG', getenv('DEBUG')); +defined('DEBUG_FILE') or define('DEBUG_FILE', getenv('DEBUG_FILE') ?: __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); // Plugin directory defined('PLUGINS_DIR') or define('PLUGINS_DIR', __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'); @@ -23,6 +23,9 @@ defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost'); defined('DB_NAME') or define('DB_NAME', 'kanboard'); defined('DB_PORT') or define('DB_PORT', null); +// Database backend group provider +defined('DB_GROUP_PROVIDER') or define('DB_GROUP_PROVIDER', true); + // LDAP configuration defined('LDAP_AUTH') or define('LDAP_AUTH', false); defined('LDAP_SERVER') or define('LDAP_SERVER', ''); @@ -51,27 +54,6 @@ defined('LDAP_GROUP_BASE_DN') or define('LDAP_GROUP_BASE_DN', ''); defined('LDAP_GROUP_FILTER') or define('LDAP_GROUP_FILTER', ''); defined('LDAP_GROUP_ATTRIBUTE_NAME') or define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); -// Google authentication -defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false); -defined('GOOGLE_CLIENT_ID') or define('GOOGLE_CLIENT_ID', ''); -defined('GOOGLE_CLIENT_SECRET') or define('GOOGLE_CLIENT_SECRET', ''); - -// Github authentication -defined('GITHUB_AUTH') or define('GITHUB_AUTH', false); -defined('GITHUB_CLIENT_ID') or define('GITHUB_CLIENT_ID', ''); -defined('GITHUB_CLIENT_SECRET') or define('GITHUB_CLIENT_SECRET', ''); -defined('GITHUB_OAUTH_AUTHORIZE_URL') or define('GITHUB_OAUTH_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize'); -defined('GITHUB_OAUTH_TOKEN_URL') or define('GITHUB_OAUTH_TOKEN_URL', 'https://github.com/login/oauth/access_token'); -defined('GITHUB_API_URL') or define('GITHUB_API_URL', 'https://api.github.com/'); - -// Gitlab authentication -defined('GITLAB_AUTH') or define('GITLAB_AUTH', false); -defined('GITLAB_CLIENT_ID') or define('GITLAB_CLIENT_ID', ''); -defined('GITLAB_CLIENT_SECRET') or define('GITLAB_CLIENT_SECRET', ''); -defined('GITLAB_OAUTH_AUTHORIZE_URL') or define('GITLAB_OAUTH_AUTHORIZE_URL', 'https://gitlab.com/oauth/authorize'); -defined('GITLAB_OAUTH_TOKEN_URL') or define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token'); -defined('GITLAB_API_URL') or define('GITLAB_API_URL', 'https://gitlab.com/api/v3/'); - // Proxy authentication defined('REVERSE_PROXY_AUTH') or define('REVERSE_PROXY_AUTH', false); defined('REVERSE_PROXY_USER_HEADER') or define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); @@ -98,7 +80,7 @@ defined('ENABLE_HSTS') or define('ENABLE_HSTS', true); defined('ENABLE_XFRAME') or define('ENABLE_XFRAME', true); // Syslog -defined('ENABLE_SYSLOG') or define('ENABLE_SYSLOG', true); +defined('ENABLE_SYSLOG') or define('ENABLE_SYSLOG', getenv('ENABLE_SYSLOG')); // Default files directory defined('FILES_DIR') or define('FILES_DIR', 'data'.DIRECTORY_SEPARATOR.'files'); @@ -115,6 +97,9 @@ defined('ENABLE_URL_REWRITE') or define('ENABLE_URL_REWRITE', isset($_SERVER['HT // Hide login form defined('HIDE_LOGIN_FORM') or define('HIDE_LOGIN_FORM', false); +// Disabling logout (for external SSO authentication) +defined('DISABLE_LOGOUT') or define('DISABLE_LOGOUT', false); + // Bruteforce protection defined('BRUTEFORCE_CAPTCHA') or define('BRUTEFORCE_CAPTCHA', 3); defined('BRUTEFORCE_LOCKDOWN') or define('BRUTEFORCE_LOCKDOWN', 6); diff --git a/app/functions.php b/app/functions.php index fe6e6757..0c611f95 100644 --- a/app/functions.php +++ b/app/functions.php @@ -25,19 +25,10 @@ function e() /** * Translate a number * + * @param mixed $value * @return string */ function n($value) { return Translator::getInstance()->number($value); } - -/** - * Translate a date - * - * @return string - */ -function dt($format, $timestamp) -{ - return Translator::getInstance()->datetime($format, $timestamp); -} diff --git a/assets/css/app.css b/assets/css/app.css index c2f41142..9bdb5512 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -18,4 +18,4 @@ * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} -.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{list-style-type:none;margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.chosen-select{min-height:27px}.avatar{float:left;margin-right:10px}#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{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h2{font-size:1.3em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd) td{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{width:70%}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]{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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input.form-numeric,input[type="number"]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-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:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:0}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0;margin-right:15px}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-datetime,input.form-date{width:150px}input.form-input-large{width:400px}.form-row{margin-top:10px;margin-bottom:20px}.form-column{float:left;margin-right:3%;max-width:47%}.form-column ul{margin-top:15px}.form-login{width:350px;margin:0 auto;margin-top:8%}.form-column li,.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:bold}label+.form-tabs{margin-top:10px}.form-tabs{width:100%;max-width:800px}ul.form-tabs-nav{margin-bottom:8px;margin-top:0}.form-tabs-nav li{margin-left:0;display:inline}.form-tab{margin-right:20px}.form-tab a{color:#ccc;font-weight:bold;text-decoration:none}.form-tab a:focus,.form-tab a:hover{color:#000}.form-tab-selected a{color:#333}.preview-area{border:1px dashed #000;padding-top:5px;padding-left:5px;padding-right:5px;margin-bottom:5px;display:none;overflow:auto}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{-webkit-appearance:none;appearance:none;display:inline-block;color:#333;border:1px solid #ccc;background:#efefef;padding:5px;padding-left:15px;padding-right:15px;font-size:.9em;cursor:pointer;border-radius:2px}a.btn{text-decoration:none;font-weight:bold}.btn-small{padding:2px;padding-left:5px;padding-right:5px}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}a.btn-red:hover,.btn-red:hover,.btn-red:focus{color:#fff;background:#c53727}a.btn-blue,.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}a.btn-blue:hover,.btn-blue:hover,a.btn-blue:focus,.btn-blue:focus{border-color:#2f5bb7;background:#357ae8}.btn-blue:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}#main .alert,.page .alert{margin-top:10px}.alert{padding:8px 35px 8px 14px;margin-bottom:10px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:550px}.ui-tooltip-content .markdown p{margin-bottom:0}.tooltip .fa-info-circle{color:#999;font-size:.95em}.ui-tooltip ul{margin-left:20px}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#777;text-decoration:none}nav .active a{color:#333;font-weight:bold}.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 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:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#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:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}@media only screen and (max-width:640px){.page-header-mobile li{display:block;margin-bottom:5px}}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size: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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#df5353}.draggable-item{cursor:pointer;user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{box-shadow:2px 2px 3px rgba(0,0,0,0.2)}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-table a,.task-board a{color:#000;text-decoration:none;font-weight:bold}.task-table a:focus,.task-table a:hover,.task-board a:focus,.task-board a:hover{text-decoration:underline}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}a.task-board-collapsed-title{font-weight:normal}.task-board .dropdown{font-size:1.1em}.task-board-title{margin-top:5px;margin-bottom:5px;font-size:1.1em}.task-board-title a{font-weight:normal}.task-board-user{font-size:.8em}.task-board-current-user a{text-decoration:underline}.task-board-current-user a:focus,.task-board-current-user a:hover{text-decoration:none}a.task-board-nobody{font-weight:normal;font-style:italic;color:#444}.task-board-category-container{text-align:right}.task-board-category{font-weight:bold;font-size:.9em;color:#000;border:1px solid #555;padding:2px;padding-right:5px;padding-left:5px}.task-board-icons{text-align:right;margin-top:8px}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-score{font-weight:bold}.task-board .task-score{font-size:1.1em}.task-show-details .task-score{position:absolute;bottom:5px;right:5px;font-size:2em}.task-board-closed,.task-board-days{position:absolute;right:5px;top:5px;opacity:.5;font-size:.8em}.task-board-days:hover{opacity:1.0}.task-days-age{border:#666 1px solid;padding:1px 4px 1px 2px;border-top-left-radius:3px;border-bottom-left-radius:3px}.task-days-incolumn{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 2px 1px 4px;border-top-right-radius:3px;border-bottom-right-radius:3px}.board-container-compact .task-board-days{display:none}.task-show-details{position:relative;border-radius:5px;padding-bottom:10px}.task-show-details h2{font-size:1.8em;margin:0;margin-bottom:25px;padding:0;padding-left:10px;padding-right:10px}.task-show-details li{margin-left:25px;list-style-type:circle}.task-show-section{margin-top:30px;margin-bottom:20px}.task-show-files a{font-weight:bold;text-decoration:none}.task-show-files li{margin-left:25px;list-style-type:square;line-height:25px}.task-show-file-actions{font-size:.75em}.task-show-file-actions:before{content:" ["}.task-show-file-actions:after{content:"]"}.task-show-file-actions a{color:#333}.task-show-description{border-left:4px solid #333;padding-left:20px}.task-show-description-textarea{width:99%;max-width:99%;height:300px}.task-file-viewer{position:relative}.task-file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.task-time-form{margin-top:10px;margin-bottom:25px;padding:3px}.task-link-closed{text-decoration:line-through}.task-show-images{list-style-type:none}.task-show-images li img{width:100%}.task-show-images li .img_container{width:250px;height:100px;overflow:hidden}.task-show-images li{padding:10px;overflow:auto;width:250px;min-height:120px;display:inline-block;vertical-align:top}.task-show-images li p{padding:5px;font-weight:bold}.task-show-images li:hover{background:#eee}.task-show-image-actions{margin-left:5px}.task-show-file-table{width:auto}.task-show-start-link{color:#000}.task-show-start-link:hover,.task-show-start-link:focus{color:red}.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;cursor:pointer}.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,0.9)}.comment{margin-bottom:20px}.comment:hover{background:#f7f8e0}.comment-inner{border-left:4px solid #333;padding-bottom:10px;padding-left:20px;margin-left:20px;margin-right:10px}.comment-preview{border:2px solid #000;border-radius:3px;padding:10px}.comment-preview .comment-inner{border:0;padding:0;margin:0}.comment-title{margin-bottom:8px;padding-bottom:3px;border-bottom:1px dotted #aaa}.ui-tooltip .comment-title{font-size:80%}.ui-tooltip .comment-inner{padding-bottom:0}.comment-actions{font-size:.8em;padding:0;text-align:right}.comment-actions li{display:inline;padding-left:5px;padding-right:5px;border-right:1px dotted #000}.comment-actions li:last-child{padding-right:0;border:0}.comment-username{font-weight:bold}.comment-textarea{height:200px;width:80%;max-width:800px}.comment-sorting{font-size:.5em}span.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}span.comment-sorting a:hover{color:#aaa}#comments .comment-textarea{height:80px;width:500px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.markdown{line-height:1.4em;font-size:1.0}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;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:bold;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:#fefefe;overflow:auto}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:20px}.activity-datetime{color:#999;font-size:.85em}.activity-content{margin-top:10px;margin-left:20px;padding-left:20px;border-left:2px solid #666}.activity-title{font-weight:bold;color:#000}.activity-description{font-size:.9em;color:#aaa;padding-top:5px}.activity-description ul{margin-top:10px}.activity-description li{margin-left:40px;list-style-type:circle;color:#555}.activity-description .markdown{margin-top:10px;color:#555}.activity-changes{margin-top:10px;font-size:.85em}.activity-changes ul{margin-left:25px}.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:bold;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination{text-align:center}.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,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;margin:0 0 0 -35%;left:50%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:85%}#main .confirm{max-width:700px;font-size:1.1em}.sidebar-container{margin-top:10px;position:relative;clear:both}.sidebar-content{margin-left:23%;width:76%;position:absolute}.sidebar{width:20%;float:left;padding:10px;padding-top:0;border:1px solid #ddd;background:#fdfdfd;border-radius:5px}.sidebar li{list-style-type:square;margin-left:30px;line-height:1.8em}.sidebar li.active a{color:#000;font-weight:bold;text-decoration:none}.sidebar li.active a:focus,.sidebar li.active a:hover{text-decoration:underline}.sidebar-collapsed .sidebar{width:10px;padding-bottom:0;float:none}.sidebar-collapsed .sidebar-content{margin:0;margin-top:15px;width:100%}.sidebar-collapse{text-align:right}.sidebar-collapse a,.sidebar-expand a{color:#333;text-decoration:none}.sidebar-collapse a:hover,.sidebar-expand a:hover{color:#df5353}@media only screen and (max-width:1024px){.sidebar{width:25%}.sidebar-content{margin-left:30%;width:70%}}@media only screen and (max-width:767px){.sidebar{width:95%;float:none}.sidebar-content{margin:0;margin-top:15px;width:100%}}@media only screen and (max-width:1080px){div.filter-dropdowns .filters{margin-left:0}div.filter-dropdowns{display:block;margin-top:5px}}@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}.task-time-form label{display:block}.task-time-form input[type="submit"]{margin-top:10px;display:block}.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;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.textarea-dropdown li,.dropdown-submenu-open li{display:block;margin:0;padding:0;padding-left:10px;padding-right:10px;padding-top:8px;padding-bottom:8px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.textarea-dropdown li:last-child,.dropdown-submenu-open li:last-child{border:0}.textarea-dropdown .active,.textarea-dropdown li:hover,.dropdown-submenu-open li:not(.no-hover):hover{background:#4078c0;color:#fff}.textarea-dropdown .active a,.textarea-dropdown li:hover a,.dropdown-submenu-open li:hover a{color:#fff}.textarea-dropdown a,.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon{color:#333;text-decoration:none}.textarea-dropdown{list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}#screenshot-zone{position:relative;border:2px dashed #ccc;width:90%;height:250px;overflow:auto}#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center}#screenshot-zone.screenshot-pasted{border:2px solid #333}.toolbar{font-size:.9em;padding-top:5px}.views{display:inline-block;margin-right:10px;font-size:.9em}.views li{border:1px solid #eee;padding-left:8px;padding-right:8px;padding-top:5px;padding-bottom:5px;display:inline}.menu-inline li.active a,.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-right:0;border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-left:0;border-top-right-radius:5px;border-bottom-right-radius:5px}.filters{display:inline-block;border:1px solid #eee;border-radius:5px;padding:5px;padding-right:10px;margin-left:8px}.filters ul{font-size:.8em}.page-header .filters ul{font-size:.9em}form.search{display:inline}div.search{margin-bottom:20px}.filter-dropdowns{font-size:.9em;display:inline-block}div.ganttview-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:0}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;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}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}
\ No newline at end of file +.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{list-style-type:none;margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.chosen-select{min-height:27px}.avatar{float:left;margin-right:10px}#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}.smaller{font-size:.85em}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h2{font-size:1.3em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]{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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input.form-numeric,input[type="number"]{width:70px}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-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:bold}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:0}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0;margin-right:15px}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-datetime,input.form-date{width:150px}input.form-input-large{width:400px}.form-column{float:left;margin-right:3%;max-width:47%}.form-column ul{margin-top:15px}.form-clear{clear:both;padding-top:20px;padding-bottom:10px}.form-login{width:350px;margin:0 auto;margin-top:8%}.form-column li,.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:bold}.popover-form{margin-bottom:0}label+.form-tabs{margin-top:10px}.form-tabs{width:100%;max-width:800px}ul.form-tabs-nav{margin-bottom:8px;margin-top:0}.form-tabs-nav li{margin-left:0;display:inline}.form-tab{margin-right:20px}.form-tab a{color:#ccc;font-weight:bold;text-decoration:none}.form-tab a:focus,.form-tab a:hover{color:#000}.form-tab-selected a{color:#333}.preview-area{border:1px dashed #000;padding-top:5px;padding-left:5px;padding-right:5px;margin-bottom:5px;display:none;overflow:auto}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{-webkit-appearance:none;appearance:none;display:inline-block;color:#333;border:1px solid #ccc;background:#efefef;padding:5px;padding-left:15px;padding-right:15px;font-size:.9em;cursor:pointer;border-radius:2px}a.btn{text-decoration:none;font-weight:bold}.btn-small{padding:2px;padding-left:5px;padding-right:5px}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}a.btn-red:hover,.btn-red:hover,.btn-red:focus{color:#fff;background:#c53727}a.btn-blue,.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}a.btn-blue:hover,.btn-blue:hover,a.btn-blue:focus,.btn-blue:focus{border-color:#2f5bb7;background:#357ae8}.btn-blue:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}#main .alert,.page .alert{margin-top:10px}.alert{padding:8px 35px 8px 14px;margin-bottom:10px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.alert-fade-out{position:fixed;bottom:0;left:0;width:100%;font-size:1.1em;padding-top:15px;padding-bottom:15px;margin-bottom:0;border-width:1px 0 0;border-radius:0;z-index:9999}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:550px}.ui-tooltip-content .markdown p{margin-bottom:0}.tooltip .fa-info-circle{color:#999;font-size:.95em}.ui-tooltip ul{margin-left:20px}.ui-tooltip dl{margin:-5px 0 0 0;padding:0}.ui-tooltip dt{margin-top:5px}.ui-tooltip dd{margin-left:0}.ui-tooltip .progress{display:inline-block;min-width:3em;text-align:right}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#777;text-decoration:none}nav .active a{color:#333;font-weight:bold}.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:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#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:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}@media only screen and (max-width:640px){.page-header-mobile li{display:block;margin-bottom:5px}}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size: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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}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-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;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-table a,.task-board a{color:#000;text-decoration:none;font-weight:bold}.task-table a:focus,.task-table a:hover,.task-board a:focus,.task-board a:hover{text-decoration:underline}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}a.task-board-collapsed-title{font-weight:normal}.task-board .dropdown{font-size:1.1em}.task-board-title{margin-top:5px;margin-bottom:5px;font-size:1.1em}.task-board-title a{font-weight:normal}.task-board-user{font-size:.8em}.task-board-current-user a{text-decoration:underline}.task-board-current-user a:focus,.task-board-current-user a:hover{text-decoration:none}a.task-board-nobody{font-weight:normal;font-style:italic;color:#444}.task-board-category-container{text-align:right}.task-board-category{font-weight:bold;font-size:.9em;color:#000;border:1px solid #555;padding:2px;padding-right:5px;padding-left:5px}.task-board-icons{text-align:right;margin-top:8px}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold;font-size:1.1em}.task-board-closed,.task-board-days{position:absolute;right:5px;top:5px;opacity:.5;font-size:.8em}.task-board-days:hover{opacity:1.0}.task-days-age{border:#666 1px solid;padding:1px 4px 1px 2px;border-top-left-radius:3px;border-bottom-left-radius:3px}.task-days-incolumn{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 2px 1px 4px;border-top-right-radius:3px;border-bottom-right-radius:3px}.board-container-compact .task-board-days{display:none}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.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}.task-show-description{border-left:4px solid #333;padding-left:20px}.task-show-description-textarea{width:99%;max-width:99%;height:300px}.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;cursor:pointer}.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,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment{margin-bottom:20px}.comment:hover{background:#f7f8e0}.comment-inner{border-left:4px solid #333;padding-bottom:10px;padding-left:20px;margin-left:20px;margin-right:10px}.comment-preview{border:2px solid #000;border-radius:3px;padding:10px}.comment-preview .comment-inner{border:0;padding:0;margin:0}.comment-title{margin-bottom:8px;padding-bottom:3px;border-bottom:1px dotted #aaa}.ui-tooltip .comment-title{font-size:80%}.ui-tooltip .comment-inner{padding-bottom:0}.comment-actions{font-size:.8em;padding:0;text-align:right}.comment-actions li{display:inline;padding-left:5px;padding-right:5px;border-right:1px dotted #000}.comment-actions li:last-child{padding-right:0;border:0}.comment-username{font-weight:bold}.comment-textarea{height:200px;width:80%;max-width:800px}.comment-sorting{font-size:.5em}span.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}span.comment-sorting a:hover{color:#aaa}#comments .comment-textarea{height:80px;width:500px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.markdown{line-height:1.4em;font-size:1.0}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;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:bold;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}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:20px}.activity-datetime{color:#999;font-size:.85em}.activity-content{margin-top:10px;margin-left:20px;padding-left:20px;border-left:2px solid #666}.activity-title{font-weight:bold;color:#000}.activity-description{font-size:.9em;color:#aaa;padding-top:5px}.activity-description ul{margin-top:10px}.activity-description li{margin-left:40px;list-style-type:circle;color:#555}.activity-description .markdown{margin-top:10px;color:#555}.activity-changes{margin-top:10px;font-size:.85em}.activity-changes ul{margin-left:25px}.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:bold;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination{text-align:center}.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,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;margin:0 0 0 -35%;left:50%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:85%}#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 a{text-decoration:none}.sidebar li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar li:hover{border-left:5px solid #555;padding-left:8px}.sidebar li.active{border-left:5px solid #333;padding-left:8px}.sidebar li.active a{color:#333;font-weight:bold}.sidebar li.active a:focus,.sidebar 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;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.textarea-dropdown li,.dropdown-submenu-open li{display:block;margin:0;padding:0;padding-left:10px;padding-right:10px;padding-top:8px;padding-bottom:8px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.textarea-dropdown li:last-child,.dropdown-submenu-open li:last-child{border:0}.textarea-dropdown .active,.textarea-dropdown li:hover,.dropdown-submenu-open li:not(.no-hover):hover{background:#4078c0;color:#fff}.textarea-dropdown .active a,.textarea-dropdown li:hover a,.dropdown-submenu-open li:hover a{color:#fff}.textarea-dropdown a,.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:bold;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.project-header .filter-box{margin:0}.filter-box form{margin:0}.filter-box input[type="text"]{margin:0;font-size:16px;height:26px;border-color:#dedede;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,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}.filter-box div.dropdown{display:inline-block;font-size:16px;border:1px solid #dedede;border-left:0;margin:0;padding:0;padding-left:5px;padding-right:8px;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-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:0}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;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}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{text-align:center;margin-right:80px}.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,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:.5}.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}.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{border:1px solid #eee;padding-left:8px;padding-right:8px;padding-top:5px;padding-bottom:5px;display:inline}.menu-inline li.active a,.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-right:0;border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-left:0;border-top-right-radius:5px;border-bottom-right-radius:5px}
\ No newline at end of file diff --git a/assets/css/print.css b/assets/css/print.css index 0222b865..3e945352 100644 --- a/assets/css/print.css +++ b/assets/css/print.css @@ -18,4 +18,4 @@ * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} -.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}header,.sidebar,.form-comment,.page-header{display:none}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd) td{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{width:70%}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size: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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#df5353}.draggable-item{cursor:pointer;user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board-saving-state{opacity:.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{box-shadow:2px 2px 3px rgba(0,0,0,0.2)}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-table a,.task-board a{color:#000;text-decoration:none;font-weight:bold}.task-table a:focus,.task-table a:hover,.task-board a:focus,.task-board a:hover{text-decoration:underline}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}a.task-board-collapsed-title{font-weight:normal}.task-board .dropdown{font-size:1.1em}.task-board-title{margin-top:5px;margin-bottom:5px;font-size:1.1em}.task-board-title a{font-weight:normal}.task-board-user{font-size:.8em}.task-board-current-user a{text-decoration:underline}.task-board-current-user a:focus,.task-board-current-user a:hover{text-decoration:none}a.task-board-nobody{font-weight:normal;font-style:italic;color:#444}.task-board-category-container{text-align:right}.task-board-category{font-weight:bold;font-size:.9em;color:#000;border:1px solid #555;padding:2px;padding-right:5px;padding-left:5px}.task-board-icons{text-align:right;margin-top:8px}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-score{font-weight:bold}.task-board .task-score{font-size:1.1em}.task-show-details .task-score{position:absolute;bottom:5px;right:5px;font-size:2em}.task-board-closed,.task-board-days{position:absolute;right:5px;top:5px;opacity:.5;font-size:.8em}.task-board-days:hover{opacity:1.0}.task-days-age{border:#666 1px solid;padding:1px 4px 1px 2px;border-top-left-radius:3px;border-bottom-left-radius:3px}.task-days-incolumn{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 2px 1px 4px;border-top-right-radius:3px;border-bottom-right-radius:3px}.board-container-compact .task-board-days{display:none}.task-show-details{position:relative;border-radius:5px;padding-bottom:10px}.task-show-details h2{font-size:1.8em;margin:0;margin-bottom:25px;padding:0;padding-left:10px;padding-right:10px}.task-show-details li{margin-left:25px;list-style-type:circle}.task-show-section{margin-top:30px;margin-bottom:20px}.task-show-files a{font-weight:bold;text-decoration:none}.task-show-files li{margin-left:25px;list-style-type:square;line-height:25px}.task-show-file-actions{font-size:.75em}.task-show-file-actions:before{content:" ["}.task-show-file-actions:after{content:"]"}.task-show-file-actions a{color:#333}.task-show-description{border-left:4px solid #333;padding-left:20px}.task-show-description-textarea{width:99%;max-width:99%;height:300px}.task-file-viewer{position:relative}.task-file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.task-time-form{margin-top:10px;margin-bottom:25px;padding:3px}.task-link-closed{text-decoration:line-through}.task-show-images{list-style-type:none}.task-show-images li img{width:100%}.task-show-images li .img_container{width:250px;height:100px;overflow:hidden}.task-show-images li{padding:10px;overflow:auto;width:250px;min-height:120px;display:inline-block;vertical-align:top}.task-show-images li p{padding:5px;font-weight:bold}.task-show-images li:hover{background:#eee}.task-show-image-actions{margin-left:5px}.task-show-file-table{width:auto}.task-show-start-link{color:#000}.task-show-start-link:hover,.task-show-start-link:focus{color:red}.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;cursor:pointer}.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,0.9)}.comment{margin-bottom:20px}.comment:hover{background:#f7f8e0}.comment-inner{border-left:4px solid #333;padding-bottom:10px;padding-left:20px;margin-left:20px;margin-right:10px}.comment-preview{border:2px solid #000;border-radius:3px;padding:10px}.comment-preview .comment-inner{border:0;padding:0;margin:0}.comment-title{margin-bottom:8px;padding-bottom:3px;border-bottom:1px dotted #aaa}.ui-tooltip .comment-title{font-size:80%}.ui-tooltip .comment-inner{padding-bottom:0}.comment-actions{font-size:.8em;padding:0;text-align:right}.comment-actions li{display:inline;padding-left:5px;padding-right:5px;border-right:1px dotted #000}.comment-actions li:last-child{padding-right:0;border:0}.comment-username{font-weight:bold}.comment-textarea{height:200px;width:80%;max-width:800px}.comment-sorting{font-size:.5em}span.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}span.comment-sorting a:hover{color:#aaa}#comments .comment-textarea{height:80px;width:500px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.markdown{line-height:1.4em;font-size:1.0}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;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:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
\ No newline at end of file +.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}header,.sidebar,.form-comment,.page-header{display:none}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}#calendar table{margin-bottom:0}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:0;border-bottom:0}tr.draggable-item-selected td:first-child{border-left:0}tr.draggable-item-selected td:last-child{border-right:0}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#fefff2}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size: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:normal}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:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:0}.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}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-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;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-table a,.task-board a{color:#000;text-decoration:none;font-weight:bold}.task-table a:focus,.task-table a:hover,.task-board a:focus,.task-board a:hover{text-decoration:underline}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}a.task-board-collapsed-title{font-weight:normal}.task-board .dropdown{font-size:1.1em}.task-board-title{margin-top:5px;margin-bottom:5px;font-size:1.1em}.task-board-title a{font-weight:normal}.task-board-user{font-size:.8em}.task-board-current-user a{text-decoration:underline}.task-board-current-user a:focus,.task-board-current-user a:hover{text-decoration:none}a.task-board-nobody{font-weight:normal;font-style:italic;color:#444}.task-board-category-container{text-align:right}.task-board-category{font-weight:bold;font-size:.9em;color:#000;border:1px solid #555;padding:2px;padding-right:5px;padding-left:5px}.task-board-icons{text-align:right;margin-top:8px}.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.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-board .task-score{font-weight:bold;font-size:1.1em}.task-board-closed,.task-board-days{position:absolute;right:5px;top:5px;opacity:.5;font-size:.8em}.task-board-days:hover{opacity:1.0}.task-days-age{border:#666 1px solid;padding:1px 4px 1px 2px;border-top-left-radius:3px;border-bottom-left-radius:3px}.task-days-incolumn{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 2px 1px 4px;border-top-right-radius:3px;border-bottom-right-radius:3px}.board-container-compact .task-board-days{display:none}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.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}.task-show-description{border-left:4px solid #333;padding-left:20px}.task-show-description-textarea{width:99%;max-width:99%;height:300px}.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;cursor:pointer}.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,0.9)}.assign-me{font-size:.8em;vertical-align:bottom}.comment{margin-bottom:20px}.comment:hover{background:#f7f8e0}.comment-inner{border-left:4px solid #333;padding-bottom:10px;padding-left:20px;margin-left:20px;margin-right:10px}.comment-preview{border:2px solid #000;border-radius:3px;padding:10px}.comment-preview .comment-inner{border:0;padding:0;margin:0}.comment-title{margin-bottom:8px;padding-bottom:3px;border-bottom:1px dotted #aaa}.ui-tooltip .comment-title{font-size:80%}.ui-tooltip .comment-inner{padding-bottom:0}.comment-actions{font-size:.8em;padding:0;text-align:right}.comment-actions li{display:inline;padding-left:5px;padding-right:5px;border-right:1px dotted #000}.comment-actions li:last-child{padding-right:0;border:0}.comment-username{font-weight:bold}.comment-textarea{height:200px;width:80%;max-width:800px}.comment-sorting{font-size:.5em}span.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}span.comment-sorting a:hover{color:#aaa}#comments .comment-textarea{height:80px;width:500px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.task-links-table{font-size:.85em}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.markdown{line-height:1.4em;font-size:1.0}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;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:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
\ No newline at end of file diff --git a/assets/css/src/alert.css b/assets/css/src/alert.css index 0a5a35ee..2cde8220 100644 --- a/assets/css/src/alert.css +++ b/assets/css/src/alert.css @@ -45,3 +45,17 @@ .alert li { margin-left: 25px; } + +.alert-fade-out { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + font-size: 1.1em; + padding-top: 15px; + padding-bottom: 15px; + margin-bottom: 0; + border-width: 1px 0 0; + border-radius: 0; + z-index: 9999; +} diff --git a/assets/css/src/base.css b/assets/css/src/base.css index dd78ec84..7f5642e7 100644 --- a/assets/css/src/base.css +++ b/assets/css/src/base.css @@ -70,3 +70,7 @@ hr { .web-notification-icon:hover { color: #000; } + +.smaller { + font-size: 0.85em; +} diff --git a/assets/css/src/board.css b/assets/css/src/board.css index 9fd9a38d..586093b8 100644 --- a/assets/css/src/board.css +++ b/assets/css/src/board.css @@ -138,6 +138,8 @@ a.board-swimlane-toggle:focus { .draggable-item { cursor: pointer; user-select: none; + -webkit-user-select: none; + -moz-user-select: none; } .draggable-placeholder { diff --git a/assets/css/src/dropdown.css b/assets/css/src/dropdown.css index 8f13f5bf..7d967b06 100644 --- a/assets/css/src/dropdown.css +++ b/assets/css/src/dropdown.css @@ -71,11 +71,16 @@ ul.dropdown-submenu-open { padding-right: 10px; } +.dropdown-menu-link-text, .dropdown-menu-link-icon { color: #333; text-decoration: none; } +.dropdown-menu-link-text:hover { + text-decoration: underline; +} + /* textarea dropdown */ .textarea-dropdown { list-style: none; diff --git a/assets/css/src/files.css b/assets/css/src/files.css new file mode 100644 index 00000000..a81b387b --- /dev/null +++ b/assets/css/src/files.css @@ -0,0 +1,56 @@ +.file-thumbnails { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-justify-content: flex-start; + justify-content: flex-start; +} + +.file-thumbnail { + width: 250px; + border: 1px solid #efefef; + border-radius: 5px; + margin-bottom: 20px; + box-shadow: 4px 2px 10px -6px rgba(0,0,0,0.55); + margin-right: 15px; +} + +.file-thumbnail img { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.file-thumbnail img:hover { + opacity: 0.5; +} + +.file-thumbnail-content { + padding-left: 8px; + padding-right: 8px; +} + +.file-thumbnail-title { + font-weight: 700; + font-size: 0.9em; + color: #555; +} + +.file-thumbnail-description { + font-size: 0.8em; + color: #aaa; + margin-top: 8px; + margin-bottom: 5px; +} + +.file-viewer { + position: relative; +} + +.file-viewer img { + max-width: 95%; + max-height: 85%; + margin-top: 10px; +} diff --git a/assets/css/src/filters.css b/assets/css/src/filters.css index 8f889556..0e0a35e7 100644 --- a/assets/css/src/filters.css +++ b/assets/css/src/filters.css @@ -1,68 +1,57 @@ -.toolbar { - font-size: 0.9em; - padding-top: 5px; +.project-header { + margin-top: 8px; + margin-bottom: 20px; } -.views { +.filter-box { display: inline-block; - margin-right: 10px; - font-size: 0.9em; + position: relative; + font-size: 0; + margin-bottom: 20px; } -.views li { - border: 1px solid #eee; - padding-left: 8px; - padding-right: 8px; - padding-top: 5px; - padding-bottom: 5px; - display: inline; +.project-header .filter-box { + margin: 0; } -.menu-inline li.active a, -.views li.active a { - font-weight: bold; - color: #000; - text-decoration: none; +.filter-box form { + margin: 0; } -.views li:first-child { - border-right: none; +.filter-box input[type="text"] { + margin: 0; + font-size: 16px; + height: 26px; + border-color: #dedede; border-top-left-radius: 5px; border-bottom-left-radius: 5px; + vertical-align: top; } -.views li:last-child { - border-left: none; - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; +.filter-box input[type="text"]:focus { + color: #000; + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + box-shadow: 0 0 8px rgba(82, 168, 236, 0.6); } -.filters { +.filter-box div.dropdown { display: inline-block; - border: 1px solid #eee; - border-radius: 5px; - padding: 5px; - padding-right: 10px; - margin-left: 8px; -} - -.filters ul { - font-size: 0.8em; -} - -.page-header .filters ul { - font-size: 0.9em; -} - -form.search { - display: inline; + font-size: 16px; + border: 1px solid #dedede; + border-left: none; + margin: 0; + padding: 0; + padding-left: 5px; + padding-right: 8px; + height: 27px; } -div.search { - margin-bottom: 20px; +.filter-box div.dropdown:last-child { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; } -.filter-dropdowns { - font-size: 0.9em; - display: inline-block; +.filter-box div.dropdown a { + line-height: 27px; } diff --git a/assets/css/src/form.css b/assets/css/src/form.css index 24dcb0fc..22dcb412 100644 --- a/assets/css/src/form.css +++ b/assets/css/src/form.css @@ -146,11 +146,6 @@ input.form-input-large { width: 400px; } -.form-row { - margin-top: 10px; - margin-bottom: 20px; -} - .form-column { float: left; margin-right: 3%; @@ -161,6 +156,12 @@ input.form-input-large { margin-top: 15px; } +.form-clear { + clear: both; + padding-top: 20px; + padding-bottom: 10px; +} + .form-login { width: 350px; margin: 0 auto; @@ -179,6 +180,10 @@ input.form-input-large { font-weight: bold; } +.popover-form { + margin-bottom: 0; +} + /* preview tabs */ label + .form-tabs { margin-top: 10px; diff --git a/assets/css/src/header.css b/assets/css/src/header.css index 946a665b..f4903128 100644 --- a/assets/css/src/header.css +++ b/assets/css/src/header.css @@ -55,6 +55,11 @@ nav .active a { color: #d40000; } +/* user links on the left */ +header .user-links .dropdown { + margin-left: 15px; +} + /* title tooltip */ header h1 .tooltip { opacity: 0.3; diff --git a/assets/css/src/listing.css b/assets/css/src/listing.css index c40c4821..e96197e4 100644 --- a/assets/css/src/listing.css +++ b/assets/css/src/listing.css @@ -5,7 +5,7 @@ margin-bottom: 20px; border: 1px solid #ddd; color: #333; - background-color: #fefefe; + background-color: #fcfcfc; overflow: auto; } diff --git a/assets/css/src/project.css b/assets/css/src/project.css new file mode 100644 index 00000000..7a77067f --- /dev/null +++ b/assets/css/src/project.css @@ -0,0 +1,42 @@ +.project-creation-options { + max-width: 500px; + border-left: 3px dotted #efefef; + margin-top: 20px; + padding-left: 15px; + padding-bottom: 5px; + padding-top: 5px; +} + +.project-overview-columns { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + + -webkit-align-items: center; + align-items: center; + + -webkit-justify-content: center; + justify-content: center; + + margin-bottom: 20px; + font-size: 1.4em; +} + +.project-overview-column { + text-align: center; + margin-right: 80px; +} + +.project-overview-column strong { + font-size: 1.3em; + color: #444; +} + +.project-overview-column span { + font-size: 0.8em; + color: #777; +} diff --git a/assets/css/src/responsive.css b/assets/css/src/responsive.css index 3ec47255..c94be166 100644 --- a/assets/css/src/responsive.css +++ b/assets/css/src/responsive.css @@ -1,14 +1,3 @@ -@media only screen and (max-width: 1080px) { - div.filter-dropdowns .filters { - margin-left: 0; - } - - div.filter-dropdowns { - display: block; - margin-top: 5px; - } -} - @media only screen and (max-width: 1024px) { body { @@ -32,15 +21,6 @@ max-width: 150px; } - .task-time-form label { - display: block; - } - - .task-time-form input[type="submit"] { - margin-top: 10px; - display: block; - } - .page-header .form-input-large { width: 300px; } diff --git a/assets/css/src/screenshot.css b/assets/css/src/screenshot.css deleted file mode 100644 index 4d917200..00000000 --- a/assets/css/src/screenshot.css +++ /dev/null @@ -1,19 +0,0 @@ -#screenshot-zone { - position: relative; - border: 2px dashed #ccc; - width: 90%; - height: 250px; - overflow: auto; -} - -#screenshot-inner { - position: absolute; - left: 0; - bottom: 48%; - width: 100%; - text-align: center; -} - -#screenshot-zone.screenshot-pasted { - border: 2px solid #333; -}
\ No newline at end of file diff --git a/assets/css/src/sidebar.css b/assets/css/src/sidebar.css index a9d56865..2da0f2e3 100644 --- a/assets/css/src/sidebar.css +++ b/assets/css/src/sidebar.css @@ -1,90 +1,73 @@ -/* sidebar */ .sidebar-container { margin-top: 10px; - position: relative; - clear: both; + 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 { - margin-left: 23%; - width: 76%; - position: absolute; + padding-left: 10px; + -ms-flex: 1; + -webkit-box-flex: 1; + -moz-box-flex: 1; + -ms-box-flex: 1; + box-flex: 1; } .sidebar { - width: 20%; - float: left; - padding: 10px; - padding-top: 0; - border: 1px solid #ddd; - background: #fdfdfd; - border-radius: 5px; + padding-right: 10px; + border-right: 1px dotted #eee; + font-size: 0.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 li { - list-style-type: square; - margin-left: 30px; - line-height: 1.8em; +.sidebar h2 { + margin-top: 0; } -.sidebar li.active a { - color: #000; - font-weight: bold; +.sidebar a { text-decoration: none; } -.sidebar li.active a:focus, -.sidebar li.active a:hover { - text-decoration: underline; +.sidebar li { + list-style-type: none; + line-height: 35px; + border-bottom: 1px dotted #efefef; + padding-left: 13px; } -.sidebar-collapsed .sidebar { - width: 10px; - padding-bottom: 0; - float: none; +.sidebar li:hover { + border-left: 5px solid #555; + padding-left: 8px; } -.sidebar-collapsed .sidebar-content { - margin: 0; - margin-top: 15px; - width: 100%; +.sidebar li.active { + border-left: 5px solid #333; + padding-left: 8px; } -.sidebar-collapse { - text-align: right; -} - -.sidebar-collapse a, -.sidebar-expand a { +.sidebar li.active a { color: #333; - text-decoration: none; -} - -.sidebar-collapse a:hover, -.sidebar-expand a:hover { - color: #DF5353; -} - -@media only screen and (max-width: 1024px) { - .sidebar { - width: 25%; - } - - .sidebar-content { - margin-left: 30%; - width: 70%; - } + font-weight: bold; } -@media only screen and (max-width: 767px) { - .sidebar { - width: 95%; - float: none; - } - - .sidebar-content { - margin: 0; - margin-top: 15px; - width: 100%; - } +.sidebar li.active a:focus, +.sidebar li.active a:hover { + color: #555; } diff --git a/assets/css/src/table.css b/assets/css/src/table.css index 51d6ecde..49569381 100644 --- a/assets/css/src/table.css +++ b/assets/css/src/table.css @@ -62,7 +62,7 @@ th a:hover { text-overflow: ellipsis; } -.table-stripped tr:nth-child(odd) td { +.table-stripped tr:nth-child(odd) { background: #fefefe; } @@ -124,4 +124,38 @@ th a:hover { .column-70 { width: 70%; -}
\ No newline at end of file +} + +.draggable-row-handle { + cursor: move; + color: #dedede; +} + +.draggable-row-handle:hover { + color: #333; +} + +tr.draggable-item-selected { + background: #fff; + border: 2px solid #666; + box-shadow: 4px 2px 10px -4px rgba(0,0,0,0.55); +} + +tr.draggable-item-selected td { + border-top: none; + border-bottom: none; +} + +tr.draggable-item-selected td:first-child { + border-left: none; +} + +tr.draggable-item-selected td:last-child { + border-right: none; +} + +.table-stripped tr.draggable-item-hover, +tr.draggable-item-hover { + background: #FEFFF2; +} + diff --git a/assets/css/src/task.css b/assets/css/src/task.css index 7bfb63e2..b465a283 100644 --- a/assets/css/src/task.css +++ b/assets/css/src/task.css @@ -9,7 +9,7 @@ } div.task-board-recent { - box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2); + border-width: 2px; } div.task-board-status-closed { @@ -118,21 +118,11 @@ span.task-board-date-overdue { } /* task score */ -.task-score { - font-weight: bold; -} - .task-board .task-score { + font-weight: bold; font-size: 1.1em; } -.task-show-details .task-score { - position: absolute; - bottom: 5px; - right: 5px; - font-size: 2em; -} - /* task age */ .task-board-closed, .task-board-days { @@ -167,57 +157,54 @@ span.task-board-date-overdue { display: none; } -/* task view */ -.task-show-details { - position: relative; - border-radius: 5px; - padding-bottom: 10px; +/* task summary */ +#task-summary { + margin-bottom: 15px; } -.task-show-details h2 { - font-size: 1.8em; - margin: 0; - margin-bottom: 25px; - padding: 0; - padding-left: 10px; - padding-right: 10px; +#task-summary h2 { + color: #666; + font-size: 2.5em; + margin-top: 0; + padding-top: 0; } -.task-show-details li { - margin-left: 25px; - list-style-type: circle; -} - -.task-show-section { - margin-top: 30px; - margin-bottom: 20px; +.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-show-files a { - font-weight: bold; - text-decoration: none; -} - -.task-show-files li { - margin-left: 25px; - list-style-type: square; - line-height: 25px; +.task-summary-column { + font-size: 0.9em; + color: #666; } -.task-show-file-actions { - font-size: 0.75em; +.task-summary-column span { + color: #555; } -.task-show-file-actions:before { - content: " ["; +.task-summary-column li { + line-height: 23px; } -.task-show-file-actions:after { - content: "]"; +.task-show-title { + border: 2px solid #000; + border-radius: 8px; + margin-bottom: 20px; } -.task-show-file-actions a { - color: #333; +.task-show-title h2 { + color: #555; + font-size: 1.8em; + margin: 0; + padding: 8px; } .task-show-description { @@ -231,75 +218,10 @@ span.task-board-date-overdue { height: 300px; } -.task-file-viewer { - position: relative; -} - -.task-file-viewer img { - max-width: 95%; - max-height: 85%; - margin-top: 10px; -} - -.task-time-form { - margin-top: 10px; - margin-bottom: 25px; - padding: 3px; -} - .task-link-closed { text-decoration: line-through; } -.task-show-images { - list-style-type: none; -} - -.task-show-images li img { - width: 100%; -} - -.task-show-images li .img_container { - width: 250px; - height: 100px; - overflow: hidden; -} - -.task-show-images li { - padding: 10px; - overflow: auto; - width: 250px; - min-height: 120px; - display: inline-block; - vertical-align: top; -} - -.task-show-images li p{ - padding: 5px; - font-weight: bold; -} - -.task-show-images li:hover { - background: #eee; -} - -.task-show-image-actions { - margin-left: 5px; -} - -.task-show-file-table { - width: auto; -} - -.task-show-start-link { - color: #000; -} - -.task-show-start-link:hover, -.task-show-start-link:focus { - color: red; -} - .flag-milestone { color: green; } @@ -329,3 +251,9 @@ div.color-square-selected { height: 28px; box-shadow: 3px 2px 10px 0 rgba(180,180,180,0.9); } + +/* Assign to me */ +.assign-me { + font-size: 0.8em; + vertical-align: bottom; +} diff --git a/assets/css/src/tasklink.css b/assets/css/src/tasklink.css new file mode 100644 index 00000000..826792cd --- /dev/null +++ b/assets/css/src/tasklink.css @@ -0,0 +1,12 @@ +/* tasklinks */ +.task-links-table { + font-size: 0.85em; +} + +.task-links-table td { + vertical-align: middle; +} + +.task-links-task-count { + color: #999; +} diff --git a/assets/css/src/tooltip.css b/assets/css/src/tooltip.css index f74ac09a..84d709c9 100644 --- a/assets/css/src/tooltip.css +++ b/assets/css/src/tooltip.css @@ -76,3 +76,22 @@ div.ui-tooltip { .ui-tooltip ul { margin-left: 20px; } + +.ui-tooltip dl { + margin: -5px 0 0 0; + padding: 0; +} + +.ui-tooltip dt { + margin-top: 5px; +} + +.ui-tooltip dd { + margin-left: 0; +} + +.ui-tooltip .progress { + display: inline-block; + min-width: 3em; + text-align: right; +}
\ No newline at end of file diff --git a/assets/css/src/upload.css b/assets/css/src/upload.css new file mode 100644 index 00000000..aa46bc7a --- /dev/null +++ b/assets/css/src/upload.css @@ -0,0 +1,39 @@ +#file-dropzone, +#screenshot-zone { + position: relative; + border: 2px dashed #ccc; + width: 99%; + height: 250px; + overflow: auto; +} + +#file-dropzone-inner, +#screenshot-inner { + position: absolute; + left: 0; + bottom: 48%; + width: 100%; + text-align: center; + color: #aaa; +} + +#screenshot-zone.screenshot-pasted { + border: 2px solid #333; +} + +#file-list { + margin: 20px; +} + +#file-list li { + list-style-type: none; + padding-top: 8px; + padding-bottom: 8px; + border-bottom: 1px dotted #ddd; + width: 95%; +} + +#file-list li.file-error { + font-weight: bold; + color: #b94a48; +} diff --git a/assets/css/src/views.css b/assets/css/src/views.css new file mode 100644 index 00000000..191b30c6 --- /dev/null +++ b/assets/css/src/views.css @@ -0,0 +1,34 @@ +.views { + display: inline-block; + margin-left: 10px; + margin-right: 10px; + font-size: 0.9em; +} + +.views li { + border: 1px solid #eee; + padding-left: 8px; + padding-right: 8px; + padding-top: 5px; + padding-bottom: 5px; + display: inline; +} + +.menu-inline li.active a, +.views li.active a { + font-weight: bold; + color: #000; + text-decoration: none; +} + +.views li:first-child { + border-right: none; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.views li:last-child { + border-left: none; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} diff --git a/assets/img/gitlab-icon.png b/assets/img/gitlab-icon.png Binary files differdeleted file mode 100644 index 88d48b40..00000000 --- a/assets/img/gitlab-icon.png +++ /dev/null diff --git a/assets/js/app.js b/assets/js/app.js index 6ab3ca2e..c119ffe9 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1278,4 +1278,4 @@ if (typeof jQuery === 'undefined') { return jQuery; })); -!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"<Dříve",nextText:"Později>",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"<Forrige",nextText:"Næste>",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"<Ant",nextText:"Sig>",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"«Edellinen",nextText:"Seuraava»",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"<mundur",nextText:"maju>",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"<Prec",nextText:"Succ>",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"<前",nextText:"次>",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"«Forrige",nextText:"Neste»",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"<Poprzedni",nextText:"Następny>",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"<Anterior",nextText:"Próximo>",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"<Пред",nextText:"След>",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"«Förra",nextText:"Nästa»",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"« ย้อน",nextText:"ถัดไป »",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"<geri",nextText:"ileri>",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"<上月",nextText:"下月>",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});(function(){function t(x){this.app=x;this.router=new v();this.router.addRoute("screenshot-zone",e)}t.prototype.isOpen=function(){return $("#popover-container").size()>0};t.prototype.open=function(y){var x=this;x.app.dropdown.close();$.get(y,function(z){$("body").append('<div id="popover-container"><div id="popover-content">'+z+"</div></div>");x.app.refresh();x.router.dispatch(this.app);x.afterOpen()})};t.prototype.close=function(x){if(this.isOpen()){if(x){x.preventDefault()}$("#popover-container").remove()}};t.prototype.onClick=function(y){y.preventDefault();y.stopPropagation();var x=y.target.getAttribute("href");if(!x){x=y.target.getAttribute("data-href")}if(x){this.open(x)}};t.prototype.listen=function(){$(document).on("click",".popover",this.onClick.bind(this));$(document).on("click",".close-popover",this.close.bind(this));$(document).on("click","#popover-container",this.close.bind(this));$(document).on("click","#popover-content",function(x){x.stopPropagation()})};t.prototype.afterOpen=function(){var x=this;var y=$("#task-form");if(y){y.on("submit",function(z){z.preventDefault();$.ajax({type:"POST",url:y.attr("action"),data:y.serialize(),success:function(B,C,A){if(A.getResponseHeader("X-Ajax-Redirect")){window.location=A.getResponseHeader("X-Ajax-Redirect")}else{$("#popover-content").html(B);x.afterOpen()}}})})}};function r(){}r.prototype.listen=function(){var x=this;$(document).on("click",function(){x.close()});$(document).on("click",".dropdown-menu",function(B){B.preventDefault();B.stopImmediatePropagation();x.close();var z=$(this).next("ul");var C=$(this).offset();$("body").append(jQuery("<div>",{id:"dropdown"}));z.clone().appendTo("#dropdown");var D=$("#dropdown ul");D.addClass("dropdown-submenu-open");var A=D.outerHeight();var y=D.outerWidth();if(C.top+A-$(window).scrollTop()>$(window).height()){D.css("top",C.top-A-5)}else{D.css("top",C.top+$(this).height())}if(C.left+y>$(window).width()){D.css("left",C.left-y+$(this).outerWidth())}else{D.css("left",C.left)}});$(document).on("click",".dropdown-submenu-open li",function(y){if($(y.target).is("li")){$(this).find("a:visible")[0].click()}});$("textarea[data-mention-search-url]").textcomplete([{match:/(^|\s)@(\w*)$/,search:function(z,A){var y=$("textarea[data-mention-search-url]").data("mention-search-url");$.getJSON(y,{q:z}).done(function(B){A(B)}).fail(function(){A([])})},replace:function(y){return"$1@"+y+" "},cache:true}],{className:"textarea-dropdown"})};r.prototype.close=function(){$("#dropdown").remove()};function q(x){this.app=x}q.prototype.listen=function(){var x=this;$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(y,z){$(this).css(y);var A=z.target.left+z.target.width/2-z.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(z.vertical).addClass(A<1?"align-left":"align-right").appendTo(this)}},content:function(){var A=this;var y=$(this).attr("data-href");if(!y){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(y,function z(D){var C=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(D);C.css({top:"",left:""});C.children(".tooltip-arrow").remove();var B=$(A).tooltip("option","position");B.of=$(A);C.position(B);$("#tooltip-subtasks a").not(".popover").click(function(E){E.preventDefault();E.stopPropagation();if($(this).hasClass("popover-subtask-restriction")){x.app.popover.open($(this).attr("href"));$(A).tooltip("close")}else{$.get($(this).attr("href"),z)}})});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var y=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(y).tooltip("close")})}).on("mouseleave focusout",function(y){y.stopImmediatePropagation();var z=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(z).tooltip("close")}},100)})};function l(){}l.prototype.showPreview=function(B){B.preventDefault();var y=$(".write-area");var A=$(".preview-area");var x=$("textarea");$("#markdown-write").parent().removeClass("form-tab-selected");$("#markdown-preview").parent().addClass("form-tab-selected");var z=$.ajax({url:$("body").data("markdown-preview-url"),contentType:"application/json",type:"POST",processData:false,dataType:"html",data:JSON.stringify({text:x.val()})});z.done(function(C){A.find(".markdown").html(C);A.css("height",x.css("height"));A.css("width",x.css("width"));y.hide();A.show()})};l.prototype.showWriter=function(x){x.preventDefault();$("#markdown-write").parent().addClass("form-tab-selected");$("#markdown-preview").parent().removeClass("form-tab-selected");$(".write-area").show();$(".preview-area").hide()};l.prototype.listen=function(){$(document).on("click","#markdown-preview",this.showPreview.bind(this));$(document).on("click","#markdown-write",this.showWriter.bind(this))};function b(){}b.prototype.expand=function(x){x.preventDefault();$(".sidebar-container").removeClass("sidebar-collapsed");$(".sidebar-collapse").show();$(".sidebar h2").show();$(".sidebar ul").show();$(".sidebar-expand").hide()};b.prototype.collapse=function(x){x.preventDefault();$(".sidebar-container").addClass("sidebar-collapsed");$(".sidebar-expand").show();$(".sidebar h2").hide();$(".sidebar ul").hide();$(".sidebar-collapse").hide()};b.prototype.listen=function(){$(document).on("click",".sidebar-collapse",this.collapse);$(document).on("click",".sidebar-expand",this.expand)};function f(x){this.app=x;this.keyboardShortcuts()}f.prototype.focus=function(){$(document).on("focus","#form-search",function(){if($("#form-search")[0].setSelectionRange){$("#form-search")[0].setSelectionRange($("#form-search").val().length,$("#form-search").val().length)}})};f.prototype.listen=function(){var x=this;$(document).on("click",".filter-helper",function(A){A.preventDefault();var z=$(this).data("filter");var y=$(this).data("append-filter");if(y){z=$("#form-search").val()+" "+y}$("#form-search").val(z);if($("#board").length){x.app.board.reloadFilters(z)}else{$("form.search").submit()}})};f.prototype.keyboardShortcuts=function(){var x=this;Mousetrap.bind("v b",function(z){var y=$(".view-board");if(y.length){window.location=y.attr("href")}});Mousetrap.bind("v c",function(z){var y=$(".view-calendar");if(y.length){window.location=y.attr("href")}});Mousetrap.bind("v l",function(z){var y=$(".view-listing");if(y.length){window.location=y.attr("href")}});Mousetrap.bind("v g",function(z){var y=$(".view-gantt");if(y.length){window.location=y.attr("href")}});Mousetrap.bind("f",function(z){z.preventDefault();var y=document.getElementById("form-search");if(y){y.focus()}});Mousetrap.bind("r",function(z){z.preventDefault();var y=$(".filter-reset").data("filter");$("#form-search").val(y);if($("#board").length){x.app.board.reloadFilters(y)}else{$("form.search").submit()}})};function m(){this.board=new k(this);this.markdown=new l();this.sidebar=new b();this.search=new f(this);this.swimlane=new g();this.dropdown=new r();this.tooltip=new q(this);this.popover=new t(this);this.task=new a();this.project=new n();this.keyboardShortcuts();this.chosen();this.poll();$(".alert-fade-out").delay(4000).fadeOut(800,function(){$(this).remove()});var x=false;$("select.task-reload-project-destination").change(function(){if(!x){$(".loading-icon").show();x=true;window.location=$(this).data("redirect").replace(/PROJECT_ID/g,$(this).val())}})}m.prototype.listen=function(){this.project.listen();this.popover.listen();this.markdown.listen();this.sidebar.listen();this.tooltip.listen();this.dropdown.listen();this.search.listen();this.task.listen();this.swimlane.listen();this.search.focus();this.autoComplete();this.datePicker();this.focus()};m.prototype.refresh=function(){$(document).off();this.listen()};m.prototype.focus=function(){$("[autofocus]").each(function(x,y){$(this).focus()});$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(x){x.preventDefault()})};m.prototype.poll=function(){window.setInterval(this.checkSession,60000)};m.prototype.keyboardShortcuts=function(){var x=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(y){y.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){x.popover.close();x.dropdown.close()})};m.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};m.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};m.prototype.autoComplete=function(){$(".autocomplete").each(function(){var y=$(this);var z=y.data("dst-field");var x=y.data("dst-extra-field");if($("#form-"+z).val()==""){y.parent().find("input[type=submit]").attr("disabled","disabled")}y.autocomplete({source:y.data("search-url"),minLength:1,select:function(A,B){$("input[name="+z+"]").val(B.item.id);if(x){$("input[name="+x+"]").val(B.item[x])}y.parent().find("input[type=submit]").removeAttr("disabled")}})})};m.prototype.chosen=function(){$(".chosen-select").each(function(){var x=$(this).data("search-threshold");if(x===undefined){x=10}$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:x})});$(".select-auto-redirect").change(function(){var x=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(x,$(this).val())})};m.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon"> <i class="fa fa-spinner fa-spin"></i></span>')};m.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};m.prototype.isVisible=function(){var x="";if(typeof document.hidden!=="undefined"){x="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){x="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){x="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){x="webkitVisibilityState"}}}}if(x!=""){return document[x]=="visible"}return true};m.prototype.formatDuration=function(x){if(x>=86400){return Math.round(x/86400)+"d"}else{if(x>=3600){return Math.round(x/3600)+"h"}else{if(x>=60){return Math.round(x/60)+"m"}}}return x+"s"};function e(){this.pasteCatcher=null}e.prototype.execute=function(){this.initialize()};e.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};e.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};e.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};e.prototype.pasteHandler=function(C){if(C.clipboardData&&C.clipboardData.items){var A=C.clipboardData.items;if(A){for(var B=0;B<A.length;B++){if(A[B].type.indexOf("image")!==-1){var z=A[B].getAsFile();var x=new FileReader();var y=this;x.onload=function(D){y.createImage(D.target.result)};x.readAsDataURL(z)}}}}else{setTimeout(this.checkInput.bind(this),100)}};e.prototype.checkInput=function(){var x=this.pasteCatcher.childNodes[0];if(x){if(x.tagName==="IMG"){this.createImage(x.src)}}this.pasteCatcher.innerHTML=""};e.prototype.createImage=function(z){var y=new Image();y.src=z;y.onload=function(){var A=z.split("base64,");var B=A[1];$("input[name=screenshot]").val(B)};var x=document.getElementById("screenshot-zone");x.innerHTML="";x.className="screenshot-pasted";x.appendChild(y);this.destroy();this.initialize()};function j(){}j.prototype.execute=function(){var x=$("#calendar");x.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(y){$.ajax({cache:false,url:x.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:y.id,date_due:y.start.format()})})},viewRender:function(){var y=x.data("check-url");var A={start:x.fullCalendar("getView").start.format(),end:x.fullCalendar("getView").end.format()};for(var z in A){y+="&"+z+"="+A[z]}$.getJSON(y,function(B){x.fullCalendar("removeEvents");x.fullCalendar("addEventSource",B);x.fullCalendar("rerenderEvents")})}})};function k(x){this.app=x;this.checkInterval=null;this.savingInProgress=false}k.prototype.execute=function(){this.app.swimlane.refresh();this.restoreColumnViewMode();this.compactView();this.poll();this.keyboardShortcuts();this.listen();this.dragAndDrop();$(window).on("load",this.columnScrolling);$(window).resize(this.columnScrolling)};k.prototype.poll=function(){var x=parseInt($("#board").attr("data-check-interval"));if(x>0){this.checkInterval=window.setInterval(this.check.bind(this),x*1000)}};k.prototype.reloadFilters=function(x){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("reload-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({search:x}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};k.prototype.check=function(){if(this.app.isVisible()&&!this.savingInProgress){var x=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(y){x.refresh(y)},304:function(){x.app.hideLoadingIcon()}}})}};k.prototype.save=function(A,B,x,z){var y=this;this.app.showLoadingIcon();this.savingInProgress=true;$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:A,column_id:B,swimlane_id:z,position:x}),success:function(C){y.refresh(C);this.savingInProgress=false},error:function(){y.app.hideLoadingIcon();this.savingInProgress=false}})};k.prototype.refresh=function(x){$("#board-container").replaceWith(x);this.app.refresh();this.app.swimlane.refresh();this.app.hideLoadingIcon();this.listen();this.dragAndDrop();this.compactView();this.restoreColumnViewMode();this.columnScrolling()};k.prototype.dragAndDrop=function(){var x=this;var y={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(A,H){var C=H.item;var G=C.attr("data-task-id");var I=C.attr("data-position");var F=C.attr("data-column-id");var E=C.attr("data-swimlane-id");var B=C.parent().attr("data-column-id");var z=C.parent().attr("data-swimlane-id");var D=C.index()+1;C.removeClass("draggable-item-selected");if(B!=F||z!=E||D!=I){x.changeTaskState(G);x.save(G,B,D,z)}},start:function(z,A){A.item.addClass("draggable-item-selected");A.placeholder.height(A.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");y.handle=".task-board-sort-handle"}$(".board-task-list").sortable(y)};k.prototype.changeTaskState=function(y){var x=$("div[data-task-id="+y+"]");x.addClass("task-board-saving-state");x.find(".task-board-saving-icon").show()};k.prototype.listen=function(){var x=this;$(document).on("click",".task-board",function(y){if(y.target.tagName!="A"){window.location=$(this).data("task-url")}});$(document).on("click",".filter-toggle-scrolling",function(y){y.preventDefault();x.toggleCompactView()});$(document).on("click",".filter-toggle-height",function(y){y.preventDefault();x.toggleColumnScrolling()});$(document).on("click",".board-toggle-column-view",function(){x.toggleColumnViewMode($(this).data("column-id"))})};k.prototype.toggleColumnScrolling=function(){var x=localStorage.getItem("column_scroll");if(x==undefined){x=1}localStorage.setItem("column_scroll",x==0?1:0);this.columnScrolling()};k.prototype.columnScrolling=function(){if(localStorage.getItem("column_scroll")==0){var x=80;$(".filter-max-height").show();$(".filter-min-height").hide();$(".board-rotation-wrapper").css("min-height","");$(".board-task-list").each(function(){var y=$(this).height();if(y>x){x=y}});$(".board-task-list").css("min-height",x);$(".board-task-list").css("height","")}else{$(".filter-max-height").hide();$(".filter-min-height").show();if($(".board-swimlane").length>1){$(".board-task-list").each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);$(".board-rotation-wrapper").css("min-height",320)}})}else{var x=$(window).height()-170;$(".board-task-list").css("height",x);$(".board-rotation-wrapper").css("min-height",x)}}};k.prototype.toggleCompactView=function(){var x=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",x==0?1:0);this.compactView()};k.prototype.compactView=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};k.prototype.toggleCollapsedMode=function(){var x=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(y){$(".filter-display-mode").toggle();x.refresh(y)}})};k.prototype.restoreColumnViewMode=function(){var x=this;$(".board-column-header").each(function(){var y=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+y)){x.hideColumn(y)}})};k.prototype.toggleColumnViewMode=function(x){if(localStorage.getItem("hidden_column_"+x)){this.showColumn(x)}else{this.hideColumn(x)}};k.prototype.hideColumn=function(x){$(".board-column-"+x+" .board-column-expanded").hide();$(".board-column-"+x+" .board-column-collapsed").show();$(".board-column-header-"+x+" .board-column-expanded").hide();$(".board-column-header-"+x+" .board-column-collapsed").show();$(".board-column-header-"+x).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+x).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+x+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+x+"").height())});localStorage.setItem("hidden_column_"+x,1)};k.prototype.showColumn=function(x){$(".board-column-"+x+" .board-column-expanded").show();$(".board-column-"+x+" .board-column-collapsed").hide();$(".board-column-header-"+x+" .board-column-expanded").show();$(".board-column-header-"+x+" .board-column-collapsed").hide();$(".board-column-header-"+x).removeClass("board-column-header-collapsed");$(".board-column-"+x).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+x).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+x)};k.prototype.keyboardShortcuts=function(){var x=this;Mousetrap.bind("c",function(){x.toggleCompactView()});Mousetrap.bind("s",function(){x.toggleCollapsedMode()});Mousetrap.bind("n",function(){x.app.popover.open($("#board").data("task-creation-url"))})};function g(){}g.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};g.prototype.expand=function(y){var z=this.getAllCollapsed();var x=z.indexOf(y);if(x>-1){z.splice(x,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(z));$(".board-swimlane-columns-"+y).css("display","table-row");$(".board-swimlane-tasks-"+y).css("display","table-row");$(".hide-icon-swimlane-"+y).css("display","inline");$(".show-icon-swimlane-"+y).css("display","none")};g.prototype.collapse=function(x){var y=this.getAllCollapsed();if(y.indexOf(x)<0){y.push(x);localStorage.setItem(this.getStorageKey(),JSON.stringify(y))}$(".board-swimlane-columns-"+x+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+x).css("display","none");$(".hide-icon-swimlane-"+x).css("display","none");$(".show-icon-swimlane-"+x).css("display","inline")};g.prototype.isCollapsed=function(x){return this.getAllCollapsed().indexOf(x)>-1};g.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};g.prototype.refresh=function(){var y=this.getAllCollapsed();for(var x=0;x<y.length;x++){this.collapse(y[x])}};g.prototype.listen=function(){var x=this;$(document).on("click",".board-swimlane-toggle",function(z){z.preventDefault();var y=$(this).data("swimlane-id");if(x.isCollapsed(y)){x.expand(y)}else{x.collapse(y)}})};function c(x){this.app=x;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}}c.prototype.saveRecord=function(x){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(x),complete:this.app.hideLoadingIcon.bind(this)})};c.prototype.execute=function(){this.data=this.prepareData($(this.options.container).data("records"));var A=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var z=this.getDateRange(A);var x=z[0];var C=z[1];var y=$(this.options.container);var B=jQuery("<div>",{"class":"ganttview"});B.append(this.renderVerticalHeader());B.append(this.renderSlider(x,C));y.append(B);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",y).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",y).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",y).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(x);this.listenForBlockMove(x)}else{this.options.allowResizes=false;this.options.allowMoves=false}};c.prototype.renderVerticalHeader=function(){var B=jQuery("<div>",{"class":"ganttview-vtheader"});var y=jQuery("<div>",{"class":"ganttview-vtheader-item"});var A=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var x=0;x<this.data.length;x++){var z=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[x])})).append(" ");if(this.data[x].type=="task"){z.append(jQuery("<a>",{href:this.data[x].link,target:"_blank",title:this.data[x].title}).append(this.data[x].title))}else{z.append(jQuery("<a>",{href:this.data[x].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append(" ").append(jQuery("<a>",{href:this.data[x].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append(" ").append(jQuery("<a>",{href:this.data[x].link,target:"_blank"}).append(this.data[x].title))}A.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(z))}y.append(A);B.append(y);return B};c.prototype.renderSlider=function(y,A){var x=jQuery("<div>",{"class":"ganttview-slide-container"});var z=this.getDates(y,A);x.append(this.renderHorizontalHeader(z));x.append(this.renderGrid(z));x.append(this.addBlockContainers());this.addBlocks(x,y);return x};c.prototype.renderHorizontalHeader=function(x){var E=jQuery("<div>",{"class":"ganttview-hzheader"});var C=jQuery("<div>",{"class":"ganttview-hzheader-months"});var B=jQuery("<div>",{"class":"ganttview-hzheader-days"});var A=0;for(var F in x){for(var z in x[F]){var G=x[F][z].length*this.options.cellWidth;A=A+G;C.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(G-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[z]+" "+F));for(var D in x[F][z]){B.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(x[F][z][D].getDate()))}}}C.css("width",A+"px");B.css("width",A+"px");E.append(C).append(B);return E};c.prototype.renderGrid=function(x){var G=jQuery("<div>",{"class":"ganttview-grid"});var B=jQuery("<div>",{"class":"ganttview-grid-row"});for(var E in x){for(var z in x[E]){for(var D in x[E][z]){var A=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(x[E][z][D])){A.addClass("ganttview-weekend")}B.append(A)}}}var F=jQuery("div.ganttview-grid-row-cell",B).length*this.options.cellWidth;B.css("width",F+"px");G.css("width",F+"px");for(var C=0;C<this.data.length;C++){G.append(B.clone())}return G};c.prototype.addBlockContainers=function(){var y=jQuery("<div>",{"class":"ganttview-blocks"});for(var x=0;x<this.data.length;x++){y.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return y};c.prototype.addBlocks=function(y,x){var F=jQuery("div.ganttview-blocks div.ganttview-block-container",y);var z=0;for(var C=0;C<this.data.length;C++){var D=this.data[C];var G=this.daysBetween(D.start,D.end)+1;var B=this.daysBetween(x,D.start);var E=jQuery("<div>",{"class":"ganttview-block-text"});var A=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(D),css:{width:((G*this.options.cellWidth)-9)+"px","margin-left":(B*this.options.cellWidth)+"px"}}).append(E);if(G>=2){E.append(D.progress)}A.data("record",D);this.setBarColor(A,D);if(D.progress!="0%"){A.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":D.color.border,width:D.progress,opacity:0.4}}))}jQuery(F[z]).append(A);z=z+1}};c.prototype.getVerticalHeaderTooltip=function(y){var D="";if(y.type=="task"){D="<strong>"+y.column_title+"</strong> ("+y.progress+")<br/>"+y.title}else{var A=["managers","members"];for(var z in A){var B=A[z];if(!jQuery.isEmptyObject(y.users[B])){var C=jQuery("<ul>");for(var x in y.users[B]){C.append(jQuery("<li>").append(y.users[B][x]))}D+="<p><strong>"+$(this.options.container).data("label-"+B)+"</strong></p>"+C[0].outerHTML}}}return D};c.prototype.getBarTooltip=function(x){var y="";if(x.not_defined){y=$(this.options.container).data("label-not-defined")}else{if(x.type=="task"){y="<strong>"+x.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(x.assignee?x.assignee:"")+"<br/>"}y+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",x.start)+"<br/>";y+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",x.end)}return y};c.prototype.setBarColor=function(y,x){if(x.not_defined){y.addClass("ganttview-block-not-defined")}else{y.css("background-color",x.color.background);y.css("border-color",x.color.border)}};c.prototype.listenForBlockResize=function(x){var y=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var z=jQuery(this);y.updateDataAndPosition(z,x);y.saveRecord(z.data("record"))}})};c.prototype.listenForBlockMove=function(x){var y=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var z=jQuery(this);y.updateDataAndPosition(z,x);y.saveRecord(z.data("record"))}})};c.prototype.updateDataAndPosition=function(C,A){var x=jQuery("div.ganttview-slide-container",this.options.container);var G=x.scrollLeft();var D=C.offset().left-x.offset().left-1+G;var F=C.data("record");F.not_defined=false;this.setBarColor(C,F);var z=Math.round(D/this.options.cellWidth);var E=this.addDays(this.cloneDate(A),z);F.start=E;var y=C.outerWidth();var B=Math.round(y/this.options.cellWidth)-1;F.end=this.addDays(this.cloneDate(E),B);if(F.type==="task"&&B>0){jQuery("div.ganttview-block-text",C).text(F.progress)}C.attr("title",this.getBarTooltip(F));C.data("record",F);C.css("top","").css("left","").css("position","relative").css("margin-left",D+"px")};c.prototype.getDates=function(B,x){var A=[];A[B.getFullYear()]=[];A[B.getFullYear()][B.getMonth()]=[B];var z=B;while(this.compareDate(z,x)==-1){var y=this.addDays(this.cloneDate(z),1);if(!A[y.getFullYear()]){A[y.getFullYear()]=[]}if(!A[y.getFullYear()][y.getMonth()]){A[y.getFullYear()][y.getMonth()]=[]}A[y.getFullYear()][y.getMonth()].push(y);z=y}return A};c.prototype.prepareData=function(z){for(var y=0;y<z.length;y++){var A=new Date(z[y].start[0],z[y].start[1]-1,z[y].start[2],0,0,0,0);z[y].start=A;var x=new Date(z[y].end[0],z[y].end[1]-1,z[y].end[2],0,0,0,0);z[y].end=x}return z};c.prototype.getDateRange=function(z){var C=new Date();var y=new Date();for(var A=0;A<this.data.length;A++){var B=new Date();B.setTime(Date.parse(this.data[A].start));var x=new Date();x.setTime(Date.parse(this.data[A].end));if(A==0){C=B;y=x}if(this.compareDate(C,B)==1){C=B}if(this.compareDate(y,x)==-1){y=x}}if(this.daysBetween(C,y)<z){y=this.addDays(this.cloneDate(C),z)}C.setDate(C.getDate()-1);return[C,y]};c.prototype.daysBetween=function(A,x){if(!A||!x){return 0}var z=0,y=this.cloneDate(A);while(this.compareDate(y,x)==-1){z=z+1;this.addDays(y,1)}return z};c.prototype.isWeekend=function(x){return x.getDay()%6==0};c.prototype.cloneDate=function(x){return new Date(x.getTime())};c.prototype.addDays=function(x,y){x.setDate(x.getDate()+y*1);return x};c.prototype.compareDate=function(y,x){if(isNaN(y)||isNaN(x)){throw new Error(y+" - "+x)}else{if(y instanceof Date&&x instanceof Date){return(y<x)?-1:(y>x)?1:0}else{throw new TypeError(y+" - "+x)}}};function a(){}a.prototype.listen=function(){$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))})};function n(){}n.prototype.listen=function(){$(".project-change-role").on("change",function(){$.ajax({cache:false,url:$(this).data("url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({id:$(this).data("id"),role:$(this).val()})})})};function s(){}s.prototype.execute=function(){var z=$("#chart").data("metrics");var y=[];for(var x=0;x<z.length;x++){y.push([z[x].column_title,z[x].nb_tasks])}c3.generate({data:{columns:y,type:"donut"}})};function p(){}p.prototype.execute=function(){var z=$("#chart").data("metrics");var y=[];for(var x=0;x<z.length;x++){y.push([z[x].user,z[x].nb_tasks])}c3.generate({data:{columns:y,type:"donut"}})};function d(){}d.prototype.execute=function(){var D=$("#chart").data("metrics");var C=[];var x=[];var y=[];var A=d3.time.format("%Y-%m-%d");var E=d3.time.format($("#chart").data("date-format"));for(var B=0;B<D.length;B++){for(var z=0;z<D[B].length;z++){if(B==0){C.push([D[B][z]]);if(z>0){x.push(D[B][z])}}else{C[z].push(D[B][z]);if(z==0){y.push(E(A.parse(D[B][z])))}}}}c3.generate({data:{columns:C,type:"area-spline",groups:[x]},axis:{x:{type:"category",categories:y}}})};function o(){}o.prototype.execute=function(){var C=$("#chart").data("metrics");var B=[[$("#chart").data("label-total")]];var x=[];var z=d3.time.format("%Y-%m-%d");var D=d3.time.format($("#chart").data("date-format"));for(var A=0;A<C.length;A++){for(var y=0;y<C[A].length;y++){if(A==0){B.push([C[A][y]])}else{B[y+1].push(C[A][y]);if(y>0){if(B[0][A]==undefined){B[0].push(0)}B[0][A]+=C[A][y]}if(y==0){x.push(D(z.parse(C[A][y])))}}}}c3.generate({data:{columns:B},axis:{x:{type:"category",categories:x}}})};function h(x){this.app=x}h.prototype.execute=function(){var z=$("#chart").data("metrics");var A=[$("#chart").data("label")];var x=[];for(var y in z){A.push(z[y].average);x.push(z[y].title)}c3.generate({data:{columns:[A],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:x},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function w(x){this.app=x}w.prototype.execute=function(){var z=$("#chart").data("metrics");var A=[$("#chart").data("label")];var x=[];for(var y=0;y<z.length;y++){A.push(z[y].time_spent);x.push(z[y].title)}c3.generate({data:{columns:[A],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:x},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function u(x){this.app=x}u.prototype.execute=function(){var D=$("#chart").data("metrics");var C=[$("#chart").data("label-cycle")];var z=[$("#chart").data("label-lead")];var y=[];var B={};B[$("#chart").data("label-cycle")]="area";B[$("#chart").data("label-lead")]="area-spline";var x={};x[$("#chart").data("label-lead")]="#afb42b";x[$("#chart").data("label-cycle")]="#4e342e";for(var A=0;A<D.length;A++){C.push(parseInt(D[A].avg_cycle_time));z.push(parseInt(D[A].avg_lead_time));y.push(D[A].day)}c3.generate({data:{columns:[z,C],types:B,colors:x},axis:{x:{type:"category",categories:y},y:{tick:{format:this.app.formatDuration}}}})};function i(x){this.app=x}i.prototype.execute=function(){var C=$("#chart").data("metrics");var y=$("#chart").data("label-open");var x=$("#chart").data("label-closed");var D=[$("#chart").data("label-spent")];var B=[$("#chart").data("label-estimated")];var A=[];for(var z in C){D.push(parseFloat(C[z].time_spent));B.push(parseFloat(C[z].time_estimated));A.push(z=="open"?y:x)}c3.generate({data:{columns:[D,B],type:"bar"},bar:{width:{ratio:0.2}},axis:{x:{type:"category",categories:A}},legend:{show:true}})};function v(){this.routes={}}v.prototype.addRoute=function(y,x){this.routes[y]=x};v.prototype.dispatch=function(y){for(var z in this.routes){if(document.getElementById(z)){var x=Object.create(this.routes[z].prototype);this.routes[z].apply(x,[y]);x.execute();break}}};jQuery(document).ready(function(){var y=new m();var x=new v();x.addRoute("board",k);x.addRoute("calendar",j);x.addRoute("screenshot-zone",e);x.addRoute("analytic-task-repartition",s);x.addRoute("analytic-user-repartition",p);x.addRoute("analytic-cfd",d);x.addRoute("analytic-burndown",o);x.addRoute("analytic-avg-time-column",h);x.addRoute("analytic-task-time-column",w);x.addRoute("analytic-lead-cycle-time",u);x.addRoute("analytic-compare-hours",i);x.addRoute("gantt-chart",c);x.dispatch(y);y.listen()})})();
\ No newline at end of file +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"<Dříve",nextText:"Později>",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"<Forrige",nextText:"Næste>",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"<Ant",nextText:"Sig>",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,b){var c=this._calendarEl[a],d=b&&b.hours();return"function"==typeof c&&(c=c.apply(b)),c.replace("{}",d%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"«Edellinen",nextText:"Seuraava»",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"<mundur",nextText:"maju>",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"<Prec",nextText:"Succ>",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"<前",nextText:"次>",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"«Forrige",nextText:"Neste»",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"<Poprzedni",nextText:"Następny>",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"<Anterior",nextText:"Próximo>",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"<Пред",nextText:"След>",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"«Förra",nextText:"Nästa»",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"« ย้อน",nextText:"ถัดไป »",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"<geri",nextText:"ileri>",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"<上月",nextText:"下月>",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});(function(){function u(z){this.app=z;this.router=new x();this.router.addRoute("screenshot-zone",d)}u.prototype.isOpen=function(){return $("#popover-container").size()>0};u.prototype.open=function(A){var z=this;z.app.dropdown.close();$.get(A,function(B){$("body").prepend('<div id="popover-container"><div id="popover-content">'+B+"</div></div>");z.app.refresh();z.router.dispatch(this.app);z.afterOpen()})};u.prototype.close=function(z){if(this.isOpen()){if(z){z.preventDefault()}$("#popover-container").remove()}};u.prototype.onClick=function(B){B.preventDefault();B.stopPropagation();var A=B.currentTarget||B.target;var z=A.getAttribute("href");if(!z){z=A.getAttribute("data-href")}if(z){this.open(z)}};u.prototype.listen=function(){$(document).on("click",".popover",this.onClick.bind(this));$(document).on("click",".close-popover",this.close.bind(this));$(document).on("click","#popover-container",this.close.bind(this));$(document).on("click","#popover-content",function(z){z.stopPropagation()})};u.prototype.afterOpen=function(){var A=this;var z=$("#popover-content .popover-form");if(z){z.on("submit",function(B){B.preventDefault();$.ajax({type:"POST",url:z.attr("action"),data:z.serialize(),success:function(D,E,C){A.afterSubmit(D,C,A)},beforeSend:function(){var C=$('.popover-form button[type="submit"]');C.html('<i class="fa fa-spinner fa-pulse"></i> '+C.html());C.attr("disabled",true)}})})}$(document).on("click",".popover-link",function(B){B.preventDefault();$.ajax({type:"GET",url:$(this).attr("href"),success:function(D,E,C){A.afterSubmit(D,C,A)}})})};u.prototype.afterSubmit=function(B,A,z){var C=A.getResponseHeader("X-Ajax-Redirect");if(C){window.location=C==="self"?window.location.href.split("#")[0]:C}else{$("#popover-content").html(B);$("#popover-content input[autofocus]").focus();z.afterOpen()}};function s(){}s.prototype.listen=function(){var z=this;$(document).on("click",function(){z.close()});$(document).on("click",".dropdown-menu",function(D){D.preventDefault();D.stopImmediatePropagation();z.close();var B=$(this).next("ul");var E=$(this).offset();$("body").append(jQuery("<div>",{id:"dropdown"}));B.clone().appendTo("#dropdown");var F=$("#dropdown ul");F.addClass("dropdown-submenu-open");var C=F.outerHeight();var A=F.outerWidth();if(E.top+C-$(window).scrollTop()<$(window).height()||$(window).scrollTop()+E.top<C){F.css("top",E.top+$(this).height())}else{F.css("top",E.top-C-5)}if(E.left+A>$(window).width()){F.css("left",E.left-A+$(this).outerWidth())}else{F.css("left",E.left)}});$(document).on("click",".dropdown-submenu-open li",function(A){if($(A.target).is("li")){$(this).find("a:visible")[0].click()}});$("textarea[data-mention-search-url]").textcomplete([{match:/(^|\s)@(\w*)$/,search:function(B,C){var A=$("textarea[data-mention-search-url]").data("mention-search-url");$.getJSON(A,{q:B}).done(function(D){C(D)}).fail(function(){C([])})},replace:function(A){return"$1@"+A+" "},cache:true}],{className:"textarea-dropdown"})};s.prototype.close=function(){$("#dropdown").remove()};function r(z){this.app=z}r.prototype.listen=function(){var z=this;$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(A,B){$(this).css(A);var C=B.target.left+B.target.width/2-B.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(B.vertical).addClass(C<1?"align-left":"align-right").appendTo(this)}},content:function(){var C=this;var A=$(this).attr("data-href");if(!A){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(A,function B(F){var E=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(F);E.css({top:"",left:""});E.children(".tooltip-arrow").remove();var D=$(C).tooltip("option","position");D.of=$(C);E.position(D)});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var A=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(A).tooltip("close")})}).on("mouseleave focusout",function(A){A.stopImmediatePropagation();var B=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(B).tooltip("close")}},100)})};function m(){}m.prototype.showPreview=function(D){D.preventDefault();var A=$(".write-area");var C=$(".preview-area");var z=$("textarea");$("#markdown-write").parent().removeClass("form-tab-selected");$("#markdown-preview").parent().addClass("form-tab-selected");var B=$.ajax({url:$("body").data("markdown-preview-url"),contentType:"application/json",type:"POST",processData:false,dataType:"html",data:JSON.stringify({text:z.val()})});B.done(function(E){C.find(".markdown").html(E);C.css("height",z.css("height"));C.css("width",z.css("width"));A.hide();C.show()})};m.prototype.showWriter=function(z){z.preventDefault();$("#markdown-write").parent().addClass("form-tab-selected");$("#markdown-preview").parent().removeClass("form-tab-selected");$(".write-area").show();$(".preview-area").hide()};m.prototype.listen=function(){$(document).on("click","#markdown-preview",this.showPreview.bind(this));$(document).on("click","#markdown-write",this.showWriter.bind(this))};function f(z){this.app=z;this.keyboardShortcuts()}f.prototype.focus=function(){$(document).on("focus","#form-search",function(){if($("#form-search")[0].setSelectionRange){$("#form-search")[0].setSelectionRange($("#form-search").val().length,$("#form-search").val().length)}})};f.prototype.listen=function(){var z=this;$(document).on("click",".filter-helper",function(C){C.preventDefault();var B=$(this).data("filter");var A=$(this).data("append-filter");if(A){B=$("#form-search").val()+" "+A}$("#form-search").val(B);if($("#board").length){z.app.board.reloadFilters(B)}else{$("form.search").submit()}})};f.prototype.keyboardShortcuts=function(){var z=this;Mousetrap.bind("v o",function(B){var A=$(".view-overview");if(A.length){window.location=A.attr("href")}});Mousetrap.bind("v b",function(B){var A=$(".view-board");if(A.length){window.location=A.attr("href")}});Mousetrap.bind("v c",function(B){var A=$(".view-calendar");if(A.length){window.location=A.attr("href")}});Mousetrap.bind("v l",function(B){var A=$(".view-listing");if(A.length){window.location=A.attr("href")}});Mousetrap.bind("v g",function(B){var A=$(".view-gantt");if(A.length){window.location=A.attr("href")}});Mousetrap.bind("f",function(B){B.preventDefault();var A=document.getElementById("form-search");if(A){A.focus()}});Mousetrap.bind("r",function(B){B.preventDefault();var A=$(".filter-reset").data("filter");$("#form-search").val(A);if($("#board").length){z.app.board.reloadFilters(A)}else{$("form.search").submit()}})};function n(){this.board=new k(this);this.markdown=new m();this.search=new f(this);this.swimlane=new g(this);this.dropdown=new s();this.tooltip=new r(this);this.popover=new u(this);this.task=new a(this);this.project=new o();this.subtask=new e(this);this.column=new l(this);this.file=new w(this);this.keyboardShortcuts();this.chosen();this.poll();$(".alert-fade-out").delay(5000).fadeOut(800,function(){$(this).remove()})}n.prototype.listen=function(){this.project.listen();this.popover.listen();this.markdown.listen();this.tooltip.listen();this.dropdown.listen();this.search.listen();this.task.listen();this.swimlane.listen();this.subtask.listen();this.column.listen();this.file.listen();this.search.focus();this.autoComplete();this.datePicker();this.focus()};n.prototype.refresh=function(){$(document).off();this.listen()};n.prototype.focus=function(){$("[autofocus]").each(function(z,A){$(this).focus()});$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(z){z.preventDefault()})};n.prototype.poll=function(){window.setInterval(this.checkSession,60000)};n.prototype.keyboardShortcuts=function(){var z=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(A){A.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){z.popover.close();z.dropdown.close()})};n.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};n.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};n.prototype.autoComplete=function(){$(".autocomplete").each(function(){var A=$(this);var B=A.data("dst-field");var z=A.data("dst-extra-field");if($("#form-"+B).val()==""){A.parent().find("input[type=submit]").attr("disabled","disabled")}A.autocomplete({source:A.data("search-url"),minLength:1,select:function(C,D){$("input[name="+B+"]").val(D.item.id);if(z){$("input[name="+z+"]").val(D.item[z])}A.parent().find("input[type=submit]").removeAttr("disabled")}})})};n.prototype.chosen=function(){$(".chosen-select").each(function(){var z=$(this).data("search-threshold");if(z===undefined){z=10}$(this).chosen({width:"180px",no_results_text:$(this).data("notfound"),disable_search_threshold:z})});$(".select-auto-redirect").change(function(){var z=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(z,$(this).val())})};n.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon"> <i class="fa fa-spinner fa-spin"></i></span>')};n.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};n.prototype.isVisible=function(){var z="";if(typeof document.hidden!=="undefined"){z="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){z="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){z="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){z="webkitVisibilityState"}}}}if(z!=""){return document[z]=="visible"}return true};n.prototype.formatDuration=function(z){if(z>=86400){return Math.round(z/86400)+"d"}else{if(z>=3600){return Math.round(z/3600)+"h"}else{if(z>=60){return Math.round(z/60)+"m"}}}return z+"s"};function d(){this.pasteCatcher=null}d.prototype.execute=function(){this.initialize()};d.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};d.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};d.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};d.prototype.pasteHandler=function(E){if(E.clipboardData&&E.clipboardData.items){var C=E.clipboardData.items;if(C){for(var D=0;D<C.length;D++){if(C[D].type.indexOf("image")!==-1){var B=C[D].getAsFile();var z=new FileReader();var A=this;z.onload=function(F){A.createImage(F.target.result)};z.readAsDataURL(B)}}}}else{setTimeout(this.checkInput.bind(this),100)}};d.prototype.checkInput=function(){var z=this.pasteCatcher.childNodes[0];if(z){if(z.tagName==="IMG"){this.createImage(z.src)}}this.pasteCatcher.innerHTML=""};d.prototype.createImage=function(B){var A=new Image();A.src=B;A.onload=function(){var C=B.split("base64,");var D=C[1];$("input[name=screenshot]").val(D)};var z=document.getElementById("screenshot-zone");z.innerHTML="";z.className="screenshot-pasted";z.appendChild(A);this.destroy();this.initialize()};function w(z){this.app=z;this.files=[];this.currentFile=0}w.prototype.listen=function(){var z=document.getElementById("file-dropzone");var A=this;if(z){z.ondragover=z.ondragenter=function(B){B.stopPropagation();B.preventDefault()};z.ondrop=function(B){B.stopPropagation();B.preventDefault();A.files=B.dataTransfer.files;A.show();$("#file-error-max-size").hide()};$(document).on("click","#file-browser",function(B){B.preventDefault();$("#file-form-element").get(0).click()});$(document).on("click","#file-upload-button",function(B){B.preventDefault();A.currentFile=0;A.checkFiles()});$("#file-form-element").change(function(){A.files=document.getElementById("file-form-element").files;A.show();$("#file-error-max-size").hide()})}};w.prototype.show=function(){$("#file-list").remove();if(this.files.length>0){$("#file-upload-button").prop("disabled",false);$("#file-dropzone-inner").hide();var D=jQuery("<ul>",{id:"file-list"});for(var C=0;C<this.files.length;C++){var A=jQuery("<span>",{id:"file-percentage-"+C}).append("(0%)");var B=jQuery("<progress>",{id:"file-progress-"+C,value:0});var z=jQuery("<li>",{id:"file-label-"+C}).append(B).append(" ").append(this.files[C].name).append(" ").append(A);D.append(z)}$("#file-dropzone").append(D)}else{$("#file-dropzone-inner").show()}};w.prototype.checkFiles=function(){var z=parseInt($("#file-dropzone").data("max-size"));for(var A=0;A<this.files.length;A++){if(this.files[A].size>z){$("#file-error-max-size").show();$("#file-label-"+A).addClass("file-error");$("#file-upload-button").prop("disabled",true);return}}this.uploadFiles()};w.prototype.uploadFiles=function(){if(this.files.length>0){this.uploadFile(this.files[this.currentFile])}};w.prototype.uploadFile=function(C){var z=document.getElementById("file-dropzone");var A=z.dataset.url;var D=new XMLHttpRequest();var B=new FormData();D.upload.addEventListener("progress",this.updateProgress.bind(this));D.upload.addEventListener("load",this.transferComplete.bind(this));D.open("POST",A,true);B.append("files[]",C);D.send(B)};w.prototype.updateProgress=function(z){if(z.lengthComputable){$("#file-progress-"+this.currentFile).val(z.loaded/z.total);$("#file-percentage-"+this.currentFile).text("("+Math.floor((z.loaded/z.total)*100)+"%)")}};w.prototype.transferComplete=function(){this.currentFile++;if(this.currentFile<this.files.length){this.uploadFile(this.files[this.currentFile])}else{$("#file-upload-button").prop("disabled",true);$("#file-upload-button").parent().hide();$("#file-done").show()}};function j(){}j.prototype.execute=function(){var z=$("#calendar");z.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(A){$.ajax({cache:false,url:z.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:A.id,date_due:A.start.format()})})},viewRender:function(){var A=z.data("check-url");var C={start:z.fullCalendar("getView").start.format(),end:z.fullCalendar("getView").end.format()};for(var B in C){A+="&"+B+"="+C[B]}$.getJSON(A,function(D){z.fullCalendar("removeEvents");z.fullCalendar("addEventSource",D);z.fullCalendar("rerenderEvents")})}})};function k(z){this.app=z;this.checkInterval=null;this.savingInProgress=false}k.prototype.execute=function(){this.app.swimlane.refresh();this.restoreColumnViewMode();this.compactView();this.poll();this.keyboardShortcuts();this.listen();this.dragAndDrop();$(window).on("load",this.columnScrolling);$(window).resize(this.columnScrolling)};k.prototype.poll=function(){var z=parseInt($("#board").attr("data-check-interval"));if(z>0){this.checkInterval=window.setInterval(this.check.bind(this),z*1000)}};k.prototype.reloadFilters=function(z){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("reload-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({search:z}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};k.prototype.check=function(){if(this.app.isVisible()&&!this.savingInProgress){var z=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(A){z.refresh(A)},304:function(){z.app.hideLoadingIcon()}}})}};k.prototype.save=function(C,D,z,B){var A=this;this.app.showLoadingIcon();this.savingInProgress=true;$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:C,column_id:D,swimlane_id:B,position:z}),success:function(E){A.refresh(E);this.savingInProgress=false},error:function(){A.app.hideLoadingIcon();this.savingInProgress=false}})};k.prototype.refresh=function(z){$("#board-container").replaceWith(z);this.app.refresh();this.app.swimlane.refresh();this.app.hideLoadingIcon();this.listen();this.dragAndDrop();this.compactView();this.restoreColumnViewMode();this.columnScrolling()};k.prototype.dragAndDrop=function(){var z=this;var A={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(C,J){var E=J.item;var I=E.attr("data-task-id");var K=E.attr("data-position");var H=E.attr("data-column-id");var G=E.attr("data-swimlane-id");var D=E.parent().attr("data-column-id");var B=E.parent().attr("data-swimlane-id");var F=E.index()+1;E.removeClass("draggable-item-selected");if(D!=H||B!=G||F!=K){z.changeTaskState(I);z.save(I,D,F,B)}},start:function(B,C){C.item.addClass("draggable-item-selected");C.placeholder.height(C.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");A.handle=".task-board-sort-handle"}$(".board-task-list").sortable(A)};k.prototype.changeTaskState=function(A){var z=$("div[data-task-id="+A+"]");z.addClass("task-board-saving-state");z.find(".task-board-saving-icon").show()};k.prototype.listen=function(){var z=this;$(document).on("click",".task-board",function(A){if(A.target.tagName!="A"){window.location=$(this).data("task-url")}});$(document).on("click",".filter-toggle-scrolling",function(A){A.preventDefault();z.toggleCompactView()});$(document).on("click",".filter-toggle-height",function(A){A.preventDefault();z.toggleColumnScrolling()});$(document).on("click",".board-toggle-column-view",function(){z.toggleColumnViewMode($(this).data("column-id"))})};k.prototype.toggleColumnScrolling=function(){var z=localStorage.getItem("column_scroll");if(z==undefined){z=1}localStorage.setItem("column_scroll",z==0?1:0);this.columnScrolling()};k.prototype.columnScrolling=function(){if(localStorage.getItem("column_scroll")==0){var z=80;$(".filter-max-height").show();$(".filter-min-height").hide();$(".board-rotation-wrapper").css("min-height","");$(".board-task-list").each(function(){var A=$(this).height();if(A>z){z=A}});$(".board-task-list").css("min-height",z);$(".board-task-list").css("height","")}else{$(".filter-max-height").hide();$(".filter-min-height").show();if($(".board-swimlane").length>1){$(".board-task-list").each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);$(".board-rotation-wrapper").css("min-height",320)}})}else{var z=$(window).height()-170;$(".board-task-list").css("height",z);$(".board-rotation-wrapper").css("min-height",z)}}};k.prototype.toggleCompactView=function(){var z=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",z==0?1:0);this.compactView()};k.prototype.compactView=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};k.prototype.toggleCollapsedMode=function(){var z=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(A){$(".filter-display-mode").toggle();z.refresh(A)}})};k.prototype.restoreColumnViewMode=function(){var z=this;$(".board-column-header").each(function(){var A=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+A)){z.hideColumn(A)}})};k.prototype.toggleColumnViewMode=function(z){if(localStorage.getItem("hidden_column_"+z)){this.showColumn(z)}else{this.hideColumn(z)}};k.prototype.hideColumn=function(z){$(".board-column-"+z+" .board-column-expanded").hide();$(".board-column-"+z+" .board-column-collapsed").show();$(".board-column-header-"+z+" .board-column-expanded").hide();$(".board-column-header-"+z+" .board-column-collapsed").show();$(".board-column-header-"+z).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+z).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+z+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+z+"").height())});localStorage.setItem("hidden_column_"+z,1)};k.prototype.showColumn=function(z){$(".board-column-"+z+" .board-column-expanded").show();$(".board-column-"+z+" .board-column-collapsed").hide();$(".board-column-header-"+z+" .board-column-expanded").show();$(".board-column-header-"+z+" .board-column-collapsed").hide();$(".board-column-header-"+z).removeClass("board-column-header-collapsed");$(".board-column-"+z).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+z).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+z)};k.prototype.keyboardShortcuts=function(){var z=this;Mousetrap.bind("c",function(){z.toggleCompactView()});Mousetrap.bind("s",function(){z.toggleCollapsedMode()});Mousetrap.bind("n",function(){z.app.popover.open($("#board").data("task-creation-url"))})};function l(z){this.app=z}l.prototype.listen=function(){this.dragAndDrop()};l.prototype.dragAndDrop=function(){var z=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:true,handle:"td:first i",helper:function(B,A){A.children().each(function(){$(this).width($(this).width())});return A},stop:function(B,C){var A=C.item;A.removeClass("draggable-item-selected");z.savePosition(A.data("column-id"),A.index()+1)},start:function(A,B){B.item.addClass("draggable-item-selected")}}).disableSelection()};l.prototype.savePosition=function(C,z){var B=$(".columns-table").data("save-position-url");var A=this;this.app.showLoadingIcon();$.ajax({cache:false,url:B,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({column_id:C,position:z}),complete:function(){A.app.hideLoadingIcon()}})};function g(z){this.app=z}g.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};g.prototype.expand=function(A){var B=this.getAllCollapsed();var z=B.indexOf(A);if(z>-1){B.splice(z,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(B));$(".board-swimlane-columns-"+A).css("display","table-row");$(".board-swimlane-tasks-"+A).css("display","table-row");$(".hide-icon-swimlane-"+A).css("display","inline");$(".show-icon-swimlane-"+A).css("display","none")};g.prototype.collapse=function(z){var A=this.getAllCollapsed();if(A.indexOf(z)<0){A.push(z);localStorage.setItem(this.getStorageKey(),JSON.stringify(A))}$(".board-swimlane-columns-"+z+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+z).css("display","none");$(".hide-icon-swimlane-"+z).css("display","none");$(".show-icon-swimlane-"+z).css("display","inline")};g.prototype.isCollapsed=function(z){return this.getAllCollapsed().indexOf(z)>-1};g.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};g.prototype.refresh=function(){var A=this.getAllCollapsed();for(var z=0;z<A.length;z++){this.collapse(A[z])}};g.prototype.listen=function(){var z=this;z.dragAndDrop();$(document).on("click",".board-swimlane-toggle",function(B){B.preventDefault();var A=$(this).data("swimlane-id");if(z.isCollapsed(A)){z.expand(A)}else{z.collapse(A)}})};g.prototype.dragAndDrop=function(){var z=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".swimlanes-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(B,A){A.children().each(function(){$(this).width($(this).width())});return A},stop:function(A,C){var B=C.item;B.removeClass("draggable-item-selected");z.savePosition(B.data("swimlane-id"),B.index()+1)},start:function(A,B){B.item.addClass("draggable-item-selected")}}).disableSelection()};g.prototype.savePosition=function(C,z){var B=$(".swimlanes-table").data("save-position-url");var A=this;this.app.showLoadingIcon();$.ajax({cache:false,url:B,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({swimlane_id:C,position:z}),complete:function(){A.app.hideLoadingIcon()}})};function b(z){this.app=z;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}}b.prototype.saveRecord=function(z){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(z),complete:this.app.hideLoadingIcon.bind(this)})};b.prototype.execute=function(){this.data=this.prepareData($(this.options.container).data("records"));var C=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var B=this.getDateRange(C);var z=B[0];var E=B[1];var A=$(this.options.container);var D=jQuery("<div>",{"class":"ganttview"});D.append(this.renderVerticalHeader());D.append(this.renderSlider(z,E));A.append(D);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",A).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",A).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",A).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(z);this.listenForBlockMove(z)}else{this.options.allowResizes=false;this.options.allowMoves=false}};b.prototype.renderVerticalHeader=function(){var D=jQuery("<div>",{"class":"ganttview-vtheader"});var A=jQuery("<div>",{"class":"ganttview-vtheader-item"});var C=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var z=0;z<this.data.length;z++){var B=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[z])})).append(" ");if(this.data[z].type=="task"){B.append(jQuery("<a>",{href:this.data[z].link,target:"_blank",title:this.data[z].title}).append(this.data[z].title))}else{B.append(jQuery("<a>",{href:this.data[z].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append(" ").append(jQuery("<a>",{href:this.data[z].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append(" ").append(jQuery("<a>",{href:this.data[z].link,target:"_blank"}).append(this.data[z].title))}C.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(B))}A.append(C);D.append(A);return D};b.prototype.renderSlider=function(A,C){var z=jQuery("<div>",{"class":"ganttview-slide-container"});var B=this.getDates(A,C);z.append(this.renderHorizontalHeader(B));z.append(this.renderGrid(B));z.append(this.addBlockContainers());this.addBlocks(z,A);return z};b.prototype.renderHorizontalHeader=function(z){var F=jQuery("<div>",{"class":"ganttview-hzheader"});var D=jQuery("<div>",{"class":"ganttview-hzheader-months"});var C=jQuery("<div>",{"class":"ganttview-hzheader-days"});var B=0;for(var G in z){for(var A in z[G]){var H=z[G][A].length*this.options.cellWidth;B=B+H;D.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(H-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[A]+" "+G));for(var E in z[G][A]){C.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(z[G][A][E].getDate()))}}}D.css("width",B+"px");C.css("width",B+"px");F.append(D).append(C);return F};b.prototype.renderGrid=function(z){var H=jQuery("<div>",{"class":"ganttview-grid"});var C=jQuery("<div>",{"class":"ganttview-grid-row"});for(var F in z){for(var A in z[F]){for(var E in z[F][A]){var B=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(z[F][A][E])){B.addClass("ganttview-weekend")}C.append(B)}}}var G=jQuery("div.ganttview-grid-row-cell",C).length*this.options.cellWidth;C.css("width",G+"px");H.css("width",G+"px");for(var D=0;D<this.data.length;D++){H.append(C.clone())}return H};b.prototype.addBlockContainers=function(){var A=jQuery("<div>",{"class":"ganttview-blocks"});for(var z=0;z<this.data.length;z++){A.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return A};b.prototype.addBlocks=function(A,z){var H=jQuery("div.ganttview-blocks div.ganttview-block-container",A);var B=0;for(var E=0;E<this.data.length;E++){var F=this.data[E];var I=this.daysBetween(F.start,F.end)+1;var D=this.daysBetween(z,F.start);var G=jQuery("<div>",{"class":"ganttview-block-text"});var C=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(F),css:{width:((I*this.options.cellWidth)-9)+"px","margin-left":(D*this.options.cellWidth)+"px"}}).append(G);if(I>=2){G.append(F.progress)}C.data("record",F);this.setBarColor(C,F);if(F.progress!="0%"){C.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":F.color.border,width:F.progress,opacity:0.4}}))}jQuery(H[B]).append(C);B=B+1}};b.prototype.getVerticalHeaderTooltip=function(A){var F="";if(A.type=="task"){F="<strong>"+A.column_title+"</strong> ("+A.progress+")<br/>"+A.title}else{var C=["managers","members"];for(var B in C){var D=C[B];if(!jQuery.isEmptyObject(A.users[D])){var E=jQuery("<ul>");for(var z in A.users[D]){E.append(jQuery("<li>").append(A.users[D][z]))}F+="<p><strong>"+$(this.options.container).data("label-"+D)+"</strong></p>"+E[0].outerHTML}}}return F};b.prototype.getBarTooltip=function(z){var A="";if(z.not_defined){A=$(this.options.container).data("label-not-defined")}else{if(z.type=="task"){A="<strong>"+z.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(z.assignee?z.assignee:"")+"<br/>"}A+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",z.start)+"<br/>";A+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",z.end)}return A};b.prototype.setBarColor=function(A,z){if(z.not_defined){A.addClass("ganttview-block-not-defined")}else{A.css("background-color",z.color.background);A.css("border-color",z.color.border)}};b.prototype.listenForBlockResize=function(z){var A=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var B=jQuery(this);A.updateDataAndPosition(B,z);A.saveRecord(B.data("record"))}})};b.prototype.listenForBlockMove=function(z){var A=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var B=jQuery(this);A.updateDataAndPosition(B,z);A.saveRecord(B.data("record"))}})};b.prototype.updateDataAndPosition=function(E,C){var z=jQuery("div.ganttview-slide-container",this.options.container);var I=z.scrollLeft();var F=E.offset().left-z.offset().left-1+I;var H=E.data("record");H.not_defined=false;this.setBarColor(E,H);var B=Math.round(F/this.options.cellWidth);var G=this.addDays(this.cloneDate(C),B);H.start=G;var A=E.outerWidth();var D=Math.round(A/this.options.cellWidth)-1;H.end=this.addDays(this.cloneDate(G),D);if(H.type==="task"&&D>0){jQuery("div.ganttview-block-text",E).text(H.progress)}E.attr("title",this.getBarTooltip(H));E.data("record",H);E.css("top","").css("left","").css("position","relative").css("margin-left",F+"px")};b.prototype.getDates=function(D,z){var C=[];C[D.getFullYear()]=[];C[D.getFullYear()][D.getMonth()]=[D];var B=D;while(this.compareDate(B,z)==-1){var A=this.addDays(this.cloneDate(B),1);if(!C[A.getFullYear()]){C[A.getFullYear()]=[]}if(!C[A.getFullYear()][A.getMonth()]){C[A.getFullYear()][A.getMonth()]=[]}C[A.getFullYear()][A.getMonth()].push(A);B=A}return C};b.prototype.prepareData=function(B){for(var A=0;A<B.length;A++){var C=new Date(B[A].start[0],B[A].start[1]-1,B[A].start[2],0,0,0,0);B[A].start=C;var z=new Date(B[A].end[0],B[A].end[1]-1,B[A].end[2],0,0,0,0);B[A].end=z}return B};b.prototype.getDateRange=function(B){var E=new Date();var A=new Date();for(var C=0;C<this.data.length;C++){var D=new Date();D.setTime(Date.parse(this.data[C].start));var z=new Date();z.setTime(Date.parse(this.data[C].end));if(C==0){E=D;A=z}if(this.compareDate(E,D)==1){E=D}if(this.compareDate(A,z)==-1){A=z}}if(this.daysBetween(E,A)<B){A=this.addDays(this.cloneDate(E),B)}E.setDate(E.getDate()-1);return[E,A]};b.prototype.daysBetween=function(C,z){if(!C||!z){return 0}var B=0,A=this.cloneDate(C);while(this.compareDate(A,z)==-1){B=B+1;this.addDays(A,1)}return B};b.prototype.isWeekend=function(z){return z.getDay()%6==0};b.prototype.cloneDate=function(z){return new Date(z.getTime())};b.prototype.addDays=function(z,A){z.setDate(z.getDate()+A*1);return z};b.prototype.compareDate=function(A,z){if(isNaN(A)||isNaN(z)){throw new Error(A+" - "+z)}else{if(A instanceof Date&&z instanceof Date){return(A<z)?-1:(A>z)?1:0}else{throw new TypeError(A+" - "+z)}}};function a(z){this.app=z}a.prototype.listen=function(){var z=this;var A=0;$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))});$(document).on("click",".assign-me",function(D){D.preventDefault();var B=$(this).data("current-id");var C="#"+$(this).data("target-id");if($(C+" option[value="+B+"]").length){$(C).val(B)}});$(document).on("change","select.task-reload-project-destination",function(){if(A>0){$(this).val(A)}else{A=$(this).val();var B=$(this).data("redirect").replace(/PROJECT_ID/g,A);$(".loading-icon").show();$.ajax({type:"GET",url:B,success:function(D,E,C){A=0;$(".loading-icon").hide();z.app.popover.afterSubmit(D,C,z.app.popover)}})}})};function o(){}o.prototype.listen=function(){$(".project-change-role").on("change",function(){$.ajax({cache:false,url:$(this).data("url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({id:$(this).data("id"),role:$(this).val()})})});$("#project-creation-form #form-src_project_id").on("change",function(){var z=$(this).val();if(z==0){$(".project-creation-options").hide()}else{$(".project-creation-options").show()}})};function e(z){this.app=z}e.prototype.listen=function(){var z=this;this.dragAndDrop();$(document).on("click",".subtask-toggle-status",function(B){B.preventDefault();var A=$(this);$.ajax({cache:false,url:A.attr("href"),success:function(C){if(A.hasClass("subtask-refresh-table")){$(".subtasks-table").replaceWith(C)}else{A.replaceWith(C)}z.dragAndDrop()}})});$(document).on("click",".subtask-toggle-timer",function(B){B.preventDefault();var A=$(this);$.ajax({cache:false,url:A.attr("href"),success:function(C){$(".subtasks-table").replaceWith(C);z.dragAndDrop()}})})};e.prototype.dragAndDrop=function(){var z=this;$(".draggable-row-handle").mouseenter(function(){$(this).parent().parent().addClass("draggable-item-hover")}).mouseleave(function(){$(this).parent().parent().removeClass("draggable-item-hover")});$(".subtasks-table tbody").sortable({forcePlaceholderSize:true,handle:"td:first i",helper:function(B,A){A.children().each(function(){$(this).width($(this).width())});return A},stop:function(A,B){var C=B.item;C.removeClass("draggable-item-selected");z.savePosition(C.data("subtask-id"),C.index()+1)},start:function(A,B){B.item.addClass("draggable-item-selected")}}).disableSelection()};e.prototype.savePosition=function(C,z){var B=$(".subtasks-table").data("save-position-url");var A=this;this.app.showLoadingIcon();$.ajax({cache:false,url:B,contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({subtask_id:C,position:z}),complete:function(){A.app.hideLoadingIcon()}})};function t(){}t.prototype.execute=function(){var B=$("#chart").data("metrics");var A=[];for(var z=0;z<B.length;z++){A.push([B[z].column_title,B[z].nb_tasks])}c3.generate({data:{columns:A,type:"donut"}})};function q(){}q.prototype.execute=function(){var B=$("#chart").data("metrics");var A=[];for(var z=0;z<B.length;z++){A.push([B[z].user,B[z].nb_tasks])}c3.generate({data:{columns:A,type:"donut"}})};function c(){}c.prototype.execute=function(){var F=$("#chart").data("metrics");var E=[];var z=[];var A=[];var C=d3.time.format("%Y-%m-%d");var G=d3.time.format($("#chart").data("date-format"));for(var D=0;D<F.length;D++){for(var B=0;B<F[D].length;B++){if(D==0){E.push([F[D][B]]);if(B>0){z.push(F[D][B])}}else{E[B].push(F[D][B]);if(B==0){A.push(G(C.parse(F[D][B])))}}}}c3.generate({data:{columns:E,type:"area-spline",groups:[z]},axis:{x:{type:"category",categories:A}}})};function p(){}p.prototype.execute=function(){var E=$("#chart").data("metrics");var D=[[$("#chart").data("label-total")]];var z=[];var B=d3.time.format("%Y-%m-%d");var F=d3.time.format($("#chart").data("date-format"));for(var C=0;C<E.length;C++){for(var A=0;A<E[C].length;A++){if(C==0){D.push([E[C][A]])}else{D[A+1].push(E[C][A]);if(A>0){if(D[0][C]==undefined){D[0].push(0)}D[0][C]+=E[C][A]}if(A==0){z.push(F(B.parse(E[C][A])))}}}}c3.generate({data:{columns:D},axis:{x:{type:"category",categories:z}}})};function h(z){this.app=z}h.prototype.execute=function(){var B=$("#chart").data("metrics");var C=[$("#chart").data("label")];var z=[];for(var A in B){C.push(B[A].average);z.push(B[A].title)}c3.generate({data:{columns:[C],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:z},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function y(z){this.app=z}y.prototype.execute=function(){var B=$("#chart").data("metrics");var C=[$("#chart").data("label")];var z=[];for(var A=0;A<B.length;A++){C.push(B[A].time_spent);z.push(B[A].title)}c3.generate({data:{columns:[C],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:z},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function v(z){this.app=z}v.prototype.execute=function(){var F=$("#chart").data("metrics");var E=[$("#chart").data("label-cycle")];var B=[$("#chart").data("label-lead")];var A=[];var D={};D[$("#chart").data("label-cycle")]="area";D[$("#chart").data("label-lead")]="area-spline";var z={};z[$("#chart").data("label-lead")]="#afb42b";z[$("#chart").data("label-cycle")]="#4e342e";for(var C=0;C<F.length;C++){E.push(parseInt(F[C].avg_cycle_time));B.push(parseInt(F[C].avg_lead_time));A.push(F[C].day)}c3.generate({data:{columns:[B,E],types:D,colors:z},axis:{x:{type:"category",categories:A},y:{tick:{format:this.app.formatDuration}}}})};function i(z){this.app=z}i.prototype.execute=function(){var E=$("#chart").data("metrics");var A=$("#chart").data("label-open");var z=$("#chart").data("label-closed");var F=[$("#chart").data("label-spent")];var D=[$("#chart").data("label-estimated")];var C=[];for(var B in E){F.push(parseFloat(E[B].time_spent));D.push(parseFloat(E[B].time_estimated));C.push(B=="open"?A:z)}c3.generate({data:{columns:[F,D],type:"bar"},bar:{width:{ratio:0.2}},axis:{x:{type:"category",categories:C}},legend:{show:true}})};function x(){this.routes={}}x.prototype.addRoute=function(A,z){this.routes[A]=z};x.prototype.dispatch=function(A){for(var B in this.routes){if(document.getElementById(B)){var z=Object.create(this.routes[B].prototype);this.routes[B].apply(z,[A]);z.execute();break}}};jQuery(document).ready(function(){var A=new n();var z=new x();z.addRoute("board",k);z.addRoute("calendar",j);z.addRoute("screenshot-zone",d);z.addRoute("analytic-task-repartition",t);z.addRoute("analytic-user-repartition",q);z.addRoute("analytic-cfd",c);z.addRoute("analytic-burndown",p);z.addRoute("analytic-avg-time-column",h);z.addRoute("analytic-task-time-column",y);z.addRoute("analytic-lead-cycle-time",v);z.addRoute("analytic-compare-hours",i);z.addRoute("gantt-chart",b);z.dispatch(A);A.listen()})})();
\ No newline at end of file diff --git a/assets/js/src/App.js b/assets/js/src/App.js index 976b4554..56efd706 100644 --- a/assets/js/src/App.js +++ b/assets/js/src/App.js @@ -1,44 +1,38 @@ function App() { this.board = new Board(this); this.markdown = new Markdown(); - this.sidebar = new Sidebar(); this.search = new Search(this); - this.swimlane = new Swimlane(); + this.swimlane = new Swimlane(this); this.dropdown = new Dropdown(); this.tooltip = new Tooltip(this); this.popover = new Popover(this); - this.task = new Task(); + this.task = new Task(this); this.project = new Project(); + this.subtask = new Subtask(this); + this.column = new Column(this); + this.file = new FileUpload(this); this.keyboardShortcuts(); this.chosen(); this.poll(); // Alert box fadeout - $(".alert-fade-out").delay(4000).fadeOut(800, function() { + $(".alert-fade-out").delay(5000).fadeOut(800, function() { $(this).remove(); }); - - // Reload page when a destination project is changed - var reloading_project = false; - $("select.task-reload-project-destination").change(function() { - if (! reloading_project) { - $(".loading-icon").show(); - reloading_project = true; - window.location = $(this).data("redirect").replace(/PROJECT_ID/g, $(this).val()); - } - }); } App.prototype.listen = function() { this.project.listen(); this.popover.listen(); this.markdown.listen(); - this.sidebar.listen(); this.tooltip.listen(); this.dropdown.listen(); this.search.listen(); this.task.listen(); this.swimlane.listen(); + this.subtask.listen(); + this.column.listen(); + this.file.listen(); this.search.focus(); this.autoComplete(); this.datePicker(); diff --git a/assets/js/src/Column.js b/assets/js/src/Column.js new file mode 100644 index 00000000..2c8ebbd2 --- /dev/null +++ b/assets/js/src/Column.js @@ -0,0 +1,59 @@ +function Column(app) { + this.app = app; +} + +Column.prototype.listen = function() { + this.dragAndDrop(); +}; + +Column.prototype.dragAndDrop = function() { + var self = 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: true, + handle: "td:first i", + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + + return ui; + }, + stop: function(event, ui) { + var column = ui.item; + column.removeClass("draggable-item-selected"); + self.savePosition(column.data("column-id"), column.index() + 1); + }, + start: function(event, ui) { + ui.item.addClass("draggable-item-selected"); + } + }).disableSelection(); +}; + +Column.prototype.savePosition = function(columnId, position) { + var url = $(".columns-table").data("save-position-url"); + var self = this; + + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: url, + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + "column_id": columnId, + "position": position + }), + complete: function() { + self.app.hideLoadingIcon(); + } + }); +}; diff --git a/assets/js/src/Dropdown.js b/assets/js/src/Dropdown.js index 146a3c17..61738da9 100644 --- a/assets/js/src/Dropdown.js +++ b/assets/js/src/Dropdown.js @@ -26,11 +26,11 @@ Dropdown.prototype.listen = function() { var submenuHeight = clone.outerHeight(); var submenuWidth = clone.outerWidth(); - if (offset.top + submenuHeight - $(window).scrollTop() > $(window).height()) { - clone.css('top', offset.top - submenuHeight - 5); + if (offset.top + submenuHeight - $(window).scrollTop() < $(window).height() || $(window).scrollTop() + offset.top < submenuHeight) { + clone.css('top', offset.top + $(this).height()); } else { - clone.css('top', offset.top + $(this).height()); + clone.css('top', offset.top - submenuHeight - 5); } if (offset.left + submenuWidth > $(window).width()) { diff --git a/assets/js/src/FileUpload.js b/assets/js/src/FileUpload.js new file mode 100644 index 00000000..a8816bcd --- /dev/null +++ b/assets/js/src/FileUpload.js @@ -0,0 +1,124 @@ +function FileUpload(app) { + this.app = app; + this.files = []; + this.currentFile = 0; +} + +FileUpload.prototype.listen = function() { + var dropzone = document.getElementById("file-dropzone"); + var self = this; + + if (dropzone) { + dropzone.ondragover = dropzone.ondragenter = function(e) { + e.stopPropagation(); + e.preventDefault(); + } + + dropzone.ondrop = function(e) { + e.stopPropagation(); + e.preventDefault(); + self.files = e.dataTransfer.files; + self.show(); + $("#file-error-max-size").hide(); + } + + $(document).on("click", "#file-browser", function(e) { + e.preventDefault(); + $("#file-form-element").get(0).click(); + }); + + $(document).on("click", "#file-upload-button", function(e) { + e.preventDefault(); + self.currentFile = 0; + self.checkFiles(); + }); + + $("#file-form-element").change(function() { + self.files = document.getElementById("file-form-element").files; + self.show(); + $("#file-error-max-size").hide(); + }); + } +}; + +FileUpload.prototype.show = function() { + $("#file-list").remove(); + + if (this.files.length > 0) { + $("#file-upload-button").prop("disabled", false); + $("#file-dropzone-inner").hide(); + + var ul = jQuery("<ul>", {"id": "file-list"}); + + for (var i = 0; i < this.files.length; i++) { + var percentage = jQuery("<span>", {"id": "file-percentage-" + i}).append("(0%)"); + var progress = jQuery("<progress>", {"id": "file-progress-" + i, "value": 0}); + var li = jQuery("<li>", {"id": "file-label-" + i}) + .append(progress) + .append(" ") + .append(this.files[i].name) + .append(" ") + .append(percentage); + + ul.append(li); + } + + $("#file-dropzone").append(ul); + } else { + $("#file-dropzone-inner").show(); + } +}; + +FileUpload.prototype.checkFiles = function() { + var max = parseInt($("#file-dropzone").data("max-size")); + + for (var i = 0; i < this.files.length; i++) { + if (this.files[i].size > max) { + $("#file-error-max-size").show(); + $("#file-label-" + i).addClass("file-error"); + $("#file-upload-button").prop("disabled", true); + return; + } + } + + this.uploadFiles(); +}; + +FileUpload.prototype.uploadFiles = function() { + if (this.files.length > 0) { + this.uploadFile(this.files[this.currentFile]); + } +}; + +FileUpload.prototype.uploadFile = function(file) { + var dropzone = document.getElementById("file-dropzone"); + var url = dropzone.dataset.url; + var xhr = new XMLHttpRequest(); + var fd = new FormData(); + + xhr.upload.addEventListener("progress", this.updateProgress.bind(this)); + xhr.upload.addEventListener("load", this.transferComplete.bind(this)); + + xhr.open("POST", url, true); + fd.append('files[]', file); + xhr.send(fd); +}; + +FileUpload.prototype.updateProgress = function(e) { + if (e.lengthComputable) { + $("#file-progress-" + this.currentFile).val(e.loaded / e.total); + $("#file-percentage-" + this.currentFile).text('(' + Math.floor((e.loaded / e.total) * 100) + '%)'); + } +}; + +FileUpload.prototype.transferComplete = function() { + this.currentFile++; + + if (this.currentFile < this.files.length) { + this.uploadFile(this.files[this.currentFile]); + } else { + $("#file-upload-button").prop("disabled", true); + $("#file-upload-button").parent().hide(); + $("#file-done").show(); + } +}; diff --git a/assets/js/src/Popover.js b/assets/js/src/Popover.js index 8d72dec8..3a209e8a 100644 --- a/assets/js/src/Popover.js +++ b/assets/js/src/Popover.js @@ -13,7 +13,7 @@ Popover.prototype.open = function(link) { self.app.dropdown.close(); $.get(link, function(content) { - $("body").append('<div id="popover-container"><div id="popover-content">' + content + '</div></div>'); + $("body").prepend('<div id="popover-container"><div id="popover-content">' + content + '</div></div>'); self.app.refresh(); self.router.dispatch(this.app); self.afterOpen(); @@ -35,10 +35,11 @@ Popover.prototype.onClick = function(e) { e.preventDefault(); e.stopPropagation(); - var link = e.target.getAttribute("href"); + var target = e.currentTarget || e.target; + var link = target.getAttribute("href"); if (! link) { - link = e.target.getAttribute("data-href"); + link = target.getAttribute("data-href"); } if (link) { @@ -55,26 +56,52 @@ Popover.prototype.listen = function() { Popover.prototype.afterOpen = function() { var self = this; - var taskForm = $("#task-form"); + var popoverForm = $("#popover-content .popover-form"); - if (taskForm) { - taskForm.on("submit", function(e) { + // Submit forms with Ajax request + if (popoverForm) { + popoverForm.on("submit", function(e) { e.preventDefault(); $.ajax({ type: "POST", - url: taskForm.attr("action"), - data: taskForm.serialize(), + url: popoverForm.attr("action"), + data: popoverForm.serialize(), success: function(data, textStatus, request) { - if (request.getResponseHeader("X-Ajax-Redirect")) { - window.location = request.getResponseHeader("X-Ajax-Redirect"); - } - else { - $("#popover-content").html(data); - self.afterOpen(); - } + self.afterSubmit(data, request, self); + }, + beforeSend: function() { + var button = $('.popover-form button[type="submit"]'); + button.html('<i class="fa fa-spinner fa-pulse"></i> ' + button.html()); + button.attr("disabled", true); } }); }); } + + // Submit link with Ajax request + $(document).on("click", ".popover-link", function(e) { + e.preventDefault(); + + $.ajax({ + type: "GET", + url: $(this).attr("href"), + success: function(data, textStatus, request) { + self.afterSubmit(data, request, self); + } + }); + }); +}; + +Popover.prototype.afterSubmit = function(data, request, self) { + var redirect = request.getResponseHeader("X-Ajax-Redirect"); + + if (redirect) { + window.location = redirect === 'self' ? window.location.href.split("#")[0] : redirect; + } + else { + $("#popover-content").html(data); + $("#popover-content input[autofocus]").focus(); + self.afterOpen(); + } }; diff --git a/assets/js/src/Project.js b/assets/js/src/Project.js index e2412412..19941f03 100644 --- a/assets/js/src/Project.js +++ b/assets/js/src/Project.js @@ -15,4 +15,14 @@ Project.prototype.listen = function() { }) }); }); + + $('#project-creation-form #form-src_project_id').on('change', function() { + var srcProjectId = $(this).val(); + + if (srcProjectId == 0) { + $(".project-creation-options").hide(); + } else { + $(".project-creation-options").show(); + } + }); }; diff --git a/assets/js/src/Search.js b/assets/js/src/Search.js index de4d5959..4fbfee46 100644 --- a/assets/js/src/Search.js +++ b/assets/js/src/Search.js @@ -40,6 +40,15 @@ Search.prototype.listen = function() { Search.prototype.keyboardShortcuts = function() { var self = this; + // Switch view mode for projects: go to the overview page + Mousetrap.bind("v o", function(e) { + var link = $(".view-overview"); + + if (link.length) { + window.location = link.attr('href'); + } + }); + // Switch view mode for projects: go to the board Mousetrap.bind("v b", function(e) { var link = $(".view-board"); diff --git a/assets/js/src/Sidebar.js b/assets/js/src/Sidebar.js deleted file mode 100644 index 0794d6b3..00000000 --- a/assets/js/src/Sidebar.js +++ /dev/null @@ -1,25 +0,0 @@ -function Sidebar() { -} - -Sidebar.prototype.expand = function(e) { - e.preventDefault(); - $(".sidebar-container").removeClass("sidebar-collapsed"); - $(".sidebar-collapse").show(); - $(".sidebar h2").show(); - $(".sidebar ul").show(); - $(".sidebar-expand").hide(); -}; - -Sidebar.prototype.collapse = function(e) { - e.preventDefault(); - $(".sidebar-container").addClass("sidebar-collapsed"); - $(".sidebar-expand").show(); - $(".sidebar h2").hide(); - $(".sidebar ul").hide(); - $(".sidebar-collapse").hide(); -}; - -Sidebar.prototype.listen = function() { - $(document).on("click", ".sidebar-collapse", this.collapse); - $(document).on("click", ".sidebar-expand", this.expand); -}; diff --git a/assets/js/src/Subtask.js b/assets/js/src/Subtask.js new file mode 100644 index 00000000..7670095e --- /dev/null +++ b/assets/js/src/Subtask.js @@ -0,0 +1,94 @@ +function Subtask(app) { + this.app = app; +} + +Subtask.prototype.listen = function() { + var self = this; + + this.dragAndDrop(); + + $(document).on("click", ".subtask-toggle-status", function(e) { + e.preventDefault(); + var el = $(this); + + $.ajax({ + cache: false, + url: el.attr("href"), + success: function(data) { + if (el.hasClass("subtask-refresh-table")) { + $(".subtasks-table").replaceWith(data); + } else { + el.replaceWith(data); + } + + self.dragAndDrop(); + } + }); + }); + + $(document).on("click", ".subtask-toggle-timer", function(e) { + e.preventDefault(); + var el = $(this); + + $.ajax({ + cache: false, + url: el.attr("href"), + success: function(data) { + $(".subtasks-table").replaceWith(data); + self.dragAndDrop(); + } + }); + }); +}; + +Subtask.prototype.dragAndDrop = function() { + var self = this; + + $(".draggable-row-handle").mouseenter(function() { + $(this).parent().parent().addClass("draggable-item-hover"); + }).mouseleave(function() { + $(this).parent().parent().removeClass("draggable-item-hover"); + }); + + $(".subtasks-table tbody").sortable({ + forcePlaceholderSize: true, + handle: "td:first i", + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + + return ui; + }, + stop: function(event, ui) { + var subtask = ui.item; + subtask.removeClass("draggable-item-selected"); + self.savePosition(subtask.data("subtask-id"), subtask.index() + 1); + }, + start: function(event, ui) { + ui.item.addClass("draggable-item-selected"); + } + }).disableSelection(); +}; + +Subtask.prototype.savePosition = function(subtaskId, position) { + var url = $(".subtasks-table").data("save-position-url"); + var self = this; + + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: url, + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + "subtask_id": subtaskId, + "position": position + }), + complete: function() { + self.app.hideLoadingIcon(); + } + }); +}; diff --git a/assets/js/src/Swimlane.js b/assets/js/src/Swimlane.js index 340b40a0..60dbf41f 100644 --- a/assets/js/src/Swimlane.js +++ b/assets/js/src/Swimlane.js @@ -1,4 +1,5 @@ -function Swimlane() { +function Swimlane(app) { + this.app = app; } Swimlane.prototype.getStorageKey = function() { @@ -53,6 +54,7 @@ Swimlane.prototype.refresh = function() { Swimlane.prototype.listen = function() { var self = this; + self.dragAndDrop(); $(document).on('click', ".board-swimlane-toggle", function(e) { e.preventDefault(); @@ -67,3 +69,55 @@ Swimlane.prototype.listen = function() { } }); }; + +Swimlane.prototype.dragAndDrop = function() { + var self = this; + + $(".draggable-row-handle").mouseenter(function() { + $(this).parent().parent().addClass("draggable-item-hover"); + }).mouseleave(function() { + $(this).parent().parent().removeClass("draggable-item-hover"); + }); + + $(".swimlanes-table tbody").sortable({ + forcePlaceholderSize: true, + handle: "td:first i", + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + + return ui; + }, + stop: function(event, ui) { + var swimlane = ui.item; + swimlane.removeClass("draggable-item-selected"); + self.savePosition(swimlane.data("swimlane-id"), swimlane.index() + 1); + }, + start: function(event, ui) { + ui.item.addClass("draggable-item-selected"); + } + }).disableSelection(); +}; + +Swimlane.prototype.savePosition = function(swimlaneId, position) { + var url = $(".swimlanes-table").data("save-position-url"); + var self = this; + + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: url, + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + "swimlane_id": swimlaneId, + "position": position + }), + complete: function() { + self.app.hideLoadingIcon(); + } + }); +}; diff --git a/assets/js/src/Task.js b/assets/js/src/Task.js index b3dc1b63..955a5752 100644 --- a/assets/js/src/Task.js +++ b/assets/js/src/Task.js @@ -1,10 +1,51 @@ -function Task() { +function Task(app) { + this.app = app; } Task.prototype.listen = function() { + var self = this; + var reloadingProjectId = 0; + + // Change color $(document).on("click", ".color-square", function() { $(".color-square-selected").removeClass("color-square-selected"); $(this).addClass("color-square-selected"); $("#form-color_id").val($(this).data("color-id")); }); + + // Assign to me + $(document).on("click", ".assign-me", function(e) { + e.preventDefault(); + + var currentId = $(this).data("current-id"); + var dropdownId = "#" + $(this).data("target-id"); + + if ($(dropdownId + ' option[value=' + currentId + ']').length) { + $(dropdownId).val(currentId); + } + }); + + // Reload page when a destination project is changed + $(document).on("change", "select.task-reload-project-destination", function() { + if (reloadingProjectId > 0) { + $(this).val(reloadingProjectId); + } + else { + reloadingProjectId = $(this).val(); + var url = $(this).data("redirect").replace(/PROJECT_ID/g, reloadingProjectId); + + $(".loading-icon").show(); + + $.ajax({ + type: "GET", + url: url, + success: function(data, textStatus, request) { + reloadingProjectId = 0; + $(".loading-icon").hide(); + + self.app.popover.afterSubmit(data, request, self.app.popover); + } + }); + } + }); }; diff --git a/assets/js/src/Tooltip.js b/assets/js/src/Tooltip.js index 0ec8b268..f3ef55f9 100644 --- a/assets/js/src/Tooltip.js +++ b/assets/js/src/Tooltip.js @@ -48,21 +48,6 @@ Tooltip.prototype.listen = function() { var position = $(_this).tooltip("option", "position"); position.of = $(_this); tooltip.position(position); - - // Toggle subtasks status - $('#tooltip-subtasks a').not(".popover").click(function(e) { - - e.preventDefault(); - e.stopPropagation(); - - if ($(this).hasClass("popover-subtask-restriction")) { - self.app.popover.open($(this).attr('href')); - $(_this).tooltip('close'); - } - else { - $.get($(this).attr('href'), setTooltipContent); - } - }); }); return '<i class="fa fa-spinner fa-spin"></i>'; diff --git a/composer.json b/composer.json index a557b199..ad3a1db8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "ext-hash" : "*", "ext-openssl" : "*", "ext-json" : "*", - "ext-hash" : "*", "ext-ctype" : "*", "ext-filter" : "*", "ext-session" : "*", @@ -28,7 +27,7 @@ "eluceo/ical": "0.8.0", "erusev/parsedown" : "1.6.0", "fguillot/json-rpc" : "1.0.3", - "fguillot/picodb" : "1.0.4", + "fguillot/picodb" : "1.0.5", "fguillot/simpleLogger" : "1.0.0", "fguillot/simple-validator" : "1.0.0", "paragonie/random_compat": "@stable", diff --git a/composer.lock b/composer.lock index cf0e19a8..81375dd0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3b4f1b31e90ca81927f3f839d42e21d7", - "content-hash": "ce69cdbd50f2d27eca033e98ae7ff1e4", + "hash": "0e754e4bc3eec85b3d14c748f1ed857a", + "content-hash": "c7f7baadd60fdcf8fb9e2e3a7214357f", "packages": [ { "name": "christian-riesen/base32", @@ -239,16 +239,16 @@ }, { "name": "fguillot/picodb", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/fguillot/picoDb.git", - "reference": "9ed4ee0c412dc9259d45bbc52e55c74150f7fb99" + "reference": "3b388ef12f8c57f3bca85d278a53cf6fa2d832b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/picoDb/zipball/9ed4ee0c412dc9259d45bbc52e55c74150f7fb99", - "reference": "9ed4ee0c412dc9259d45bbc52e55c74150f7fb99", + "url": "https://api.github.com/repos/fguillot/picoDb/zipball/3b388ef12f8c57f3bca85d278a53cf6fa2d832b8", + "reference": "3b388ef12f8c57f3bca85d278a53cf6fa2d832b8", "shasum": "" }, "require": { @@ -272,7 +272,7 @@ ], "description": "Minimalist database query builder", "homepage": "https://github.com/fguillot/picoDb", - "time": "2015-12-24 11:39:04" + "time": "2016-02-20 02:56:11" }, { "name": "fguillot/simple-validator", @@ -397,16 +397,16 @@ }, { "name": "paragonie/random_compat", - "version": "1.1.5", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7" + "reference": "f078eba3bcf140fd69b5fcc3ea5ac809abf729dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/dd8998b7c846f6909f4e7a5f67fabebfc412a4f7", - "reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/f078eba3bcf140fd69b5fcc3ea5ac809abf729dc", + "reference": "f078eba3bcf140fd69b5fcc3ea5ac809abf729dc", "shasum": "" }, "require": { @@ -441,7 +441,7 @@ "pseudorandom", "random" ], - "time": "2016-01-06 13:31:20" + "time": "2016-02-29 17:25:04" }, { "name": "pimple/pimple", @@ -627,16 +627,16 @@ }, { "name": "symfony/console", - "version": "v2.8.2", + "version": "v2.8.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d0239fb42f98dd02e7d342f793c5d2cdee0c478d" + "reference": "56cc5caf051189720b8de974e4746090aaa10d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d0239fb42f98dd02e7d342f793c5d2cdee0c478d", - "reference": "d0239fb42f98dd02e7d342f793c5d2cdee0c478d", + "url": "https://api.github.com/repos/symfony/console/zipball/56cc5caf051189720b8de974e4746090aaa10d44", + "reference": "56cc5caf051189720b8de974e4746090aaa10d44", "shasum": "" }, "require": { @@ -683,20 +683,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-01-14 08:33:16" + "time": "2016-02-28 16:20:50" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.2", + "version": "v2.8.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ee278f7c851533e58ca307f66305ccb9188aceda" + "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ee278f7c851533e58ca307f66305ccb9188aceda", - "reference": "ee278f7c851533e58ca307f66305ccb9188aceda", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/78c468665c9568c3faaa9c416a7134308f2d85c3", + "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3", "shasum": "" }, "require": { @@ -743,20 +743,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-01-13 10:28:07" + "time": "2016-01-27 05:14:19" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.0.1", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25" + "reference": "1289d16209491b584839022f29257ad859b8532d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/49ff736bd5d41f45240cec77b44967d76e0c3d25", - "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/1289d16209491b584839022f29257ad859b8532d", + "reference": "1289d16209491b584839022f29257ad859b8532d", "shasum": "" }, "require": { @@ -768,7 +768,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -802,13 +802,13 @@ "portable", "shim" ], - "time": "2015-11-20 09:19:13" + "time": "2016-01-20 09:13:37" } ], "packages-dev": [ { "name": "symfony/stopwatch", - "version": "v2.8.2", + "version": "v2.8.3", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", diff --git a/config.default.php b/config.default.php index d8d0ba3b..52c0c143 100644 --- a/config.default.php +++ b/config.default.php @@ -130,51 +130,6 @@ define('LDAP_GROUP_FILTER', ''); // LDAP attribute for the group name define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); -// Enable/disable Google authentication -define('GOOGLE_AUTH', false); - -// Google client id (Get this value from the Google developer console) -define('GOOGLE_CLIENT_ID', ''); - -// Google client secret key (Get this value from the Google developer console) -define('GOOGLE_CLIENT_SECRET', ''); - -// Enable/disable GitHub authentication -define('GITHUB_AUTH', false); - -// GitHub client id (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_ID', ''); - -// GitHub client secret key (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_SECRET', ''); - -// Github oauth2 authorize url -define('GITHUB_OAUTH_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize'); - -// Github oauth2 token url -define('GITHUB_OAUTH_TOKEN_URL', 'https://github.com/login/oauth/access_token'); - -// Github API url (don't forget the trailing slash) -define('GITHUB_API_URL', 'https://api.github.com/'); - -// Enable/disable Gitlab authentication -define('GITLAB_AUTH', false); - -// Gitlab application id -define('GITLAB_CLIENT_ID', ''); - -// Gitlab application secret -define('GITLAB_CLIENT_SECRET', ''); - -// Gitlab oauth2 authorize url -define('GITLAB_OAUTH_AUTHORIZE_URL', 'https://gitlab.com/oauth/authorize'); - -// Gitlab oauth2 token url -define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token'); - -// Gitlab API url endpoint (don't forget the trailing slash) -define('GITLAB_API_URL', 'https://gitlab.com/api/v3/'); - // Enable/disable the reverse proxy authentication define('REVERSE_PROXY_AUTH', false); @@ -211,6 +166,9 @@ define('ENABLE_URL_REWRITE', false); // Hide login form, useful if all your users use Google/Github/ReverseProxy authentication define('HIDE_LOGIN_FORM', false); +// Disabling logout (for external SSO authentication) +define('DISABLE_LOGOUT', false); + // Enable captcha after 3 authentication failure define('BRUTEFORCE_CAPTCHA', 3); diff --git a/data/web.config b/data/web.config new file mode 100644 index 00000000..80cc488c --- /dev/null +++ b/data/web.config @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <handlers accessPolicy="None" /> + </system.webServer> +</configuration> diff --git a/doc/analytics.markdown b/doc/analytics.markdown index 13657b56..d72fc383 100644 --- a/doc/analytics.markdown +++ b/doc/analytics.markdown @@ -62,9 +62,4 @@ This chart show the average lead and cycle time for the last 1000 tasks over tim Those metrics are calculated and recorded every day for the whole project. -Don't forget to run the daily job for statistics calculation -------------------------------------------------------- - -To generate accurate analytic data, you should run the daily cronjob **project daily statistics**. - -[Read the documentation of Kanboard CLI](cli.markdown) +Note: Don't forget to run the [daily cronjob](cronjob.markdown) to have accurate statistics. diff --git a/doc/api-board-procedures.markdown b/doc/api-board-procedures.markdown index d8503d6d..6f8a878e 100644 --- a/doc/api-board-procedures.markdown +++ b/doc/api-board-procedures.markdown @@ -156,261 +156,3 @@ Response example: ] } ``` - -## getColumns - -- Purpose: **Get all columns information for a given project** -- Parameters: - - **project_id** (integer, required) -- Result on success: **columns properties** -- Result on failure: **empty list** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getColumns", - "id": 887036325, - "params": [ - 1 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 887036325, - "result": [ - { - "id": "1", - "title": "Backlog", - "position": "1", - "project_id": "1", - "task_limit": "0" - }, - { - "id": "2", - "title": "Ready", - "position": "2", - "project_id": "1", - "task_limit": "0" - }, - { - "id": "3", - "title": "Work in progress", - "position": "3", - "project_id": "1", - "task_limit": "0" - } - ] -} -``` - -## getColumn - -- Purpose: **Get a single column** -- Parameters: - - **column_id** (integer, required) -- Result on success: **column properties** -- Result on failure: **null** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "getColumn", - "id": 1242049935, - "params": [ - 2 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 1242049935, - "result": { - "id": "2", - "title": "Youpi", - "position": "2", - "project_id": "1", - "task_limit": "5" - } -} -``` - -## moveColumnUp - -- Purpose: **Move up the column position** -- Parameters: - - **project_id** (integer, required) - - **column_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "moveColumnUp", - "id": 99275573, - "params": [ - 1, - 2 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 99275573, - "result": true -} -``` - -## moveColumnDown - -- Purpose: **Move down the column position** -- Parameters: - - **project_id** (integer, required) - - **column_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "moveColumnDown", - "id": 957090649, - "params": { - "project_id": 1, - "column_id": 2 - } -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 957090649, - "result": true -} -``` - -## updateColumn - -- Purpose: **Update column properties** -- Parameters: - - **column_id** (integer, required) - - **title** (string, required) - - **task_limit** (integer, optional) - - **description** (string, optional) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "updateColumn", - "id": 480740641, - "params": [ - 2, - "Boo", - 5 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 480740641, - "result": true -} -``` - -## addColumn - -- Purpose: **Add a new column** -- Parameters: - - **project_id** (integer, required) - - **title** (string, required) - - **task_limit** (integer, optional) - - **description** (string, optional) -- Result on success: **column_id** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "addColumn", - "id": 638544704, - "params": [ - 1, - "Boo" - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 638544704, - "result": 5 -} -``` - -## removeColumn - -- Purpose: **Remove a column** -- Parameters: - - **column_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "removeColumn", - "id": 1433237746, - "params": [ - 1 - ] -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 1433237746, - "result": true -} -``` diff --git a/doc/api-column-procedures.markdown b/doc/api-column-procedures.markdown new file mode 100644 index 00000000..c5d2793b --- /dev/null +++ b/doc/api-column-procedures.markdown @@ -0,0 +1,229 @@ +API Column Procedures +===================== + +## getColumns + +- Purpose: **Get all columns information for a given project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **columns properties** +- Result on failure: **empty list** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getColumns", + "id": 887036325, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 887036325, + "result": [ + { + "id": "1", + "title": "Backlog", + "position": "1", + "project_id": "1", + "task_limit": "0" + }, + { + "id": "2", + "title": "Ready", + "position": "2", + "project_id": "1", + "task_limit": "0" + }, + { + "id": "3", + "title": "Work in progress", + "position": "3", + "project_id": "1", + "task_limit": "0" + } + ] +} +``` + +## getColumn + +- Purpose: **Get a single column** +- Parameters: + - **column_id** (integer, required) +- Result on success: **column properties** +- Result on failure: **null** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getColumn", + "id": 1242049935, + "params": [ + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1242049935, + "result": { + "id": "2", + "title": "Youpi", + "position": "2", + "project_id": "1", + "task_limit": "5" + } +} +``` + +## changeColumnPosition + +- Purpose: **Change the column position** +- Parameters: + - **project_id** (integer, required) + - **column_id** (integer, required) + - **position** (integer, required, must be >= 1) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "changeColumnPosition", + "id": 99275573, + "params": [ + 1, + 2, + 3 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 99275573, + "result": true +} +``` + +## updateColumn + +- Purpose: **Update column properties** +- Parameters: + - **column_id** (integer, required) + - **title** (string, required) + - **task_limit** (integer, optional) + - **description** (string, optional) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateColumn", + "id": 480740641, + "params": [ + 2, + "Boo", + 5 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 480740641, + "result": true +} +``` + +## addColumn + +- Purpose: **Add a new column** +- Parameters: + - **project_id** (integer, required) + - **title** (string, required) + - **task_limit** (integer, optional) + - **description** (string, optional) +- Result on success: **column_id** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "addColumn", + "id": 638544704, + "params": [ + 1, + "Boo" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 638544704, + "result": 5 +} +``` + +## removeColumn + +- Purpose: **Remove a column** +- Parameters: + - **column_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "removeColumn", + "id": 1433237746, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1433237746, + "result": true +} +``` diff --git a/doc/api-file-procedures.markdown b/doc/api-file-procedures.markdown index bd05ce6b..930be733 100644 --- a/doc/api-file-procedures.markdown +++ b/doc/api-file-procedures.markdown @@ -1,7 +1,7 @@ API File Procedures =================== -## createFile +## createTaskFile - Purpose: **Create and upload a new task attachment** - Parameters: @@ -18,7 +18,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "createFile", + "method": "createTaskFile", "id": 94500810, "params": [ 1, @@ -39,7 +39,7 @@ Response example: } ``` -## getAllFiles +## getAllTaskFiles - Purpose: **Get all files attached to task** - Parameters: @@ -52,7 +52,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "getAllFiles", + "method": "getAllTaskFiles", "id": 1880662820, "params": { "task_id": 1 @@ -83,7 +83,7 @@ Response example: } ``` -## getFile +## getTaskFile - Purpose: **Get file information** - Parameters: @@ -96,7 +96,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "getFile", + "method": "getTaskFile", "id": 318676852, "params": [ "1" @@ -123,7 +123,7 @@ Response example: } ``` -## downloadFile +## downloadTaskFile - Purpose: **Download file contents (encoded in base64)** - Parameters: @@ -136,7 +136,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "downloadFile", + "method": "downloadTaskFile", "id": 235943344, "params": [ "1" @@ -154,7 +154,7 @@ Response example: } ``` -## removeFile +## removeTaskFile - Purpose: **Remove file** - Parameters: @@ -167,7 +167,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "removeFile", + "method": "removeTaskFile", "id": 447036524, "params": [ "1" @@ -185,7 +185,7 @@ Response example: } ``` -## removeAllFiles +## removeAllTaskFiles - Purpose: **Remove all files associated to a task** - Parameters: @@ -198,7 +198,7 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "removeAllFiles", + "method": "removeAllTaskFiles", "id": 593312993, "params": { "task_id": 1 diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown index 34559df5..bb14b008 100644 --- a/doc/api-json-rpc.markdown +++ b/doc/api-json-rpc.markdown @@ -52,6 +52,7 @@ Usage - [Projects](api-project-procedures.markdown) - [Project Permissions](api-project-permission-procedures.markdown) - [Boards](api-board-procedures.markdown) +- [Columns](api-column-procedures.markdown) - [Swimlanes](api-swimlane-procedures.markdown) - [Categories](api-category-procedures.markdown) - [Automatic Actions](api-action-procedures.markdown) diff --git a/doc/api-swimlane-procedures.markdown b/doc/api-swimlane-procedures.markdown index ab288c0c..c58e56c9 100644 --- a/doc/api-swimlane-procedures.markdown +++ b/doc/api-swimlane-procedures.markdown @@ -235,12 +235,13 @@ Response example: } ``` -## moveSwimlaneUp +## changeSwimlanePosition -- Purpose: **Move up the swimlane position** +- Purpose: **Move up the swimlane position** (only for active swimlanes) - Parameters: - **project_id** (integer, required) - **swimlane_id** (integer, required) + - **position** (integer, required, must be >= 1) - Result on success: **true** - Result on failure: **false** @@ -249,11 +250,12 @@ Request example: ```json { "jsonrpc": "2.0", - "method": "moveSwimlaneUp", + "method": "changeSwimlanePosition", "id": 99275573, "params": [ 1, - 2 + 2, + 3 ] } ``` @@ -268,39 +270,6 @@ Response example: } ``` -## moveSwimlaneDown - -- Purpose: **Move down the swimlane position** -- Parameters: - - **project_id** (integer, required) - - **swimlane_id** (integer, required) -- Result on success: **true** -- Result on failure: **false** - -Request example: - -```json -{ - "jsonrpc": "2.0", - "method": "moveSwimlaneDown", - "id": 957090649, - "params": { - "project_id": 1, - "swimlane_id": 2 - } -} -``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 957090649, - "result": true -} -``` - ## updateSwimlane - Purpose: **Update swimlane properties** diff --git a/doc/api-user-procedures.markdown b/doc/api-user-procedures.markdown index 6ecf12c6..6c09355d 100644 --- a/doc/api-user-procedures.markdown +++ b/doc/api-user-procedures.markdown @@ -113,6 +113,48 @@ Response example: } ``` +## getUserByName + +- Purpose: **Get user information** +- Parameters: + - **username** (string, required) +- Result on success: **user properties** +- Result on failure: **null** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getUserByName", + "id": 1769674782, + "params": { + "username": "biloute" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1769674782, + "result": { + "id": "1", + "username": "biloute", + "password": "$2y$10$dRs6pPoBu935RpmsrhmbjevJH5MgZ7Kr9QrnVINwwyZ3.MOwqg.0m", + "role": "app-user", + "is_ldap_user": "0", + "name": "", + "email": "", + "google_id": null, + "github_id": null, + "notifications_enabled": "0" + } +} +``` + ## getAllUsers - Purpose: **Get all available users** @@ -220,3 +262,96 @@ Response example: "result": true } ``` + +## disableUser + +- Purpose: **Disable a user** +- Parameters: + - **user_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "disableUser", + "id": 2094191872, + "params": { + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2094191872, + "result": true +} +``` + +## enableUser + +- Purpose: **Enable a user** +- Parameters: + - **user_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "enableUser", + "id": 2094191872, + "params": { + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2094191872, + "result": true +} +``` + +## isActiveUser + +- Purpose: **Check if a user is active** +- Parameters: + - **user_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "isActiveUser", + "id": 2094191872, + "params": { + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2094191872, + "result": true +} +``` diff --git a/doc/centos-installation.markdown b/doc/centos-installation.markdown index d0fd6a00..576119b4 100644 --- a/doc/centos-installation.markdown +++ b/doc/centos-installation.markdown @@ -1,6 +1,8 @@ Centos Installation =================== +Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown). + Centos 7 -------- diff --git a/doc/cli.markdown b/doc/cli.markdown index bcb478dd..9334d84b 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -4,7 +4,7 @@ Command Line Interface Kanboard provides a simple command line interface that can be used from any Unix terminal. This tool can be used only on the local machine. -This feature is useful to run commands outside the web server process by example running a huge report. +This feature is useful to run commands outside of the web server processes. Usage ----- @@ -28,6 +28,7 @@ Options: -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: + cronjob Execute daily cronjob help Displays help for a command list Lists commands export @@ -42,6 +43,8 @@ Available commands: notification:overdue-tasks Send notifications for overdue tasks projects projects:daily-stats Calculate daily statistics for all projects + trigger + trigger:tasks Trigger scheduler event for all tasks ``` Available commands @@ -116,7 +119,7 @@ Emails will be sent to all users with notifications enabled. You can also display the overdue tasks with the flag `--show`: ```bash -$ ./kanboard notification:overdue-tasks --show +./kanboard notification:overdue-tasks --show +-----+---------+------------+------------+--------------+----------+ | Id | Title | Due date | Project Id | Project name | Assignee | +-----+---------+------------+------------+--------------+----------+ @@ -125,20 +128,22 @@ $ ./kanboard notification:overdue-tasks --show +-----+---------+------------+------------+--------------+----------+ ``` -Cronjob example: - -```bash -# Everyday at 8am we check for due tasks -0 8 * * * cd /path/to/kanboard && ./kanboard notification:overdue-tasks >/dev/null 2>&1 -``` - ### Run daily project stats calculation -You can add a background task to calculate the project statistics every day: +This command calculate the statistics of each project: ```bash -$ ./kanboard projects:daily-stats +./kanboard projects:daily-stats Run calculation for Project #0 Run calculation for Project #1 Run calculation for Project #10 ``` + +### Trigger for tasks + +This command send a "daily cronjob event" to all open tasks of each project. + +```bash +./kanboard trigger:tasks +Trigger task event: project_id=2, nb_tasks=1 +``` diff --git a/doc/config.markdown b/doc/config.markdown index 393efbae..150cb6dc 100644 --- a/doc/config.markdown +++ b/doc/config.markdown @@ -174,34 +174,6 @@ define('LDAP_GROUP_FILTER', ''); define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); ``` -Google Authentication settings ------------------------------- - -```php -// Enable/disable Google authentication -define('GOOGLE_AUTH', false); - -// Google client id (Get this value from the Google developer console) -define('GOOGLE_CLIENT_ID', ''); - -// Google client secret key (Get this value from the Google developer console) -define('GOOGLE_CLIENT_SECRET', ''); -``` - -Github Authentication settings ------------------------------- - -```php -// Enable/disable GitHub authentication -define('GITHUB_AUTH', false); - -// GitHub client id (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_ID', ''); - -// GitHub client secret key (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_SECRET', ''); -``` - Reverse-Proxy Authentication settings ------------------------------------- @@ -295,6 +267,9 @@ define('API_AUTHENTICATION_HEADER', ''); // Hide login form, useful if all your users use Google/Github/ReverseProxy authentication define('HIDE_LOGIN_FORM', false); +// Disabling logout (for external SSO authentication) +define('DISABLE_LOGOUT', false); + // Override API token stored in the database, useful for automated tests define('API_AUTHENTICATION_TOKEN', 'My unique API Token'); ``` diff --git a/doc/cronjob.markdown b/doc/cronjob.markdown new file mode 100644 index 00000000..32f12888 --- /dev/null +++ b/doc/cronjob.markdown @@ -0,0 +1,32 @@ +Background Job Scheduling +========================= + +To work properly, Kanboard requires that a background job run on a daily basis. +Usually on Unix platforms, this process is done by `cron`. + +This background job is necessary for these features: + +- Reports and analytics (calculate daily stats of each projects) +- Send overdue task notifications +- Execute automatic actions connected to the event "Daily background job for tasks" + +Configuration on Unix and Linux platforms +----------------------------------------- + +There are multiple ways to define a cronjob on Unix/Linux operating systems, this example is for Ubuntu 14.04. +The procedure is similar to other systems. + +Edit the crontab of your web server user: + +```bash +sudo crontab -u www-data -e +``` + +Example to execute the daily cronjob at 8am: + +```bash +0 8 * * * cd /path/to/kanboard && ./kanboard cronjob >/dev/null 2>&1 +``` + +Note: the cronjob process must have write access to the database in case you are using Sqlite. +Usually, running the cronjob under the web server user is enough. diff --git a/doc/debian-installation.markdown b/doc/debian-installation.markdown index 147fe452..ec956049 100644 --- a/doc/debian-installation.markdown +++ b/doc/debian-installation.markdown @@ -1,6 +1,8 @@ How to install Kanboard on Debian? ================================== +Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown). + Debian 8 (Jessie) ----------------- diff --git a/doc/docker.markdown b/doc/docker.markdown index fe72a6c8..3f13e954 100644 --- a/doc/docker.markdown +++ b/doc/docker.markdown @@ -2,7 +2,18 @@ How to run Kanboard with Docker? ================================ Kanboard can run easily with [Docker](https://www.docker.com). -There is a `Dockerfile` in the repository to build your own container. + +The image size is approximately **50MB** and contains: + +- [Alpine Linux](http://alpinelinux.org/) +- The [process manager S6](http://skarnet.org/software/s6/) +- Nginx +- PHP-FPM + +The Kanboard cronjob is also running everyday at midnight. +URL rewriting is enabled in the included config file. + +When the container is running, the memory utilization is around **20MB**. Use the stable version ---------------------- @@ -17,7 +28,7 @@ docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:stable Use the development version (automated build) --------------------------------------------- -Every new commit on the repository trigger a new build on [Docker Hub](https://registry.hub.docker.com/u/kanboard/kanboard/). +Every new commit on the repository trigger a new build on the [Docker Hub](https://registry.hub.docker.com/u/kanboard/kanboard/). ```bash docker pull kanboard/kanboard @@ -29,31 +40,57 @@ The tag **latest** is the **development version** of Kanboard, use at your own r Build your own Docker image --------------------------- +There is a `Dockerfile` in the Kanboard repository to build your own image. Clone the Kanboard repository and run the following command: ```bash docker build -t youruser/kanboard:master . ``` -To run your image in background on the port 80: +or ```bash -docker run -d --name kanboard -p 80:80 -t youruser/kanboard:master +make docker-image ``` -Store your data on a volume ---------------------------- - -By default Kanboard will store attachments and the Sqlite database in the directory data. Run this command to use a custom volume path: +To run your container in background on the port 80: ```bash -docker run -d --name kanboard -v /your/local/data/folder:/var/www/html/data -p 80:80 -t kanboard/kanboard:master +docker run -d --name kanboard -p 80:80 -t youruser/kanboard:master ``` +Volumes +------- + +You can attach 2 volumes to your container: + +- Data folder: `/var/www/kanboard/data` +- Plugins folder: `/var/www/kanboard/plugins` + +Use the flag `-v` to mount a volume on the host machine like described in [official Docker documentation](https://docs.docker.com/engine/userguide/containers/dockervolumes/). + +Upgrade your container +---------------------- + +- Pull the new image +- Remove the old container +- Restart a new container with the same volumes + +Environment variables +--------------------- + +The list of environment variables is available on [this page](env.markdown). + +Config files +------------ + +- The container already include a custom config file located at `/var/www/kanboard/config.php`. +- You can store your own config file on the data volume: `/var/www/kanboard/data/config.php`. + References ---------- - [Official Kanboard images](https://registry.hub.docker.com/u/kanboard/kanboard/) - [Docker documentation](https://docs.docker.com/) -- [Dockerfile stable version](https://github.com/kanboard/docker/blob/master/Dockerfile) +- [Dockerfile stable version](https://github.com/kanboard/docker) - [Dockerfile dev version](https://github.com/fguillot/kanboard/blob/master/Dockerfile) diff --git a/doc/env.markdown b/doc/env.markdown new file mode 100644 index 00000000..a01d0a6f --- /dev/null +++ b/doc/env.markdown @@ -0,0 +1,11 @@ +Environment Variables +===================== + +Environment variables maybe useful when Kanboard is deployed as container (Docker). + +| Variable | Description | +|---------------|---------------------------------------------------------------------------------------------------------------------------------| +| DATABASE_URL | `[database type]://[username]:[password]@[host]:[port]/[database name]`, example: `postgres://foo:foo@myserver:5432/kanboard` | +| DEBUG | Enable/Disable debug mode | +| DEBUG_FILE | Debug file location, `DEBUG_FILE=php://stderr` | +| ENABLE_SYSLOG | Enable/Disable logging to Syslog: `ENABLE_SYSLOG=1` | diff --git a/doc/freebsd-installation.markdown b/doc/freebsd-installation.markdown index 84b35ad8..7b36dff1 100644 --- a/doc/freebsd-installation.markdown +++ b/doc/freebsd-installation.markdown @@ -55,7 +55,7 @@ Generally 3 elements have to be installed: Fetch and extract ports... ```bash -$ portsnap fetch +$ portsnap fetch $ portsnap extract ``` @@ -122,6 +122,7 @@ there is no need to install it manually. Please note ----------- -Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment, +- Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment, fork and suggest updates! -
\ No newline at end of file +- Some features of Kanboard require that you run [a daily background job](cronjob.markdown). + diff --git a/doc/github-authentication.markdown b/doc/github-authentication.markdown deleted file mode 100644 index ba0f371f..00000000 --- a/doc/github-authentication.markdown +++ /dev/null @@ -1,80 +0,0 @@ -Github Authentication -===================== - -Requirements ------------- - -OAuth Github API credentials (available in your [Settings > Applications > Developer applications](https://github.com/settings/applications)) - -How does this work? -------------------- - -The Github authentication in Kanboard uses the [OAuth 2.0](http://oauth.net/2/) protocol, so any user of Kanboard can be linked to a Github account. - -That means you can use your Github account to login on Kanboard. - -How to link a Github account ----------------------------- - -1. Go to your user profile -2. Click on **External accounts** -3. Click on the link **Link my Github Account** -4. You are redirected to the **Github Authorize application form** -5. Authorize Kanboard by clicking on the button **Accept** -6. Your account is now linked - -Now, on the login page you can be authenticated in one click with the link **Login with my Github Account**. - -Your name and email are automatically updated from your Github Account if defined. - -Installation instructions -------------------------- - -### Setting up OAuth 2.0 - -- On Github, go to the page [Register a new OAuth application](https://github.com/settings/applications/new) -- Just follow the [official Github documentation](https://developer.github.com/guides/basics-of-authentication/#registering-your-app) -- In Kanboard, you can get the **callback url** in **Settings > Integrations > Github Authentication** - -### Setting up Kanboard - -Either create a new `config.php` file or rename the `config.default.php` file and set the following values: - -```php -// Enable/disable Github authentication -define('GITHUB_AUTH', true); - -// Github client id (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_ID', 'YOUR_GITHUB_CLIENT_ID'); - -// Github client secret key (Copy it from your settings -> Applications -> Developer applications) -define('GITHUB_CLIENT_SECRET', 'YOUR_GITHUB_CLIENT_SECRET'); -``` - -### Github Entreprise - -To use this authentication method with Github Enterprise you have to change the default urls. - -Replace these values by your self-hosted instance of Github: - -```php -// Github oauth2 authorize url -define('GITHUB_OAUTH_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize'); - -// Github oauth2 token url -define('GITHUB_OAUTH_TOKEN_URL', 'https://github.com/login/oauth/access_token'); - -// Github API url (don't forget the slash at the end) -define('GITHUB_API_URL', 'https://api.github.com/'); -``` - -Notes ------ - -Kanboard uses these information from your public Github profile: - -- Full name -- Public email address -- Github unique id - -The Github unique id is used to link the local user account and the Github account. diff --git a/doc/gitlab-authentication.markdown b/doc/gitlab-authentication.markdown deleted file mode 100644 index 8d2f0000..00000000 --- a/doc/gitlab-authentication.markdown +++ /dev/null @@ -1,83 +0,0 @@ -Gitlab Authentication -===================== - -Requirements ------------- - -- Account on [Gitlab.com](https://gitlab.com) or you own self-hosted Gitlab instance -- Have Kanboard registered as application in Gitlab - -How does this work? -------------------- - -The Gitlab authentication in Kanboard uses the [OAuth 2.0](http://oauth.net/2/) protocol, so any user of Kanboard can be linked to a Gitlab account. - -That means you can use your Gitlab account to login on Kanboard. - -How to link a Gitlab account ----------------------------- - -1. Go to your user profile -2. Click on **External accounts** -3. Click on the link **Link my Gitlab Account** -4. You are redirected to the **Gitlab authorization form** -5. Authorize Kanboard by clicking on the button **Accept** -6. Your account is now linked - -Now, on the login page you can be authenticated in one click with the link **Login with my Gitlab Account**. - -Your name and email are automatically updated from your Gitlab Account if defined. - -Installation instructions -------------------------- - -### Setting up OAuth 2.0 - -- On Gitlab, register a new application by following the [official documentation](http://doc.gitlab.com/ce/integration/oauth_provider.html) -- In Kanboard, you can get the **callback url** in **Settings > Integrations > Gitlab Authentication**, just copy and paste the url - -### Setting up Kanboard - -Either create a new `config.php` file or rename the `config.default.php` file and set the following values: - -```php -// Enable/disable Gitlab authentication -define('GITLAB_AUTH', true); - -// Gitlab application id -define('GITLAB_CLIENT_ID', 'YOUR_APPLICATION_ID'); - -// Gitlab application secret -define('GITLAB_CLIENT_SECRET', 'YOUR_APPLICATION_SECRET'); -``` - -### Custom endpoints for self-hosted Gitlab - -Change these default values if you use a self-hosted instance of Gitlab: - -```php -// Gitlab oauth2 authorize url -define('GITLAB_OAUTH_AUTHORIZE_URL', 'https://gitlab.com/oauth/authorize'); - -// Gitlab oauth2 token url -define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token'); - -// Gitlab API url endpoint (don't forget the slash at the end) -define('GITLAB_API_URL', 'https://gitlab.com/api/v3/'); -``` - -Notes ------ - -Kanboard uses these information from your Gitlab profile: - -- Full name -- Email address -- Gitlab unique id - -The Gitlab unique id is used to link the local user account and the Gitlab account. - -Known issues ------------- - -Gitlab OAuth will work only with url rewrite enabled. At the moment, Gitlab doesn't support callback url with query string parameters. See [Gitlab issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/2443) diff --git a/doc/google-authentication.markdown b/doc/google-authentication.markdown deleted file mode 100644 index 0f4f3ec1..00000000 --- a/doc/google-authentication.markdown +++ /dev/null @@ -1,64 +0,0 @@ -Google Authentication -===================== - -Requirements ------------- - -OAuth Google API credentials (available in the Google Developer Console) - -How does this work? -------------------- - -- The Google authentication in Kanboard use the OAuth 2.0 protocol -- Any user account in Kanboard can be linked to a Google Account -- When a Kanboard user account is linked to Google, you can login with one click - -Procedure to link a Google Account ----------------------------------- - -1. Go to your user profile -2. Click on **External accounts** -3. Click on the link **Link my Google Account** -4. You are redirected to the **Google Consent screen** -5. Authorize Kanboard by clicking on the button **Accept** -6. Your account is now linked - -Now, on the login page you can be authenticated in one click with the link **Login with my Google Account**. - -Your name and email are automatically updated from your Google Account. - -Installation instructions -------------------------- - -### Setting up OAuth 2.0 in Google Developer Console - -- Follow the [official Google documentation](https://developers.google.com/accounts/docs/OAuth2Login#appsetup) to create a new application -- In Kanboard, you can get the **redirect url** in **Settings > Integrations > Google Authentication** - -### Setting up Kanboad - -Create a custom `config.php` file or copy the `config.default.php` file: - -```php -<?php - -// Enable/disable Google authentication -define('GOOGLE_AUTH', true); // Set this value to true - -// Google client id (Get this value from the Google developer console) -define('GOOGLE_CLIENT_ID', 'YOUR_CLIENT_ID'); - -// Google client secret key (Get this value from the Google developer console) -define('GOOGLE_CLIENT_SECRET', 'YOUR_CLIENT_SECRET'); -``` - -Notes ------ - -Kanboard use these information from your Google profile: - -- Full name -- Email address -- Google unique id - -The Google unique id is used to link together the local user account and the Google account. diff --git a/doc/heroku.markdown b/doc/heroku.markdown index 56d79bc9..f145f70e 100644 --- a/doc/heroku.markdown +++ b/doc/heroku.markdown @@ -35,5 +35,5 @@ heroku open Limitations ----------- -The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot. -We may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3). +- The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot. You may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3). +- Some features of Kanboard require that you run [a daily background job](cronjob.markdown). diff --git a/doc/index.markdown b/doc/index.markdown index 1e95fe06..7a3ca8e5 100644 --- a/doc/index.markdown +++ b/doc/index.markdown @@ -93,6 +93,7 @@ Technical details - [Installation on Ubuntu](ubuntu-installation.markdown) - [Installation on Debian](debian-installation.markdown) - [Installation on Centos](centos-installation.markdown) +- [Installation on OpenSuse](suse-installation.markdown) - [Installation on FreeBSD](freebsd-installation.markdown) - [Installation on Windows Server with IIS](windows-iis-installation.markdown) - [Installation on Windows Server with Apache](windows-apache-installation.markdown) @@ -102,7 +103,9 @@ Technical details ### Configuration +- [Daily background job](cronjob.markdown) - [Config file](config.markdown) +- [Environment variables](env.markdown) - [Email configuration](email-configuration.markdown) - [URL rewriting](nice-urls.markdown) @@ -117,9 +120,6 @@ Technical details - [LDAP authentication](ldap-authentication.markdown) - [LDAP group synchronization](ldap-group-sync.markdown) - [LDAP parameters](ldap-parameters.markdown) -- [Google authentication](google-authentication.markdown) -- [Github authentication](github-authentication.markdown) -- [Gitlab authentication](gitlab-authentication.markdown) - [Reverse proxy authentication](reverse-proxy-authentication.markdown) ### Contributors diff --git a/doc/installation.markdown b/doc/installation.markdown index b208be8a..dd4283f8 100644 --- a/doc/installation.markdown +++ b/doc/installation.markdown @@ -39,3 +39,8 @@ Security - Don't forget to change the default user/password - Don't allow everybody to access to the directory `data` from the URL. There is already a `.htaccess` for Apache but nothing for Nginx. + +Notes +----- + +- Some features of Kanboard require that you run [a daily background job](cronjob.markdown) diff --git a/doc/keyboard-shortcuts.markdown b/doc/keyboard-shortcuts.markdown index 8c809b77..beb15d3d 100644 --- a/doc/keyboard-shortcuts.markdown +++ b/doc/keyboard-shortcuts.markdown @@ -6,6 +6,7 @@ Keyboard shortcuts availability depends of the page you are presently. Project views (Board, Calendar, List, Gantt) -------------------------------------------- +- Switch to the project overview = **v o** - Switch to the board view = **v b** (press on **v** then **b**) - Switch to the calendar view = **v c** - Switch to the list view = **v l** @@ -25,4 +26,4 @@ Application - Go to the search box = **f** - Reset the search box = **r** - Close dialog box = **ESC** -- Submit form = **CTRL+ENTER** or **⌘+ENTER**
\ No newline at end of file +- Submit form = **CTRL+ENTER** or **⌘+ENTER** diff --git a/doc/nice-urls.markdown b/doc/nice-urls.markdown index 7cb8dbac..9fbb3510 100644 --- a/doc/nice-urls.markdown +++ b/doc/nice-urls.markdown @@ -86,3 +86,37 @@ define('ENABLE_URL_REWRITE', true); ``` Adapt the example above according to your own configuration. + +IIS configuration example +--------------------------- + +Create a web.config in you installation folder: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <rewrite> + <rules> + <rule name="Imported Rule 1" stopProcessing="true"> + <match url="^" ignoreCase="false" /> + <conditions logicalGrouping="MatchAll"> + <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" appendQueryString="true" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> + +``` + +In your Kanboard `config.php`: + +```php +define('ENABLE_URL_REWRITE', true); +``` + +Adapt the example above according to your own configuration. + diff --git a/doc/plugin-external-link.markdown b/doc/plugin-external-link.markdown new file mode 100644 index 00000000..36252aff --- /dev/null +++ b/doc/plugin-external-link.markdown @@ -0,0 +1,78 @@ +External Link Providers +======================= + +This functionality allows you to link a task to additional items stored on another system. + +For example, you can link a task to: + +- Traditional web page +- Attachment (PDF documents stored on the web, archive...) +- Any ticketing system (bug tracker, customer support ticket...) + +Each item has a type, a URL, a dependency type and a title. + +By default, Kanboard includes two kinds of providers: + +- Web Link: You copy and paste a link and Kanboard will fetch the page title automatically +- Attachment: Link to anything that is not a web page + +Workflow +-------- + +1. The end-user copy and paste the URL to the form and submit +2. If the link type is "auto", Kanboard will loop through all providers registered until there is a match +3. Then, the link provider returns a object that implements the interface `ExternalLinkInterface` +4. A form is shown to the user with all pre-filled data before to save the link + +Interfaces +---------- + +To implement a new link provider from a plugin, you need to create 2 classes that implement those interfaces: + +- `Kanboard\Core\ExternalLink\ExternalLinkProviderInterface` +- `Kanboard\Core\ExternalLink\ExternalLinkInterface` + +### ExternalLinkProviderInterface + +| Method | Usage | +|----------------------------|-----------------------------------------------------------------| +| `getName()` | Get provider name (label) | +| `getType()` | Get link type (will be saved in the database) | +| `getDependencies()` | Get a dictionary of supported dependency types by the provider | +| `setUserTextInput($input)` | Set text entered by the user | +| `match()` | Return true if the provider can parse correctly the user input | +| `getLink()` | Get the link found with the properties | + +### ExternalLinkInterface + +| Method | Usage | +|-------------------|------------------| +| `getTitle()` | Get link title | +| `getUrl()` | Get link URL | +| `setUrl($url)` | Set link URL | + +Register a new link provider +---------------------------- + +In your `Plugin.php`, just call the method `register()` from the object `ExternalLinkManager`: + +```php +<?php + +namespace Kanboard\Plugin\MyExternalLink; + +use Kanboard\Core\Plugin\Base; + +class Plugin extends Base +{ + public function initialize() + { + $this->externalLinkManager->register(new MyLinkProvider()); + } +} +``` + +Examples +-------- + +- Kanboard includes the default providers "WebLink" and "Attachment" diff --git a/doc/plugin-helpers.markdown b/doc/plugin-helpers.markdown new file mode 100644 index 00000000..8cc6b42a --- /dev/null +++ b/doc/plugin-helpers.markdown @@ -0,0 +1,40 @@ +Registering new helpers +======================= + +Helper skeleton: + +```php +<?php + +namespace Kanboard\Plugin\MyPlugin\Helper\MyHelper; + +use Kanboard\Core\Base; + +class MyHelper extends Base +{ + public function doSomething() + { + return 'foobar'; + } +} +``` + +Register your helper class: + +```php +$this->helper->register('myHelper', '\Kanboard\Plugin\MyPlugin\Helper\MyHelper'); +``` + +Using your helper from a template: + +```php +<p> + <?= $this->myHelper->doSomething() ?> +</p> +``` + +Using your helper from another class: + +```php +$this->helper->myHelper->doSomething(); +``` diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 6e9718d9..c1b3e577 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -58,8 +58,28 @@ class Plugin extends Base } ``` +Example to override default values for task forms: + +```php +class Plugin extends Base +{ + public function initialize() + { + $this->hook->on('controller:task:form:default', function (array $default_values) { + return empty($default_values['score']) ? array('score' => 4) : array(); + }); + } +} +``` + List of merging hooks: +#### controller:task:form:default + +- Override default values for task forms +- Arguments: + - `$default_values`: actual default values (array) + #### controller:calendar:project:events - Add more events to the project calendar @@ -131,26 +151,45 @@ Template names without prefix are core templates. List of template hooks: -- `template:auth:login-form:before` -- `template:auth:login-form:after` -- `template:dashboard:sidebar` -- `template:config:sidebar` -- `template:config:integrations` -- `template:project:integrations` -- `template:user:integrations` -- `template:export:sidebar` -- `template:layout:head` -- `template:layout:top` -- `template:layout:bottom` -- `template:project:dropdown`: "Actions" menu on left in different project views -- `template:project:header:before` -- `template:project:header:after` -- `template:project-user:sidebar` -- `template:task:sidebar:information` -- `template:task:sidebar:actions` -- `template:user:sidebar:information` -- `template:user:sidebar:actions` -- `template:app:filters-helper:before` -- `template:app:filters-helper:after` +| Hook | Description | +|------------------------------------------|----------------------------------------------------| +| `template:analytic:sidebar` | Sidebar on analytic pages | +| `template:app:filters-helper:before` | Filter helper dropdown (top) | +| `template:app:filters-helper:after` | Filter helper dropdown (bottom) | +| `template:auth:login-form:before` | Login page (top) | +| `template:auth:login-form:after` | Login page (bottom) | +| `template:config:sidebar` | Sidebar on settings page | +| `template:config:application ` | Application settings form | +| `template:config:integrations` | Integration page in global settings | +| `template:dashboard:sidebar` | Sidebar on dashboard page | +| `template:export:sidebar` | Sidebar on export pages | +| `template:layout:head` | Page layout `<head/>` tag | +| `template:layout:top` | Page layout top header | +| `template:layout:bottom` | Page layout footer | +| `template:project:dropdown` | "Actions" menu on left in different project views | +| `template:project:header:before` | Project filters (before) | +| `template:project:header:after` | Project filters (after) | +| `template:project:integrations` | Integration page in projects settings | +| `template:project:sidebar` | Sidebar in project settings | +| `template:project-user:sidebar` | Sidebar on project user overview page | +| `template:task:menu` | "Actions" menu on left in different task views | +| `template:task:dropdown` | Task dropdown menu in listing pages | +| `template:task:sidebar` | Sidebar on task page | +| `template:task:form:right-column` | Right column in task form | +| `template:task:show:top ` | Show task page: top | +| `template:task:show:bottom` | Show task page: bottom | +| `template:task:show:before-description` | Show task page: before description | +| `template:task:show:before-tasklinks` | Show task page: before tasklinks | +| `template:task:show:before-subtasks` | Show task page: before subtasks | +| `template:task:show:before-timetracking` | Show task page: before timetracking | +| `template:task:show:before-attachements` | Show task page: before attachments | +| `template:task:show:before-comments` | Show task page: before comments | +| `template:user:authentication:form` | "Edit authentication" form in user profile | +| `template:user:create-remote:form` | "Create remote user" form | +| `template:user:external` | "External authentication" page in user profile | +| `template:user:integrations` | Integration page in user profile | +| `template:user:sidebar:actions` | Sidebar in user profile (section actions) | +| `template:user:sidebar:information` | Sidebar in user profile (section information) | + Another template hooks can be added if necessary, just ask on the issue tracker. diff --git a/doc/plugins.markdown b/doc/plugins.markdown index f3f922f3..e38c887f 100644 --- a/doc/plugins.markdown +++ b/doc/plugins.markdown @@ -13,6 +13,7 @@ Plugin creators should specify explicitly the compatible versions of Kanboard. I - [Override default application behaviors](plugin-overrides.markdown) - [Add schema migrations for plugins](plugin-schema-migrations.markdown) - [Custom routes](plugin-routes.markdown) +- [Add helpers](plugin-helpers.markdown) - [Add mail transports](plugin-mail-transports.markdown) - [Add notification types](plugin-notifications.markdown) - [Add automatic actions](plugin-automatic-actions.markdown) @@ -21,6 +22,7 @@ Plugin creators should specify explicitly the compatible versions of Kanboard. I - [Authentication plugin registration](plugin-authentication.markdown) - [Authorization Architecture](plugin-authorization-architecture.markdown) - [Custom Group Providers](plugin-group-provider.markdown) +- [External Link Providers](plugin-external-link.markdown) - [LDAP client](plugin-ldap-client.markdown) Examples of plugins diff --git a/doc/suse-installation.markdown b/doc/suse-installation.markdown new file mode 100644 index 00000000..ce36c8f7 --- /dev/null +++ b/doc/suse-installation.markdown @@ -0,0 +1,14 @@ +Installation on OpenSuse +======================== + +OpenSuse Leap 42.1 +------------------ + +```bash +sudo zypper install php5 php5-sqlite php5-gd php5-json php5-mcrypt php5-mbstring php5-openssl +cd /srv/www/htdocs +sudo wget http://kanboard.net/kanboard-latest.zip +sudo unzip kanboard-latest.zip +sudo chmod -R 777 kanboard +sudo rm kanboard-latest.zip +``` diff --git a/doc/translations.markdown b/doc/translations.markdown index 629c4355..00707e1c 100644 --- a/doc/translations.markdown +++ b/doc/translations.markdown @@ -5,7 +5,7 @@ How to translate Kanboard to a new language? -------------------------------------------- - Translations are stored inside the directory `app/Locale` -- There is subdirectory for each language, for example in French we have `fr_FR`, Italian `it_IT` etc. +- There is a subdirectory for each language, for example in French we have `fr_FR`, Italian `it_IT` etc. - A translation is a PHP file that returns an Array with a key-value pairs - The key is the original text in English and the value is the translation of the corresponding language - **French translations are always up to date** @@ -34,20 +34,9 @@ Translations are displayed with the following functions in the source code: - `t()`: display text with HTML escaping - `e()`: display text without HTML escaping -- `dt()`: display date and time using the `strftime()` function formats Always use the english version in the source code. -### Date and time translation - -Date strings use the function `strftime()` to format the date. - -For example, the original English version can be defined like that `Created on %B %e, %Y at %k:%M %p` and that will output something like that `Created on January 11, 2015 at 15:19 PM`. The French version can be modified to display a different format, `Créé le %d/%m/%Y à %H:%M` and the result will be `Créé le 11/01/2015 à 15:19`. - -All formats are available in the [PHP documentation](http://php.net/strftime). - -### Placeholders - Text strings use the function `sprintf()` to replace elements: - `%s` is used to replace a string diff --git a/doc/ubuntu-installation.markdown b/doc/ubuntu-installation.markdown index cec3ebba..ab4dfe7c 100644 --- a/doc/ubuntu-installation.markdown +++ b/doc/ubuntu-installation.markdown @@ -26,3 +26,5 @@ sudo unzip kanboard-latest.zip sudo chown -R www-data:www-data kanboard/data sudo rm kanboard-latest.zip ``` + +Some features of Kanboard require that you run [a daily background job](cronjob.markdown). diff --git a/doc/windows-apache-installation.markdown b/doc/windows-apache-installation.markdown index 2c8f74e1..27b6812e 100644 --- a/doc/windows-apache-installation.markdown +++ b/doc/windows-apache-installation.markdown @@ -123,3 +123,8 @@ Tested configuration -------------------- - Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8 + +Notes +----- + +- Some features of Kanboard require that you run [a daily background job](cronjob.markdown). diff --git a/doc/windows-iis-installation.markdown b/doc/windows-iis-installation.markdown index 6206db21..bd4607de 100644 --- a/doc/windows-iis-installation.markdown +++ b/doc/windows-iis-installation.markdown @@ -65,3 +65,9 @@ Tested configurations - Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16 - Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29 + +Notes +----- + +- Some features of Kanboard require that you run [a daily background job](cronjob.markdown). + diff --git a/issue_template.md b/issue_template.md new file mode 100644 index 00000000..196d2e61 --- /dev/null +++ b/issue_template.md @@ -0,0 +1,24 @@ +### Expected behaviour + +Tell us what should happen + + +### Actual behaviour + +Tell us what happens instead + + +### Steps to reproduce + +1. +2. +3. + + +### Configuration + +- Kanboard version: +- Database type and version: +- PHP version: +- OS: +- Browser: diff --git a/jsonrpc.php b/jsonrpc.php index 1d59d4ea..d2163347 100644 --- a/jsonrpc.php +++ b/jsonrpc.php @@ -8,6 +8,7 @@ use Kanboard\Api\Me; use Kanboard\Api\Action; use Kanboard\Api\App; use Kanboard\Api\Board; +use Kanboard\Api\Column; use Kanboard\Api\Category; use Kanboard\Api\Comment; use Kanboard\Api\File; @@ -30,6 +31,7 @@ $server->attach(new Me($container)); $server->attach(new Action($container)); $server->attach(new App($container)); $server->attach(new Board($container)); +$server->attach(new Column($container)); $server->attach(new Category($container)); $server->attach(new Comment($container)); $server->attach(new File($container)); @@ -13,6 +13,8 @@ use Kanboard\Console\ProjectDailyColumnStatsExport; use Kanboard\Console\TransitionExport; use Kanboard\Console\LocaleSync; use Kanboard\Console\LocaleComparator; +use Kanboard\Console\TaskTrigger; +use Kanboard\Console\Cronjob; $container['dispatcher']->dispatch('app.bootstrap', new Event); @@ -25,4 +27,6 @@ $application->add(new ProjectDailyColumnStatsExport($container)); $application->add(new TransitionExport($container)); $application->add(new LocaleSync($container)); $application->add(new LocaleComparator($container)); +$application->add(new TaskTrigger($container)); +$application->add(new Cronjob($container)); $application->run(); diff --git a/tests/integration/ApiTest.php b/tests/integration/ApiTest.php index 798bde42..5fed0368 100644 --- a/tests/integration/ApiTest.php +++ b/tests/integration/ApiTest.php @@ -170,192 +170,6 @@ class Api extends PHPUnit_Framework_TestCase $this->assertCount(0, $activities); } - public function testGetBoard() - { - $board = $this->client->getBoard(1); - $this->assertTrue(is_array($board)); - $this->assertEquals(1, count($board)); - $this->assertEquals('Default swimlane', $board[0]['name']); - $this->assertEquals(4, count($board[0]['columns'])); - } - - public function testGetColumns() - { - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals(4, count($columns)); - $this->assertEquals('Done', $columns[3]['title']); - } - - public function testMoveColumnUp() - { - $this->assertTrue($this->client->moveColumnUp(1, 4)); - - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals('Done', $columns[2]['title']); - $this->assertEquals('Work in progress', $columns[3]['title']); - } - - public function testMoveColumnDown() - { - $this->assertTrue($this->client->moveColumnDown(1, 4)); - - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals('Work in progress', $columns[2]['title']); - $this->assertEquals('Done', $columns[3]['title']); - } - - public function testUpdateColumn() - { - $this->assertTrue($this->client->updateColumn(4, 'Boo', 2)); - - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals('Boo', $columns[3]['title']); - $this->assertEquals(2, $columns[3]['task_limit']); - } - - public function testAddColumn() - { - $column_id = $this->client->addColumn(1, 'New column'); - - $this->assertNotFalse($column_id); - $this->assertInternalType('int', $column_id); - $this->assertTrue($column_id > 0); - - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals(5, count($columns)); - $this->assertEquals('New column', $columns[4]['title']); - } - - public function testRemoveColumn() - { - $this->assertTrue($this->client->removeColumn(5)); - - $columns = $this->client->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals(4, count($columns)); - } - - public function testGetDefaultSwimlane() - { - $swimlane = $this->client->getDefaultSwimlane(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals('Default swimlane', $swimlane['default_swimlane']); - } - - public function testAddSwimlane() - { - $swimlane_id = $this->client->addSwimlane(1, 'Swimlane 1'); - $this->assertNotFalse($swimlane_id); - $this->assertInternalType('int', $swimlane_id); - - $swimlane = $this->client->getSwimlaneById($swimlane_id); - $this->assertNotEmpty($swimlane); - $this->assertInternalType('array', $swimlane); - $this->assertEquals('Swimlane 1', $swimlane['name']); - } - - public function testGetSwimlane() - { - $swimlane = $this->client->getSwimlane(1); - $this->assertNotEmpty($swimlane); - $this->assertInternalType('array', $swimlane); - $this->assertEquals('Swimlane 1', $swimlane['name']); - } - - public function testUpdateSwimlane() - { - $swimlane = $this->client->getSwimlaneByName(1, 'Swimlane 1'); - $this->assertNotEmpty($swimlane); - $this->assertInternalType('array', $swimlane); - $this->assertEquals(1, $swimlane['id']); - $this->assertEquals('Swimlane 1', $swimlane['name']); - - $this->assertTrue($this->client->updateSwimlane($swimlane['id'], 'Another swimlane')); - - $swimlane = $this->client->getSwimlaneById($swimlane['id']); - $this->assertNotEmpty($swimlane); - $this->assertEquals('Another swimlane', $swimlane['name']); - } - - public function testDisableSwimlane() - { - $this->assertTrue($this->client->disableSwimlane(1, 1)); - - $swimlane = $this->client->getSwimlaneById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(0, $swimlane['is_active']); - } - - public function testEnableSwimlane() - { - $this->assertTrue($this->client->enableSwimlane(1, 1)); - - $swimlane = $this->client->getSwimlaneById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - } - - public function testGetAllSwimlanes() - { - $this->assertNotFalse($this->client->addSwimlane(1, 'Swimlane A')); - - $swimlanes = $this->client->getAllSwimlanes(1); - $this->assertNotEmpty($swimlanes); - $this->assertCount(2, $swimlanes); - $this->assertEquals('Another swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane A', $swimlanes[1]['name']); - } - - public function testGetActiveSwimlane() - { - $this->assertTrue($this->client->disableSwimlane(1, 1)); - - $swimlanes = $this->client->getActiveSwimlanes(1); - $this->assertNotEmpty($swimlanes); - $this->assertCount(2, $swimlanes); - $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane A', $swimlanes[1]['name']); - } - - public function testMoveSwimlaneUp() - { - $this->assertTrue($this->client->enableSwimlane(1, 1)); - $this->assertTrue($this->client->moveSwimlaneUp(1, 1)); - - $swimlanes = $this->client->getActiveSwimlanes(1); - $this->assertNotEmpty($swimlanes); - $this->assertCount(3, $swimlanes); - $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Another swimlane', $swimlanes[1]['name']); - $this->assertEquals('Swimlane A', $swimlanes[2]['name']); - - $this->assertTrue($this->client->moveSwimlaneUp(1, 2)); - - $swimlanes = $this->client->getActiveSwimlanes(1); - $this->assertNotEmpty($swimlanes); - $this->assertCount(3, $swimlanes); - $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Swimlane A', $swimlanes[1]['name']); - $this->assertEquals('Another swimlane', $swimlanes[2]['name']); - } - - public function testMoveSwimlaneDown() - { - $this->assertTrue($this->client->moveSwimlaneDown(1, 2)); - - $swimlanes = $this->client->getActiveSwimlanes(1); - $this->assertNotEmpty($swimlanes); - $this->assertCount(3, $swimlanes); - $this->assertEquals('Default swimlane', $swimlanes[0]['name']); - $this->assertEquals('Another swimlane', $swimlanes[1]['name']); - $this->assertEquals('Swimlane A', $swimlanes[2]['name']); - } - public function testCreateTaskWithWrongMember() { $task = array( @@ -464,18 +278,6 @@ class Api extends PHPUnit_Framework_TestCase $this->assertNotEquals($moved_timestamp, $task['date_moved']); } - public function testRemoveSwimlane() - { - $this->assertTrue($this->client->removeSwimlane(1, 2)); - - $task = $this->client->getTask($this->getTaskId()); - $this->assertNotFalse($task); - $this->assertTrue(is_array($task)); - $this->assertEquals(1, $task['position']); - $this->assertEquals(4, $task['column_id']); - $this->assertEquals(0, $task['swimlane_id']); - } - public function testUpdateTask() { $task = $this->client->getTask(1); @@ -563,6 +365,20 @@ class Api extends PHPUnit_Framework_TestCase $this->assertNull($this->client->getUser(2222)); } + public function testGetUserByName() + { + $user = $this->client->getUserByName('toto'); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals(2, $user['id']); + + $user = $this->client->getUserByName('manager'); + $this->assertNotEmpty($user); + $this->assertEquals('app-manager', $user['role']); + + $this->assertNull($this->client->getUserByName('nonexistantusername')); + } + public function testUpdateUser() { $user = array(); diff --git a/tests/integration/Base.php b/tests/integration/Base.php index 6facd9ce..983d0ed9 100644 --- a/tests/integration/Base.php +++ b/tests/integration/Base.php @@ -35,7 +35,7 @@ abstract class Base extends PHPUnit_Framework_TestCase { $this->app = new JsonRPC\Client(API_URL); $this->app->authentication('jsonrpc', API_KEY); - $this->app->debug = true; + // $this->app->debug = true; $this->admin = new JsonRPC\Client(API_URL); $this->admin->authentication('admin', 'admin'); diff --git a/tests/integration/BoardTest.php b/tests/integration/BoardTest.php new file mode 100644 index 00000000..bf8d50b9 --- /dev/null +++ b/tests/integration/BoardTest.php @@ -0,0 +1,21 @@ +<?php + +require_once __DIR__.'/Base.php'; + +class BoardTest extends Base +{ + public function testCreateProject() + { + $this->assertEquals(1, $this->app->createProject('A project')); + } + + public function testGetBoard() + { + $board = $this->app->getBoard(1); + $this->assertCount(1, $board); + $this->assertEquals('Default swimlane', $board[0]['name']); + + $this->assertCount(4, $board[0]['columns']); + $this->assertEquals('Ready', $board[0]['columns'][1]['title']); + } +} diff --git a/tests/integration/ColumnTest.php b/tests/integration/ColumnTest.php new file mode 100644 index 00000000..6d02afc0 --- /dev/null +++ b/tests/integration/ColumnTest.php @@ -0,0 +1,65 @@ +<?php + +require_once __DIR__.'/Base.php'; + +class ColumnTest extends Base +{ + public function testCreateProject() + { + $this->assertEquals(1, $this->app->createProject('A project')); + } + + public function testGetColumns() + { + $columns = $this->app->getColumns($this->getProjectId()); + $this->assertCount(4, $columns); + $this->assertEquals('Done', $columns[3]['title']); + } + + public function testUpdateColumn() + { + $this->assertTrue($this->app->updateColumn(4, 'Boo', 2)); + + $columns = $this->app->getColumns($this->getProjectId()); + $this->assertEquals('Boo', $columns[3]['title']); + $this->assertEquals(2, $columns[3]['task_limit']); + } + + public function testAddColumn() + { + $column_id = $this->app->addColumn($this->getProjectId(), 'New column'); + + $this->assertNotFalse($column_id); + $this->assertInternalType('int', $column_id); + $this->assertTrue($column_id > 0); + + $columns = $this->app->getColumns($this->getProjectId()); + $this->assertCount(5, $columns); + $this->assertEquals('New column', $columns[4]['title']); + } + + public function testRemoveColumn() + { + $this->assertTrue($this->app->removeColumn(5)); + + $columns = $this->app->getColumns($this->getProjectId()); + $this->assertCount(4, $columns); + } + + public function testChangeColumnPosition() + { + $this->assertTrue($this->app->changeColumnPosition($this->getProjectId(), 1, 3)); + + $columns = $this->app->getColumns($this->getProjectId()); + $this->assertCount(4, $columns); + + $this->assertEquals('Ready', $columns[0]['title']); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals('Work in progress', $columns[1]['title']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals('Backlog', $columns[2]['title']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals('Boo', $columns[3]['title']); + $this->assertEquals(4, $columns[3]['position']); + } +} diff --git a/tests/integration/SwimlaneTest.php b/tests/integration/SwimlaneTest.php new file mode 100644 index 00000000..88747204 --- /dev/null +++ b/tests/integration/SwimlaneTest.php @@ -0,0 +1,103 @@ +<?php + +require_once __DIR__.'/Base.php'; + +class SwimlaneTest extends Base +{ + public function testCreateProject() + { + $this->assertEquals(1, $this->app->createProject('A project')); + } + + public function testGetDefaultSwimlane() + { + $swimlane = $this->app->getDefaultSwimlane(1); + $this->assertNotEmpty($swimlane); + $this->assertEquals('Default swimlane', $swimlane['default_swimlane']); + } + + public function testAddSwimlane() + { + $swimlane_id = $this->app->addSwimlane(1, 'Swimlane 1'); + $this->assertNotFalse($swimlane_id); + $this->assertInternalType('int', $swimlane_id); + + $swimlane = $this->app->getSwimlaneById($swimlane_id); + $this->assertNotEmpty($swimlane); + $this->assertInternalType('array', $swimlane); + $this->assertEquals('Swimlane 1', $swimlane['name']); + } + + public function testGetSwimlane() + { + $swimlane = $this->app->getSwimlane(1); + $this->assertInternalType('array', $swimlane); + $this->assertEquals('Swimlane 1', $swimlane['name']); + } + + public function testUpdateSwimlane() + { + $swimlane = $this->app->getSwimlaneByName(1, 'Swimlane 1'); + $this->assertInternalType('array', $swimlane); + $this->assertEquals(1, $swimlane['id']); + $this->assertEquals('Swimlane 1', $swimlane['name']); + + $this->assertTrue($this->app->updateSwimlane($swimlane['id'], 'Another swimlane')); + + $swimlane = $this->app->getSwimlaneById($swimlane['id']); + $this->assertEquals('Another swimlane', $swimlane['name']); + } + + public function testDisableSwimlane() + { + $this->assertTrue($this->app->disableSwimlane(1, 1)); + + $swimlane = $this->app->getSwimlaneById(1); + $this->assertEquals(0, $swimlane['is_active']); + } + + public function testEnableSwimlane() + { + $this->assertTrue($this->app->enableSwimlane(1, 1)); + + $swimlane = $this->app->getSwimlaneById(1); + $this->assertEquals(1, $swimlane['is_active']); + } + + public function testGetAllSwimlanes() + { + $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane A')); + + $swimlanes = $this->app->getAllSwimlanes(1); + $this->assertCount(2, $swimlanes); + $this->assertEquals('Another swimlane', $swimlanes[0]['name']); + $this->assertEquals('Swimlane A', $swimlanes[1]['name']); + } + + public function testGetActiveSwimlane() + { + $this->assertTrue($this->app->disableSwimlane(1, 1)); + + $swimlanes = $this->app->getActiveSwimlanes(1); + $this->assertCount(2, $swimlanes); + $this->assertEquals('Default swimlane', $swimlanes[0]['name']); + $this->assertEquals('Swimlane A', $swimlanes[1]['name']); + } + + public function testRemoveSwimlane() + { + $this->assertTrue($this->app->removeSwimlane(1, 2)); + } + + public function testChangePosition() + { + $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane 1')); + $this->assertNotFalse($this->app->addSwimlane(1, 'Swimlane 2')); + + $swimlanes = $this->app->getAllSwimlanes(1); + $this->assertCount(3, $swimlanes); + + $this->assertTrue($this->app->changeSwimlanePosition(1, 1, 3)); + $this->assertFalse($this->app->changeSwimlanePosition(1, 1, 6)); + } +} diff --git a/tests/integration/UserTest.php b/tests/integration/UserTest.php new file mode 100644 index 00000000..10da051c --- /dev/null +++ b/tests/integration/UserTest.php @@ -0,0 +1,18 @@ +<?php + +require_once __DIR__.'/Base.php'; + +class UserTest extends Base +{ + public function testDisableUser() + { + $this->assertEquals(2, $this->app->createUser(array('username' => 'someone', 'password' => 'test123'))); + $this->assertTrue($this->app->isActiveUser(2)); + + $this->assertTrue($this->app->disableUser(2)); + $this->assertFalse($this->app->isActiveUser(2)); + + $this->assertTrue($this->app->enableUser(2)); + $this->assertTrue($this->app->isActiveUser(2)); + } +} diff --git a/tests/units/Action/CommentCreationMoveTaskColumnTest.php b/tests/units/Action/CommentCreationMoveTaskColumnTest.php index 87ee86ea..6464639e 100644 --- a/tests/units/Action/CommentCreationMoveTaskColumnTest.php +++ b/tests/units/Action/CommentCreationMoveTaskColumnTest.php @@ -7,7 +7,6 @@ use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\Comment; use Kanboard\Model\Project; -use Kanboard\Model\ProjectUserRole; use Kanboard\Action\CommentCreationMoveTaskColumn; class CommentCreationMoveTaskColumnTest extends Base diff --git a/tests/units/Action/CommentCreationTest.php b/tests/units/Action/CommentCreationTest.php index 8460a350..042a8f8b 100644 --- a/tests/units/Action/CommentCreationTest.php +++ b/tests/units/Action/CommentCreationTest.php @@ -3,7 +3,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Event\GenericEvent; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\Comment; use Kanboard\Model\Project; diff --git a/tests/units/Action/TaskAssignCategoryLinkTest.php b/tests/units/Action/TaskAssignCategoryLinkTest.php index f638e017..da83d541 100644 --- a/tests/units/Action/TaskAssignCategoryLinkTest.php +++ b/tests/units/Action/TaskAssignCategoryLinkTest.php @@ -2,7 +2,6 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\Project; diff --git a/tests/units/Action/TaskAssignUserTest.php b/tests/units/Action/TaskAssignUserTest.php index d1cb72b9..31404c0b 100644 --- a/tests/units/Action/TaskAssignUserTest.php +++ b/tests/units/Action/TaskAssignUserTest.php @@ -8,7 +8,6 @@ use Kanboard\Model\TaskFinder; use Kanboard\Model\Project; use Kanboard\Model\ProjectUserRole; use Kanboard\Model\User; -use Kanboard\Model\Task; use Kanboard\Action\TaskAssignUser; use Kanboard\Core\Security\Role; diff --git a/tests/units/Action/TaskCloseNoActivityTest.php b/tests/units/Action/TaskCloseNoActivityTest.php new file mode 100644 index 00000000..b6e04c47 --- /dev/null +++ b/tests/units/Action/TaskCloseNoActivityTest.php @@ -0,0 +1,43 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Event\TaskListEvent; +use Kanboard\Model\TaskCreation; +use Kanboard\Model\TaskFinder; +use Kanboard\Model\Project; +use Kanboard\Model\Task; +use Kanboard\Action\TaskCloseNoActivity; + +class TaskCloseNoActivityTest extends Base +{ + public function testClose() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($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'))); + + $this->container['db']->table(Task::TABLE)->eq('id', 1)->update(array('date_modification' => strtotime('-10days'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskCloseNoActivity($this->container); + $action->setProjectId(1); + $action->setParam('duration', 2); + + $this->assertTrue($action->execute($event, Task::EVENT_DAILY_CRONJOB)); + + $task = $taskFinderModel->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['is_active']); + + $task = $taskFinderModel->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['is_active']); + } +} diff --git a/tests/units/Action/TaskEmailNoActivityTest.php b/tests/units/Action/TaskEmailNoActivityTest.php new file mode 100644 index 00000000..af4baed5 --- /dev/null +++ b/tests/units/Action/TaskEmailNoActivityTest.php @@ -0,0 +1,103 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Event\TaskListEvent; +use Kanboard\Model\TaskCreation; +use Kanboard\Model\TaskFinder; +use Kanboard\Model\Project; +use Kanboard\Model\Task; +use Kanboard\Model\User; +use Kanboard\Action\TaskEmailNoActivity; + +class TaskEmailNoActivityTest extends Base +{ + public function testSendEmail() + { + $userModel = new User($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'test', 'email' => 'chuck@norris', 'name' => 'Chuck Norris'))); + $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'))); + + $this->container['db']->table(Task::TABLE)->eq('id', 1)->update(array('date_modification' => strtotime('-10days'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskEmailNoActivity($this->container); + $action->setProjectId(1); + $action->setParam('user_id', 2); + $action->setParam('subject', 'Old tasks'); + $action->setParam('duration', 2); + + $this->container['emailClient'] + ->expects($this->once()) + ->method('send') + ->with('chuck@norris', 'Chuck Norris', 'Old tasks', $this->anything()); + + $this->assertTrue($action->execute($event, Task::EVENT_DAILY_CRONJOB)); + } + + public function testUserWithNoEmail() + { + $userModel = new User($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'test', 'name' => 'Chuck Norris'))); + $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'))); + + $this->container['db']->table(Task::TABLE)->eq('id', 1)->update(array('date_modification' => strtotime('-10days'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskEmailNoActivity($this->container); + $action->setProjectId(1); + $action->setParam('user_id', 2); + $action->setParam('subject', 'Old tasks'); + $action->setParam('duration', 2); + + $this->container['emailClient'] + ->expects($this->never()) + ->method('send'); + + $this->assertFalse($action->execute($event, Task::EVENT_DAILY_CRONJOB)); + } + + public function testTooRecent() + { + $userModel = new User($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'test', 'email' => 'chuck@norris', 'name' => 'Chuck Norris'))); + $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'))); + + $tasks = $taskFinderModel->getAll(1); + $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1)); + + $action = new TaskEmailNoActivity($this->container); + $action->setProjectId(1); + $action->setParam('user_id', 2); + $action->setParam('subject', 'Old tasks'); + $action->setParam('duration', 2); + + $this->container['emailClient'] + ->expects($this->never()) + ->method('send'); + + $this->assertFalse($action->execute($event, Task::EVENT_DAILY_CRONJOB)); + } +} diff --git a/tests/units/Analytic/AverageLeadCycleTimeAnalyticTest.php b/tests/units/Analytic/AverageLeadCycleTimeAnalyticTest.php index 9c445dca..b8faec6c 100644 --- a/tests/units/Analytic/AverageLeadCycleTimeAnalyticTest.php +++ b/tests/units/Analytic/AverageLeadCycleTimeAnalyticTest.php @@ -37,15 +37,12 @@ class AverageLeadCycleTimeAnalyticTest extends Base $this->container['db']->table(Task::TABLE)->eq('id', 4)->update(array('date_completed' => $now + 2 * 3600)); $stats = $averageLeadCycleTimeAnalytic->build(1); - $expected = array( - 'count' => 4, - 'total_lead_time' => 3600 + 1800 + 3600 + 2*3600, - 'total_cycle_time' => 1800 + 900, - 'avg_lead_time' => (3600 + 1800 + 3600 + 2*3600) / 4, - 'avg_cycle_time' => (1800 + 900) / 4, - ); - $this->assertEquals($expected, $stats); + $this->assertEquals(4, $stats['count']); + $this->assertEquals(3600 + 1800 + 3600 + 2*3600, $stats['total_lead_time'], '', 5); + $this->assertEquals(1800 + 900, $stats['total_cycle_time'], '', 5); + $this->assertEquals((3600 + 1800 + 3600 + 2*3600) / 4, $stats['avg_lead_time'], '', 5); + $this->assertEquals((1800 + 900) / 4, $stats['avg_cycle_time'], '', 5); } public function testBuildWithNoTasks() diff --git a/tests/units/Analytic/AverageTimeSpentColumnAnalyticTest.php b/tests/units/Analytic/AverageTimeSpentColumnAnalyticTest.php index 75cb181d..4e01bfa9 100644 --- a/tests/units/Analytic/AverageTimeSpentColumnAnalyticTest.php +++ b/tests/units/Analytic/AverageTimeSpentColumnAnalyticTest.php @@ -16,45 +16,38 @@ class AverageTimeSpentColumnAnalyticTest extends Base $taskCreationModel = new TaskCreation($this->container); $projectModel = new Project($this->container); $averageLeadCycleTimeAnalytic = new AverageTimeSpentColumnAnalytic($this->container); - $now = time(); $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'))); + $now = time(); + $this->container['db']->table(Task::TABLE)->eq('id', 1)->update(array('date_completed' => $now + 3600)); $this->container['db']->table(Task::TABLE)->eq('id', 2)->update(array('date_completed' => $now + 1800)); $stats = $averageLeadCycleTimeAnalytic->build(1); - $expected = array( - 1 => array( - 'count' => 2, - 'time_spent' => 3600+1800, - 'average' => (int) ((3600+1800)/2), - 'title' => 'Backlog', - ), - 2 => array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => 'Ready', - ), - 3 => array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => 'Work in progress', - ), - 4 => array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => 'Done', - ) - ); - - $this->assertEquals($expected, $stats); + + $this->assertEquals(2, $stats[1]['count']); + $this->assertEquals(3600+1800, $stats[1]['time_spent'], '', 3); + $this->assertEquals((int) ((3600+1800)/2), $stats[1]['average'], '', 3); + $this->assertEquals('Backlog', $stats[1]['title']); + + $this->assertEquals(0, $stats[2]['count']); + $this->assertEquals(0, $stats[2]['time_spent'], '', 3); + $this->assertEquals(0, $stats[2]['average'], '', 3); + $this->assertEquals('Ready', $stats[2]['title']); + + $this->assertEquals(0, $stats[3]['count']); + $this->assertEquals(0, $stats[3]['time_spent'], '', 3); + $this->assertEquals(0, $stats[3]['average'], '', 3); + $this->assertEquals('Work in progress', $stats[3]['title']); + + $this->assertEquals(0, $stats[4]['count']); + $this->assertEquals(0, $stats[4]['time_spent'], '', 3); + $this->assertEquals(0, $stats[4]['average'], '', 3); + $this->assertEquals('Done', $stats[4]['title']); } public function testAverageWithTransitions() @@ -64,13 +57,13 @@ class AverageTimeSpentColumnAnalyticTest extends Base $taskCreationModel = new TaskCreation($this->container); $projectModel = new Project($this->container); $averageLeadCycleTimeAnalytic = new AverageTimeSpentColumnAnalytic($this->container); - $now = time(); $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'))); + $now = time(); $this->container['db']->table(Task::TABLE)->eq('id', 1)->update(array('date_completed' => $now + 3600)); $this->container['db']->table(Task::TABLE)->eq('id', 2)->update(array('date_completed' => $now + 1800)); @@ -84,33 +77,25 @@ class AverageTimeSpentColumnAnalyticTest extends Base } $stats = $averageLeadCycleTimeAnalytic->build(1); - $expected = array( - 1 => array( - 'count' => 2, - 'time_spent' => 3600+1800, - 'average' => (int) ((3600+1800)/2), - 'title' => 'Backlog', - ), - 2 => array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => 'Ready', - ), - 3 => array( - 'count' => 2, - 'time_spent' => 1800, - 'average' => 900, - 'title' => 'Work in progress', - ), - 4 => array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => 'Done', - ) - ); - - $this->assertEquals($expected, $stats); + + $this->assertEquals(2, $stats[1]['count']); + $this->assertEquals(3600+1800, $stats[1]['time_spent'], '', 3); + $this->assertEquals((int) ((3600+1800)/2), $stats[1]['average'], '', 3); + $this->assertEquals('Backlog', $stats[1]['title']); + + $this->assertEquals(0, $stats[2]['count']); + $this->assertEquals(0, $stats[2]['time_spent'], '', 3); + $this->assertEquals(0, $stats[2]['average'], '', 3); + $this->assertEquals('Ready', $stats[2]['title']); + + $this->assertEquals(2, $stats[3]['count']); + $this->assertEquals(1800, $stats[3]['time_spent'], '', 3); + $this->assertEquals(900, $stats[3]['average'], '', 3); + $this->assertEquals('Work in progress', $stats[3]['title']); + + $this->assertEquals(0, $stats[4]['count']); + $this->assertEquals(0, $stats[4]['time_spent'], '', 3); + $this->assertEquals(0, $stats[4]['average'], '', 3); + $this->assertEquals('Done', $stats[4]['title']); } } diff --git a/tests/units/Auth/DatabaseAuthTest.php b/tests/units/Auth/DatabaseAuthTest.php index a13b7fee..ac099a7e 100644 --- a/tests/units/Auth/DatabaseAuthTest.php +++ b/tests/units/Auth/DatabaseAuthTest.php @@ -3,6 +3,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Auth\DatabaseAuth; +use Kanboard\Model\User; class DatabaseAuthTest extends Base { @@ -40,12 +41,21 @@ class DatabaseAuthTest extends Base public function testIsvalidSession() { + $userModel = new User($this->container); $provider = new DatabaseAuth($this->container); + $this->assertFalse($provider->isValidSession()); - $this->container['sessionStorage']->user = array('id' => 1); + $this->assertEquals(2, $userModel->create(array('username' => 'foobar'))); + + $this->container['sessionStorage']->user = array('id' => 2); $this->assertTrue($provider->isValidSession()); + $this->container['sessionStorage']->user = array('id' => 3); + $this->assertFalse($provider->isValidSession()); + + $this->assertTrue($userModel->disable(2)); + $this->container['sessionStorage']->user = array('id' => 2); $this->assertFalse($provider->isValidSession()); } diff --git a/tests/units/Auth/GithubAuthTest.php b/tests/units/Auth/GithubAuthTest.php deleted file mode 100644 index e9ab066f..00000000 --- a/tests/units/Auth/GithubAuthTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -require_once __DIR__.'/../Base.php'; - -use Kanboard\Auth\GithubAuth; -use Kanboard\Model\User; - -class GithubAuthTest extends Base -{ - public function testGetName() - { - $provider = new GithubAuth($this->container); - $this->assertEquals('Github', $provider->getName()); - } - - public function testAuthenticationSuccessful() - { - $profile = array( - 'id' => 1234, - 'email' => 'test@localhost', - 'name' => 'Test', - ); - - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GithubAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue($profile)); - - $this->assertInstanceOf('Kanboard\Auth\GithubAuth', $provider->setCode('1234')); - - $this->assertTrue($provider->authenticate()); - - $user = $provider->getUser(); - $this->assertInstanceOf('Kanboard\User\GithubUserProvider', $user); - $this->assertEquals('Test', $user->getName()); - $this->assertEquals('', $user->getInternalId()); - $this->assertEquals(1234, $user->getExternalId()); - $this->assertEquals('', $user->getRole()); - $this->assertEquals('', $user->getUsername()); - $this->assertEquals('test@localhost', $user->getEmail()); - $this->assertEquals('github_id', $user->getExternalIdColumn()); - $this->assertEquals(array(), $user->getExternalGroupIds()); - $this->assertEquals(array(), $user->getExtraAttributes()); - $this->assertFalse($user->isUserCreationAllowed()); - } - - public function testAuthenticationFailed() - { - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GithubAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue(array())); - - $this->assertFalse($provider->authenticate()); - $this->assertEquals(null, $provider->getUser()); - } - - public function testGetService() - { - $provider = new GithubAuth($this->container); - $this->assertInstanceOf('Kanboard\Core\Http\OAuth2', $provider->getService()); - } - - public function testUnlink() - { - $userModel = new User($this->container); - $provider = new GithubAuth($this->container); - - $this->assertEquals(2, $userModel->create(array('username' => 'test', 'github_id' => '1234'))); - $this->assertNotEmpty($userModel->getByExternalId('github_id', 1234)); - - $this->assertTrue($provider->unlink(2)); - $this->assertEmpty($userModel->getByExternalId('github_id', 1234)); - } -} diff --git a/tests/units/Auth/GitlabAuthTest.php b/tests/units/Auth/GitlabAuthTest.php deleted file mode 100644 index e3ae0bdd..00000000 --- a/tests/units/Auth/GitlabAuthTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -require_once __DIR__.'/../Base.php'; - -use Kanboard\Auth\GitlabAuth; -use Kanboard\Model\User; - -class GitlabAuthTest extends Base -{ - public function testGetName() - { - $provider = new GitlabAuth($this->container); - $this->assertEquals('Gitlab', $provider->getName()); - } - - public function testAuthenticationSuccessful() - { - $profile = array( - 'id' => 1234, - 'email' => 'test@localhost', - 'name' => 'Test', - ); - - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GitlabAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue($profile)); - - $this->assertInstanceOf('Kanboard\Auth\GitlabAuth', $provider->setCode('1234')); - - $this->assertTrue($provider->authenticate()); - - $user = $provider->getUser(); - $this->assertInstanceOf('Kanboard\User\GitlabUserProvider', $user); - $this->assertEquals('Test', $user->getName()); - $this->assertEquals('', $user->getInternalId()); - $this->assertEquals(1234, $user->getExternalId()); - $this->assertEquals('', $user->getRole()); - $this->assertEquals('', $user->getUsername()); - $this->assertEquals('test@localhost', $user->getEmail()); - $this->assertEquals('gitlab_id', $user->getExternalIdColumn()); - $this->assertEquals(array(), $user->getExternalGroupIds()); - $this->assertEquals(array(), $user->getExtraAttributes()); - $this->assertFalse($user->isUserCreationAllowed()); - } - - public function testAuthenticationFailed() - { - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GitlabAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue(array())); - - $this->assertFalse($provider->authenticate()); - $this->assertEquals(null, $provider->getUser()); - } - - public function testGetService() - { - $provider = new GitlabAuth($this->container); - $this->assertInstanceOf('Kanboard\Core\Http\OAuth2', $provider->getService()); - } - - public function testUnlink() - { - $userModel = new User($this->container); - $provider = new GitlabAuth($this->container); - - $this->assertEquals(2, $userModel->create(array('username' => 'test', 'gitlab_id' => '1234'))); - $this->assertNotEmpty($userModel->getByExternalId('gitlab_id', 1234)); - - $this->assertTrue($provider->unlink(2)); - $this->assertEmpty($userModel->getByExternalId('gitlab_id', 1234)); - } -} diff --git a/tests/units/Auth/GoogleAuthTest.php b/tests/units/Auth/GoogleAuthTest.php deleted file mode 100644 index b9a7d811..00000000 --- a/tests/units/Auth/GoogleAuthTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -require_once __DIR__.'/../Base.php'; - -use Kanboard\Auth\GoogleAuth; -use Kanboard\Model\User; - -class GoogleAuthTest extends Base -{ - public function testGetName() - { - $provider = new GoogleAuth($this->container); - $this->assertEquals('Google', $provider->getName()); - } - - public function testAuthenticationSuccessful() - { - $profile = array( - 'id' => 1234, - 'email' => 'test@localhost', - 'name' => 'Test', - ); - - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GoogleAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue($profile)); - - $this->assertInstanceOf('Kanboard\Auth\GoogleAuth', $provider->setCode('1234')); - - $this->assertTrue($provider->authenticate()); - - $user = $provider->getUser(); - $this->assertInstanceOf('Kanboard\User\GoogleUserProvider', $user); - $this->assertEquals('Test', $user->getName()); - $this->assertEquals('', $user->getInternalId()); - $this->assertEquals(1234, $user->getExternalId()); - $this->assertEquals('', $user->getRole()); - $this->assertEquals('', $user->getUsername()); - $this->assertEquals('test@localhost', $user->getEmail()); - $this->assertEquals('google_id', $user->getExternalIdColumn()); - $this->assertEquals(array(), $user->getExternalGroupIds()); - $this->assertEquals(array(), $user->getExtraAttributes()); - $this->assertFalse($user->isUserCreationAllowed()); - } - - public function testAuthenticationFailed() - { - $provider = $this - ->getMockBuilder('\Kanboard\Auth\GoogleAuth') - ->setConstructorArgs(array($this->container)) - ->setMethods(array( - 'getProfile', - )) - ->getMock(); - - $provider->expects($this->once()) - ->method('getProfile') - ->will($this->returnValue(array())); - - $this->assertFalse($provider->authenticate()); - $this->assertEquals(null, $provider->getUser()); - } - - public function testGetService() - { - $provider = new GoogleAuth($this->container); - $this->assertInstanceOf('Kanboard\Core\Http\OAuth2', $provider->getService()); - } - - public function testUnlink() - { - $userModel = new User($this->container); - $provider = new GoogleAuth($this->container); - - $this->assertEquals(2, $userModel->create(array('username' => 'test', 'google_id' => '1234'))); - $this->assertNotEmpty($userModel->getByExternalId('google_id', 1234)); - - $this->assertTrue($provider->unlink(2)); - $this->assertEmpty($userModel->getByExternalId('google_id', 1234)); - } -} diff --git a/tests/units/Base.php b/tests/units/Base.php index 4b54cdb0..6af14ba5 100644 --- a/tests/units/Base.php +++ b/tests/units/Base.php @@ -10,49 +10,7 @@ use SimpleLogger\Logger; use SimpleLogger\File; use Kanboard\Core\Session\FlashMessage; use Kanboard\Core\Session\SessionStorage; - -class FakeHttpClient -{ - private $url = ''; - private $data = array(); - private $headers = array(); - - public function getUrl() - { - return $this->url; - } - - public function getData() - { - return $this->data; - } - - public function getHeaders() - { - return $this->headers; - } - - public function toPrettyJson() - { - return json_encode($this->data, JSON_PRETTY_PRINT); - } - - public function postJson($url, array $data, array $headers = array()) - { - $this->url = $url; - $this->data = $data; - $this->headers = $headers; - return true; - } - - public function postForm($url, array $data, array $headers = array()) - { - $this->url = $url; - $this->data = $data; - $this->headers = $headers; - return true; - } -} +use Kanboard\ServiceProvider\ActionProvider; abstract class Base extends PHPUnit_Framework_TestCase { @@ -75,6 +33,7 @@ abstract class Base extends PHPUnit_Framework_TestCase } $this->container = new Pimple\Container; + $this->container->register(new Kanboard\ServiceProvider\HelperProvider); $this->container->register(new Kanboard\ServiceProvider\AuthenticationProvider); $this->container->register(new Kanboard\ServiceProvider\DatabaseProvider); $this->container->register(new Kanboard\ServiceProvider\ClassProvider); @@ -90,8 +49,18 @@ abstract class Base extends PHPUnit_Framework_TestCase $this->container['logger'] = new Logger; $this->container['logger']->setLogger(new File($this->isWindows() ? 'NUL' : '/dev/null')); - $this->container['httpClient'] = new FakeHttpClient; - $this->container['emailClient'] = $this->getMockBuilder('EmailClient')->setMethods(array('send'))->getMock(); + + $this->container['httpClient'] = $this + ->getMockBuilder('\Kanboard\Core\Http\Client') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('get', 'getJson', 'postJson', 'postForm')) + ->getMock(); + + $this->container['emailClient'] = $this + ->getMockBuilder('\Kanboard\Core\Mail\Client') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('send')) + ->getMock(); $this->container['userNotificationType'] = $this ->getMockBuilder('\Kanboard\Model\UserNotificationType') @@ -99,9 +68,16 @@ abstract class Base extends PHPUnit_Framework_TestCase ->setMethods(array('getType', 'getSelectedTypes')) ->getMock(); + $this->container['objectStorage'] = $this + ->getMockBuilder('\Kanboard\Core\ObjectStorage\FileStorage') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('put', 'moveFile', 'remove', 'moveUploadedFile')) + ->getMock(); + $this->container['sessionStorage'] = new SessionStorage; + $this->container->register(new ActionProvider); - $this->container['flash'] = function($c) { + $this->container['flash'] = function ($c) { return new FlashMessage($c); }; } diff --git a/tests/units/Core/DateParserTest.php b/tests/units/Core/DateParserTest.php index 0d345784..dc3366b3 100644 --- a/tests/units/Core/DateParserTest.php +++ b/tests/units/Core/DateParserTest.php @@ -6,79 +6,167 @@ use Kanboard\Core\DateParser; class DateParserTest extends Base { + public function testGetTimeFormats() + { + $dateParser = new DateParser($this->container); + $this->assertCount(2, $dateParser->getTimeFormats()); + $this->assertContains('H:i', $dateParser->getTimeFormats()); + $this->assertContains('g:i a', $dateParser->getTimeFormats()); + } + + public function testGetDateFormats() + { + $dateParser = new DateParser($this->container); + $this->assertCount(4, $dateParser->getDateFormats()); + $this->assertCount(6, $dateParser->getDateFormats(true)); + $this->assertContains('d/m/Y', $dateParser->getDateFormats()); + $this->assertNotContains('Y-m-d', $dateParser->getDateFormats()); + $this->assertContains('Y-m-d', $dateParser->getDateFormats(true)); + } + + public function testGetDateTimeFormats() + { + $dateParser = new DateParser($this->container); + $this->assertCount(8, $dateParser->getDateTimeFormats()); + $this->assertCount(12, $dateParser->getDateTimeFormats(true)); + $this->assertContains('d/m/Y H:i', $dateParser->getDateTimeFormats()); + $this->assertNotContains('Y-m-d H:i', $dateParser->getDateTimeFormats()); + $this->assertContains('Y-m-d g:i a', $dateParser->getDateTimeFormats(true)); + } + + public function testGetAllDateFormats() + { + $dateParser = new DateParser($this->container); + $this->assertCount(12, $dateParser->getAllDateFormats()); + $this->assertCount(18, $dateParser->getAllDateFormats(true)); + $this->assertContains('d/m/Y', $dateParser->getAllDateFormats()); + $this->assertContains('d/m/Y H:i', $dateParser->getAllDateFormats()); + $this->assertNotContains('Y-m-d H:i', $dateParser->getAllDateFormats()); + $this->assertContains('Y-m-d g:i a', $dateParser->getAllDateFormats(true)); + $this->assertContains('Y-m-d', $dateParser->getAllDateFormats(true)); + } + + public function testGetAllAvailableFormats() + { + $dateParser = new DateParser($this->container); + + $formats = $dateParser->getAvailableFormats($dateParser->getDateFormats()); + $this->assertArrayHasKey('d/m/Y', $formats); + $this->assertContains(date('d/m/Y'), $formats); + + $formats = $dateParser->getAvailableFormats($dateParser->getDateTimeFormats()); + $this->assertArrayHasKey('d/m/Y H:i', $formats); + $this->assertContains(date('d/m/Y H:i'), $formats); + + $formats = $dateParser->getAvailableFormats($dateParser->getAllDateFormats()); + $this->assertArrayHasKey('d/m/Y', $formats); + $this->assertContains(date('d/m/Y'), $formats); + $this->assertArrayHasKey('d/m/Y H:i', $formats); + $this->assertContains(date('d/m/Y H:i'), $formats); + } + + public function testGetTimestamp() + { + $dateParser = new DateParser($this->container); + + $this->assertEquals(1393995600, $dateParser->getTimestamp(1393995600)); + $this->assertEquals('2014-03-05', date('Y-m-d', $dateParser->getTimestamp('2014-03-05'))); + $this->assertEquals('2014-03-05', date('Y-m-d', $dateParser->getTimestamp('2014_03_05'))); + $this->assertEquals('2014-03-05', date('Y-m-d', $dateParser->getTimestamp('03/05/2014'))); + $this->assertEquals('2014-03-25 17:18', date('Y-m-d H:i', $dateParser->getTimestamp('03/25/2014 5:18 pm'))); + $this->assertEquals('2014-03-25 05:18', date('Y-m-d H:i', $dateParser->getTimestamp('03/25/2014 5:18 am'))); + $this->assertEquals('2014-03-25 17:18', date('Y-m-d H:i', $dateParser->getTimestamp('03/25/2014 5:18pm'))); + $this->assertEquals('2014-03-25 23:14', date('Y-m-d H:i', $dateParser->getTimestamp('03/25/2014 23:14'))); + $this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $dateParser->getTimestamp('2014_03_29 23:14'))); + $this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $dateParser->getTimestamp('2014-03-29 23:14'))); + } + public function testDateRange() { - $d = new DateParser($this->container); + $dateParser = new DateParser($this->container); - $this->assertTrue($d->withinDateRange(new DateTime('2015-03-14 15:30:00'), new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 16:00:00'))); - $this->assertFalse($d->withinDateRange(new DateTime('2015-03-14 15:30:00'), new DateTime('2015-03-14 16:00:00'), new DateTime('2015-03-14 17:00:00'))); + $this->assertTrue($dateParser->withinDateRange(new DateTime('2015-03-14 15:30:00'), new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 16:00:00'))); + $this->assertFalse($dateParser->withinDateRange(new DateTime('2015-03-14 15:30:00'), new DateTime('2015-03-14 16:00:00'), new DateTime('2015-03-14 17:00:00'))); + } + + public function testGetHours() + { + $dateParser = new DateParser($this->container); + + $this->assertEquals(1, $dateParser->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 16:00:00'))); + $this->assertEquals(2.5, $dateParser->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 17:30:00'))); + $this->assertEquals(2.75, $dateParser->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 17:45:00'))); + $this->assertEquals(3, $dateParser->getHours(new DateTime('2015-03-14 14:57:00'), new DateTime('2015-03-14 17:58:00'))); + $this->assertEquals(3, $dateParser->getHours(new DateTime('2015-03-14 14:57:00'), new DateTime('2015-03-14 11:58:00'))); } public function testRoundSeconds() { - $d = new DateParser($this->container); - $this->assertEquals('16:30', date('H:i', $d->getRoundedSeconds(strtotime('16:28')))); - $this->assertEquals('16:00', date('H:i', $d->getRoundedSeconds(strtotime('16:02')))); - $this->assertEquals('16:15', date('H:i', $d->getRoundedSeconds(strtotime('16:14')))); - $this->assertEquals('17:00', date('H:i', $d->getRoundedSeconds(strtotime('16:58')))); + $dateParser = new DateParser($this->container); + $this->assertEquals('16:30', date('H:i', $dateParser->getRoundedSeconds(strtotime('16:28')))); + $this->assertEquals('16:00', date('H:i', $dateParser->getRoundedSeconds(strtotime('16:02')))); + $this->assertEquals('16:15', date('H:i', $dateParser->getRoundedSeconds(strtotime('16:14')))); + $this->assertEquals('17:00', date('H:i', $dateParser->getRoundedSeconds(strtotime('16:58')))); } - public function testGetHours() + public function testGetIsoDate() { - $d = new DateParser($this->container); + $dateParser = new DateParser($this->container); + + $this->assertEquals('2016-02-06', $dateParser->getIsoDate(1454786217)); + $this->assertEquals('2014-03-05', $dateParser->getIsoDate('2014-03-05')); + $this->assertEquals('2014-03-05', $dateParser->getIsoDate('2014_03_05')); + $this->assertEquals('2014-03-05', $dateParser->getIsoDate('03/05/2014')); + $this->assertEquals('2014-03-25', $dateParser->getIsoDate('03/25/2014 5:18 pm')); + $this->assertEquals('2014-03-25', $dateParser->getIsoDate('03/25/2014 5:18 am')); + $this->assertEquals('2014-03-25', $dateParser->getIsoDate('03/25/2014 5:18pm')); + $this->assertEquals('2014-03-25', $dateParser->getIsoDate('03/25/2014 23:14')); + $this->assertEquals('2014-03-29', $dateParser->getIsoDate('2014_03_29 23:14')); + $this->assertEquals('2014-03-29', $dateParser->getIsoDate('2014-03-29 23:14')); + } - $this->assertEquals(1, $d->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 16:00:00'))); - $this->assertEquals(2.5, $d->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 17:30:00'))); - $this->assertEquals(2.75, $d->getHours(new DateTime('2015-03-14 15:00:00'), new DateTime('2015-03-14 17:45:00'))); - $this->assertEquals(3, $d->getHours(new DateTime('2015-03-14 14:57:00'), new DateTime('2015-03-14 17:58:00'))); - $this->assertEquals(3, $d->getHours(new DateTime('2015-03-14 14:57:00'), new DateTime('2015-03-14 11:58:00'))); + public function testGetTimestampFromIsoFormat() + { + $dateParser = new DateParser($this->container); + $this->assertEquals('2014-03-05 00:00', date('Y-m-d H:i', $dateParser->getTimestampFromIsoFormat('2014-03-05'))); + $this->assertEquals(date('Y-m-d 00:00', strtotime('+2 days')), date('Y-m-d H:i', $dateParser->getTimestampFromIsoFormat(strtotime('+2 days')))); } - public function testValidDate() + public function testRemoveTimeFromTimestamp() { - $d = new DateParser($this->container); - - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('2014-03-05', 'Y-m-d'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('2014_03_05', 'Y_m_d'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('05/03/2014', 'd/m/Y'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('03/05/2014', 'm/d/Y'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('3/5/2014', 'm/d/Y'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('5/3/2014', 'd/m/Y'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getValidDate('5/3/14', 'd/m/y'))); - $this->assertEquals(0, $d->getValidDate('5/3/14', 'd/m/Y')); - $this->assertEquals(0, $d->getValidDate('5-3-2014', 'd/m/Y')); + $dateParser = new DateParser($this->container); + $this->assertEquals('2016-02-06 00:00', date('Y-m-d H:i', $dateParser->removeTimeFromTimestamp(1454786217))); } - public function testGetTimestamp() + public function testFormat() { - $d = new DateParser($this->container); - - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('2014-03-05'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('2014_03_05'))); - $this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('03/05/2014'))); - $this->assertEquals('2014-03-25 17:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18 pm'))); - $this->assertEquals('2014-03-25 05:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18 am'))); - $this->assertEquals('2014-03-25 05:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18am'))); - $this->assertEquals('2014-03-25 23:14', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 23:14'))); - $this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $d->getTimestamp('2014_03_29 23:14'))); - $this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $d->getTimestamp('2014-03-29 23:14'))); + $dateParser = new DateParser($this->container); + $values['date'] = '1454787006'; + + $this->assertEquals(array('date' => '06/02/2016'), $dateParser->format($values, array('date'), 'd/m/Y')); + $this->assertEquals(array('date' => '02/06/2016 7:30 pm'), $dateParser->format($values, array('date'), 'm/d/Y g:i a')); } public function testConvert() { - $d = new DateParser($this->container); - + $dateParser = new DateParser($this->container); $values = array( 'date_due' => '2015-01-25', - 'date_started' => '2015_01_25', + 'date_started' => '2015-01-25 17:25', ); - $d->convert($values, array('date_due', 'date_started')); + $this->assertEquals( + array('date_due' => 1422144000, 'date_started' => 1422144000), + $dateParser->convert($values, array('date_due', 'date_started')) + ); - $this->assertEquals(mktime(0, 0, 0, 1, 25, 2015), $values['date_due']); - $this->assertEquals('2015-01-25', date('Y-m-d', $values['date_due'])); + $values = array( + 'date_started' => '2015-01-25 17:25', + ); - $this->assertEquals(mktime(0, 0, 0, 1, 25, 2015), $values['date_started']); - $this->assertEquals('2015-01-25', date('Y-m-d', $values['date_started'])); + $this->assertEquals( + array('date_started' => 1422206700), + $dateParser->convert($values, array('date_due', 'date_started'), true) + ); } } diff --git a/tests/units/Core/ExternalLink/ExternalLinkManagerTest.php b/tests/units/Core/ExternalLink/ExternalLinkManagerTest.php new file mode 100644 index 00000000..d284a80b --- /dev/null +++ b/tests/units/Core/ExternalLink/ExternalLinkManagerTest.php @@ -0,0 +1,120 @@ +<?php + +require_once __DIR__.'/../../Base.php'; + +use Kanboard\Core\ExternalLink\ExternalLinkManager; +use Kanboard\ExternalLink\WebLinkProvider; +use Kanboard\ExternalLink\AttachmentLinkProvider; + +class ExternalLinkManagerTest extends Base +{ + public function testRegister() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $this->assertInstanceOf(get_class($webLinkProvider), $externalLinkManager->getProvider($webLinkProvider->getType())); + $this->assertInstanceOf(get_class($attachmentLinkProvider), $externalLinkManager->getProvider($attachmentLinkProvider->getType())); + } + + public function testGetProviderNotFound() + { + $externalLinkManager = new ExternalLinkManager($this->container); + + $this->setExpectedException('\Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound'); + $externalLinkManager->getProvider('not found'); + } + + public function testGetTypes() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $this->assertEquals(array(ExternalLinkManager::TYPE_AUTO => 'Auto'), $externalLinkManager->getTypes()); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $this->assertEquals( + array(ExternalLinkManager::TYPE_AUTO => 'Auto', 'attachment' => 'Attachment', 'weblink' => 'Web Link'), + $externalLinkManager->getTypes() + ); + } + + public function testGetDependencyLabel() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $this->assertSame('Related', $externalLinkManager->getDependencyLabel($webLinkProvider->getType(), 'related')); + $this->assertSame('custom', $externalLinkManager->getDependencyLabel($webLinkProvider->getType(), 'custom')); + } + + public function testFindProviderNotFound() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $this->setExpectedException('\Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound'); + $externalLinkManager->find(); + } + + public function testFindProvider() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $externalLinkManager->setUserInput(array('text' => 'https://google.com/', 'type' => ExternalLinkManager::TYPE_AUTO)); + $this->assertSame($webLinkProvider, $externalLinkManager->find()); + + $externalLinkManager->setUserInput(array('text' => 'https://google.com/file.pdf', 'type' => ExternalLinkManager::TYPE_AUTO)); + $this->assertSame($attachmentLinkProvider, $externalLinkManager->find()); + } + + public function testFindProviderWithSelectedType() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $externalLinkManager->setUserInput(array('text' => 'https://google.com/', 'type' => $webLinkProvider->getType())); + $this->assertSame($webLinkProvider, $externalLinkManager->find()); + + $externalLinkManager->setUserInput(array('text' => 'https://google.com/file.pdf', 'type' => $attachmentLinkProvider->getType())); + $this->assertSame($attachmentLinkProvider, $externalLinkManager->find()); + } + + public function testFindProviderWithSelectedTypeNotFound() + { + $externalLinkManager = new ExternalLinkManager($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $externalLinkManager->register($webLinkProvider); + $externalLinkManager->register($attachmentLinkProvider); + + $this->setExpectedException('\Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound'); + $externalLinkManager->setUserInput(array('text' => 'https://google.com/', 'type' => 'not found')); + $externalLinkManager->find(); + } +} diff --git a/tests/units/Core/HelperTest.php b/tests/units/Core/HelperTest.php new file mode 100644 index 00000000..b766dd96 --- /dev/null +++ b/tests/units/Core/HelperTest.php @@ -0,0 +1,17 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Core\Helper; + +class HelperTest extends Base +{ + public function testRegister() + { + $helper = new Helper($this->container); + $helper->register('foobar', '\Stdclass'); + + $this->assertInstanceOf('Stdclass', $helper->foobar); + $this->assertInstanceOf('Stdclass', $helper->getHelper('foobar')); + } +} diff --git a/tests/units/Core/Http/OAuth2Test.php b/tests/units/Core/Http/OAuth2Test.php index d703dd7a..c68ae116 100644 --- a/tests/units/Core/Http/OAuth2Test.php +++ b/tests/units/Core/Http/OAuth2Test.php @@ -27,17 +27,27 @@ class OAuth2Test extends Base public function testAccessToken() { + $params = array( + 'code' => 'something', + 'client_id' => 'A', + 'client_secret' => 'B', + 'redirect_uri' => 'C', + 'grant_type' => 'authorization_code', + ); + + $response = json_encode(array( + 'token_type' => 'bearer', + 'access_token' => 'plop', + )); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('postForm') + ->with('E', $params, array('Accept: application/json')) + ->will($this->returnValue($response)); + $oauth = new OAuth2($this->container); $oauth->createService('A', 'B', 'C', 'D', 'E', array('f', 'g')); $oauth->getAccessToken('something'); - - $data = $this->container['httpClient']->getData(); - $this->assertEquals('something', $data['code']); - $this->assertEquals('A', $data['client_id']); - $this->assertEquals('B', $data['client_secret']); - $this->assertEquals('C', $data['redirect_uri']); - $this->assertEquals('authorization_code', $data['grant_type']); - - $this->assertEquals('E', $this->container['httpClient']->getUrl()); } } diff --git a/tests/units/Core/Http/RequestTest.php b/tests/units/Core/Http/RequestTest.php index 217698f9..6fa796f7 100644 --- a/tests/units/Core/Http/RequestTest.php +++ b/tests/units/Core/Http/RequestTest.php @@ -102,6 +102,12 @@ class RequestTest extends Base $request = new Request($this->container, array('HTTPS' => '1'), array(), array(), array(), array()); $this->assertTrue($request->isHTTPS()); + + $request = new Request($this->container, array('HTTP_X_FORWARDED_PROTO' => 'https'), array(), array(), array(), array()); + $this->assertTrue($request->isHTTPS()); + + $request = new Request($this->container, array('HTTP_X_FORWARDED_PROTO' => 'http'), array(), array(), array(), array()); + $this->assertFalse($request->isHTTPS()); } public function testGetCookie() diff --git a/tests/units/Core/Http/RouterTest.php b/tests/units/Core/Http/RouterTest.php index 0b200ab5..75a3ba4f 100644 --- a/tests/units/Core/Http/RouterTest.php +++ b/tests/units/Core/Http/RouterTest.php @@ -40,21 +40,25 @@ namespace { $this->assertEquals('userImport', $dispatcher->sanitize('userImport', 'default')); } - public function testGetPath() + public function testGetPathWithFolder() { - $dispatcher = new Router($this->container); - - $this->container['helper'] = new Helper($this->container); + $router = new Router($this->container); $this->container['request'] = new Request($this->container, array('PHP_SELF' => '/index.php', 'REQUEST_URI' => '/a/b/c', 'REQUEST_METHOD' => 'GET')); - $this->assertEquals('a/b/c', $dispatcher->getPath()); + $this->assertEquals('a/b/c', $router->getPath()); + } - $this->container['helper'] = new Helper($this->container); + public function testGetPathWithQueryString() + { + $router = new Router($this->container); $this->container['request'] = new Request($this->container, array('PHP_SELF' => '/index.php', 'REQUEST_URI' => '/a/b/something?test=a', 'QUERY_STRING' => 'test=a', 'REQUEST_METHOD' => 'GET')); - $this->assertEquals('a/b/something', $dispatcher->getPath()); + $this->assertEquals('a/b/something', $router->getPath()); + } - $this->container['helper'] = new Helper($this->container); + public function testGetPathWithSubFolderAndQueryString() + { + $router = new Router($this->container); $this->container['request'] = new Request($this->container, array('PHP_SELF' => '/a/index.php', 'REQUEST_URI' => '/a/b/something?test=a', 'QUERY_STRING' => 'test=a', 'REQUEST_METHOD' => 'GET')); - $this->assertEquals('b/something', $dispatcher->getPath()); + $this->assertEquals('b/something', $router->getPath()); } public function testDispatcherWithControllerNotFound() diff --git a/tests/units/Core/Ldap/LdapGroupTest.php b/tests/units/Core/Ldap/LdapGroupTest.php index 3f538249..4341ffc8 100644 --- a/tests/units/Core/Ldap/LdapGroupTest.php +++ b/tests/units/Core/Ldap/LdapGroupTest.php @@ -4,7 +4,6 @@ require_once __DIR__.'/../../Base.php'; use Kanboard\Core\Ldap\Group; use Kanboard\Core\Ldap\Entries; -use Kanboard\Core\Security\Role; class LdapGroupTest extends Base { diff --git a/tests/units/Core/TemplateTest.php b/tests/units/Core/TemplateTest.php index 6e5ae00d..bd476c51 100644 --- a/tests/units/Core/TemplateTest.php +++ b/tests/units/Core/TemplateTest.php @@ -8,7 +8,7 @@ class TemplateTest extends Base { public function testGetTemplateFile() { - $t = new Template($this->container); + $t = new Template($this->container['helper']); $this->assertStringEndsWith( 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php', $t->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b') @@ -17,7 +17,7 @@ class TemplateTest extends Base public function testGetPluginTemplateFile() { - $t = new Template($this->container); + $t = new Template($this->container['helper']); $this->assertStringEndsWith( 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.'Myplugin'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php', $t->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b') @@ -26,7 +26,7 @@ class TemplateTest extends Base public function testGetOverridedTemplateFile() { - $t = new Template($this->container); + $t = new Template($this->container['helper']); $t->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c'); $this->assertStringEndsWith( diff --git a/tests/units/Model/TaskExportTest.php b/tests/units/Export/TaskExportTest.php index b40b0771..f0637c25 100644 --- a/tests/units/Model/TaskExportTest.php +++ b/tests/units/Export/TaskExportTest.php @@ -1,13 +1,11 @@ <?php -require_once __DIR__.'/../Base.php'; +require_once __DIR__ . '/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; -use Kanboard\Model\TaskExport; +use Kanboard\Export\TaskExport; use Kanboard\Model\Project; use Kanboard\Model\Category; -use Kanboard\Model\User; use Kanboard\Model\Swimlane; class TaskExportTest extends Base diff --git a/tests/units/Export/TransitionExportTest.php b/tests/units/Export/TransitionExportTest.php new file mode 100644 index 00000000..7ff3082e --- /dev/null +++ b/tests/units/Export/TransitionExportTest.php @@ -0,0 +1,45 @@ +<?php + +require_once __DIR__ . '/../Base.php'; + +use Kanboard\Model\TaskCreation; +use Kanboard\Model\Transition; +use Kanboard\Export\TransitionExport; +use Kanboard\Model\Project; + +class TransitionExportTest extends Base +{ + public function testExport() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $transitionModel = new Transition($this->container); + $transitionExportModel = new TransitionExport($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $task_event = array( + 'project_id' => 1, + 'task_id' => 1, + 'src_column_id' => 1, + 'dst_column_id' => 2, + 'date_moved' => time() - 3600 + ); + + $this->assertTrue($transitionModel->save(1, $task_event)); + + $export = $transitionExportModel->export(1, date('Y-m-d'), date('Y-m-d')); + $this->assertCount(2, $export); + + $this->assertEquals( + array('Id', 'Task Title', 'Source column', 'Destination column', 'Executer', 'Date', 'Time spent'), + $export[0] + ); + + $this->assertEquals( + array(1, 'test', 'Backlog', 'Ready', 'admin', date('m/d/Y H:i', time()), 1.0), + $export[1] + ); + } +} diff --git a/tests/units/ExternalLink/AttachmentLinkProviderTest.php b/tests/units/ExternalLink/AttachmentLinkProviderTest.php new file mode 100644 index 00000000..fe374664 --- /dev/null +++ b/tests/units/ExternalLink/AttachmentLinkProviderTest.php @@ -0,0 +1,64 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\ExternalLink\AttachmentLinkProvider; + +class AttachmentLinkProviderTest extends Base +{ + public function testGetName() + { + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + $this->assertEquals('Attachment', $attachmentLinkProvider->getName()); + } + + public function testGetType() + { + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + $this->assertEquals('attachment', $attachmentLinkProvider->getType()); + } + + public function testGetDependencies() + { + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + $this->assertEquals(array('related' => 'Related'), $attachmentLinkProvider->getDependencies()); + } + + public function testMatch() + { + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/FILE.DOC'); + $this->assertTrue($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/folder/document.PDF'); + $this->assertTrue($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/archive.zip'); + $this->assertTrue($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput(' https://kanboard.net/folder/archive.tar '); + $this->assertTrue($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http:// invalid url'); + $this->assertFalse($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput(''); + $this->assertFalse($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/folder/document.html'); + $this->assertFalse($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/folder/DOC.HTML'); + $this->assertFalse($attachmentLinkProvider->match()); + + $attachmentLinkProvider->setUserTextInput('http://kanboard.net/folder/document.do'); + $this->assertFalse($attachmentLinkProvider->match()); + } + + public function testGetLink() + { + $attachmentLinkProvider = new AttachmentLinkProvider($this->container); + $this->assertInstanceOf('\Kanboard\ExternalLink\AttachmentLink', $attachmentLinkProvider->getLink()); + } +} diff --git a/tests/units/ExternalLink/AttachmentLinkTest.php b/tests/units/ExternalLink/AttachmentLinkTest.php new file mode 100644 index 00000000..0211869c --- /dev/null +++ b/tests/units/ExternalLink/AttachmentLinkTest.php @@ -0,0 +1,18 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\ExternalLink\AttachmentLink; + +class AttachmentLinkTest extends Base +{ + public function testGetTitleFromUrl() + { + $url = 'https://kanboard.net/folder/document.pdf'; + + $link = new AttachmentLink($this->container); + $link->setUrl($url); + $this->assertEquals($url, $link->getUrl()); + $this->assertEquals('document.pdf', $link->getTitle()); + } +} diff --git a/tests/units/ExternalLink/WebLinkProviderTest.php b/tests/units/ExternalLink/WebLinkProviderTest.php new file mode 100644 index 00000000..95110ed8 --- /dev/null +++ b/tests/units/ExternalLink/WebLinkProviderTest.php @@ -0,0 +1,52 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\ExternalLink\WebLinkProvider; + +class WebLinkProviderTest extends Base +{ + public function testGetName() + { + $webLinkProvider = new WebLinkProvider($this->container); + $this->assertEquals('Web Link', $webLinkProvider->getName()); + } + + public function testGetType() + { + $webLinkProvider = new WebLinkProvider($this->container); + $this->assertEquals('weblink', $webLinkProvider->getType()); + } + + public function testGetDependencies() + { + $webLinkProvider = new WebLinkProvider($this->container); + $this->assertEquals(array('related' => 'Related'), $webLinkProvider->getDependencies()); + } + + public function testMatch() + { + $webLinkProvider = new WebLinkProvider($this->container); + + $webLinkProvider->setUserTextInput('http://kanboard.net/'); + $this->assertTrue($webLinkProvider->match()); + + $webLinkProvider->setUserTextInput('http://kanboard.net/mypage'); + $this->assertTrue($webLinkProvider->match()); + + $webLinkProvider->setUserTextInput(' https://kanboard.net/ '); + $this->assertTrue($webLinkProvider->match()); + + $webLinkProvider->setUserTextInput('http:// invalid url'); + $this->assertFalse($webLinkProvider->match()); + + $webLinkProvider->setUserTextInput(''); + $this->assertFalse($webLinkProvider->match()); + } + + public function testGetLink() + { + $webLinkProvider = new WebLinkProvider($this->container); + $this->assertInstanceOf('\Kanboard\ExternalLink\WebLink', $webLinkProvider->getLink()); + } +} diff --git a/tests/units/ExternalLink/WebLinkTest.php b/tests/units/ExternalLink/WebLinkTest.php new file mode 100644 index 00000000..85487e15 --- /dev/null +++ b/tests/units/ExternalLink/WebLinkTest.php @@ -0,0 +1,45 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\ExternalLink\WebLink; + +class WebLinkTest extends Base +{ + public function testGetTitleFromHtml() + { + $url = 'http://kanboard.net/something'; + $title = 'My title'; + $html = '<!DOCTYPE html><html><head><title> '.$title.' </title></head><body>Test</body></html>'; + + $webLink = new WebLink($this->container); + $webLink->setUrl($url); + $this->assertEquals($url, $webLink->getUrl()); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('get') + ->with($url) + ->will($this->returnValue($html)); + + $this->assertEquals($title, $webLink->getTitle()); + } + + public function testGetTitleFromUrl() + { + $url = 'http://kanboard.net/something'; + $html = '<!DOCTYPE html><html><head></head><body>Test</body></html>'; + + $webLink = new WebLink($this->container); + $webLink->setUrl($url); + $this->assertEquals($url, $webLink->getUrl()); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('get') + ->with($url) + ->will($this->returnValue($html)); + + $this->assertEquals('kanboard.net/something', $webLink->getTitle()); + } +} diff --git a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php b/tests/units/Formatter/TaskFilterCalendarFormatterTest.php index a42e865f..09dd0de6 100644 --- a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php +++ b/tests/units/Formatter/TaskFilterCalendarFormatterTest.php @@ -3,14 +3,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Formatter\TaskFilterCalendarFormatter; -use Kanboard\Model\Project; -use Kanboard\Model\User; -use Kanboard\Model\TaskCreation; -use Kanboard\Core\DateParser; -use Kanboard\Model\Category; -use Kanboard\Model\Subtask; -use Kanboard\Model\Config; -use Kanboard\Model\Swimlane; class TaskFilterCalendarFormatterTest extends Base { diff --git a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php b/tests/units/Formatter/TaskFilterICalendarFormatterTest.php index 915cdda2..6de9cf0f 100644 --- a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php +++ b/tests/units/Formatter/TaskFilterICalendarFormatterTest.php @@ -8,10 +8,7 @@ use Kanboard\Model\Project; use Kanboard\Model\User; use Kanboard\Model\TaskCreation; use Kanboard\Core\DateParser; -use Kanboard\Model\Category; -use Kanboard\Model\Subtask; use Kanboard\Model\Config; -use Kanboard\Model\Swimlane; class TaskFilterICalendarFormatterTest extends Base { diff --git a/tests/units/Helper/AppHelperTest.php b/tests/units/Helper/AppHelperTest.php index 0639b7aa..dee0750e 100644 --- a/tests/units/Helper/AppHelperTest.php +++ b/tests/units/Helper/AppHelperTest.php @@ -3,26 +3,25 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Core\Session\FlashMessage; -use Kanboard\Helper\App; -use Kanboard\Model\Config; +use Kanboard\Helper\AppHelper; class AppHelperTest extends Base { public function testJsLang() { - $h = new App($this->container); + $h = new AppHelper($this->container); $this->assertEquals('en', $h->jsLang()); } public function testTimezone() { - $h = new App($this->container); + $h = new AppHelper($this->container); $this->assertEquals('UTC', $h->getTimezone()); } public function testFlashMessage() { - $h = new App($this->container); + $h = new AppHelper($this->container); $f = new FlashMessage($this->container); $this->assertEmpty($h->flashMessage()); diff --git a/tests/units/Helper/AssetHelperTest.php b/tests/units/Helper/AssetHelperTest.php index 64fcd569..6ef5accd 100644 --- a/tests/units/Helper/AssetHelperTest.php +++ b/tests/units/Helper/AssetHelperTest.php @@ -2,14 +2,14 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Asset; +use Kanboard\Helper\AssetHelper; use Kanboard\Model\Config; class AssetHelperTest extends Base { public function testCustomCss() { - $h = new Asset($this->container); + $h = new AssetHelper($this->container); $c = new Config($this->container); $this->assertEmpty($h->customCss()); diff --git a/tests/units/Helper/DatetimeHelperTest.php b/tests/units/Helper/DatetimeHelperTest.php index 8e9c461b..19b1b704 100644 --- a/tests/units/Helper/DatetimeHelperTest.php +++ b/tests/units/Helper/DatetimeHelperTest.php @@ -2,27 +2,49 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Dt; +use Kanboard\Helper\DateHelper; class DatetimeHelperTest extends Base { + public function testGetTime() + { + $helper = new DateHelper($this->container); + $this->assertEquals('17:25', $helper->time(1422206700)); + } + + public function testGetDate() + { + $helper = new DateHelper($this->container); + $this->assertEquals('01/25/2015', $helper->date(1422206700)); + $this->assertEquals('01/25/2015', $helper->date('2015-01-25')); + $this->assertEquals('', $helper->date('0')); + $this->assertEquals('', $helper->date(0)); + $this->assertEquals('', $helper->date('')); + } + + public function testGetDatetime() + { + $helper = new DateHelper($this->container); + $this->assertEquals('01/25/2015 17:25', $helper->datetime(1422206700)); + } + public function testAge() { - $h = new Dt($this->container); - - $this->assertEquals('<15m', $h->age(0, 30)); - $this->assertEquals('<30m', $h->age(0, 1000)); - $this->assertEquals('<1h', $h->age(0, 3000)); - $this->assertEquals('~2h', $h->age(0, 2*3600)); - $this->assertEquals('1d', $h->age(0, 30*3600)); - $this->assertEquals('2d', $h->age(0, 65*3600)); + $helper = new DateHelper($this->container); + + $this->assertEquals('<15m', $helper->age(0, 30)); + $this->assertEquals('<30m', $helper->age(0, 1000)); + $this->assertEquals('<1h', $helper->age(0, 3000)); + $this->assertEquals('~2h', $helper->age(0, 2*3600)); + $this->assertEquals('1d', $helper->age(0, 30*3600)); + $this->assertEquals('2d', $helper->age(0, 65*3600)); } public function testGetDayHours() { - $h = new Dt($this->container); + $helper = new DateHelper($this->container); - $slots = $h->getDayHours(); + $slots = $helper->getDayHours(); $this->assertNotEmpty($slots); $this->assertCount(48, $slots); @@ -36,9 +58,9 @@ class DatetimeHelperTest extends Base public function testGetWeekDays() { - $h = new Dt($this->container); + $helper = new DateHelper($this->container); - $slots = $h->getWeekDays(); + $slots = $helper->getWeekDays(); $this->assertNotEmpty($slots); $this->assertCount(7, $slots); @@ -48,9 +70,9 @@ class DatetimeHelperTest extends Base public function testGetWeekDay() { - $h = new Dt($this->container); + $helper = new DateHelper($this->container); - $this->assertEquals('Monday', $h->getWeekDay(1)); - $this->assertEquals('Sunday', $h->getWeekDay(7)); + $this->assertEquals('Monday', $helper->getWeekDay(1)); + $this->assertEquals('Sunday', $helper->getWeekDay(7)); } } diff --git a/tests/units/Helper/FileHelperText.php b/tests/units/Helper/FileHelperText.php index a681c890..215b024b 100644 --- a/tests/units/Helper/FileHelperText.php +++ b/tests/units/Helper/FileHelperText.php @@ -2,14 +2,35 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\File; +use Kanboard\Helper\FileHelper; class FileHelperTest extends Base { public function testIcon() { - $h = new File($this->container); - $this->assertEquals('fa-file-image-o', $h->icon('test.png')); - $this->assertEquals('fa-file-o', $h->icon('test')); + $helper = new FileHelper($this->container); + $this->assertEquals('fa-file-image-o', $helper->icon('test.png')); + $this->assertEquals('fa-file-o', $helper->icon('test')); + } + + public function testGetMimeType() + { + $helper = new FileHelper($this->container); + + $this->assertEquals('image/jpeg', $helper->getImageMimeType('My File.JPG')); + $this->assertEquals('image/jpeg', $helper->getImageMimeType('My File.jpeg')); + $this->assertEquals('image/png', $helper->getImageMimeType('My File.PNG')); + $this->assertEquals('image/gif', $helper->getImageMimeType('My File.gif')); + $this->assertEquals('image/jpeg', $helper->getImageMimeType('My File.bmp')); + $this->assertEquals('image/jpeg', $helper->getImageMimeType('My File')); + } + + public function testGetPreviewType() + { + $helper = new FileHelper($this->container); + $this->assertEquals('text', $helper->getPreviewType('test.txt')); + $this->assertEquals('markdown', $helper->getPreviewType('test.markdown')); + $this->assertEquals('md', $helper->getPreviewType('test.md')); + $this->assertEquals(null, $helper->getPreviewType('test.doc')); } } diff --git a/tests/units/Helper/HookHelperTest.php b/tests/units/Helper/HookHelperTest.php index aec4a771..6e03acd1 100644 --- a/tests/units/Helper/HookHelperTest.php +++ b/tests/units/Helper/HookHelperTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Hook; +use Kanboard\Helper\HookHelper; class HookHelperTest extends Base { @@ -10,7 +10,7 @@ class HookHelperTest extends Base { $this->container['template'] = $this ->getMockBuilder('\Kanboard\Core\Template') - ->setConstructorArgs(array($this->container)) + ->setConstructorArgs(array($this->container['helper'])) ->setMethods(array('render')) ->getMock(); @@ -32,7 +32,7 @@ class HookHelperTest extends Base ) ->will($this->returnValue('tpl2_content')); - $h = new Hook($this->container); + $h = new HookHelper($this->container); $h->attach('test', 'tpl1'); $h->attach('test', 'tpl2'); $this->assertEquals('tpl1_contenttpl2_content', $h->render('test')); @@ -41,7 +41,7 @@ class HookHelperTest extends Base public function testAssetHooks() { $this->container['helper']->asset = $this - ->getMockBuilder('\Kanboard\Helper\Asset') + ->getMockBuilder('\Kanboard\Helper\AssetHelper') ->setConstructorArgs(array($this->container)) ->setMethods(array('css', 'js')) ->getMock(); @@ -64,7 +64,7 @@ class HookHelperTest extends Base ) ->will($this->returnValue('<script src="skin.js"></script>')); - $h = new Hook($this->container); + $h = new HookHelper($this->container); $h->attach('test1', 'skin.css'); $h->attach('test2', 'skin.js'); diff --git a/tests/units/Helper/TaskHelperTest.php b/tests/units/Helper/TaskHelperTest.php index 726188e4..454da553 100644 --- a/tests/units/Helper/TaskHelperTest.php +++ b/tests/units/Helper/TaskHelperTest.php @@ -2,20 +2,20 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Task; +use Kanboard\Helper\TaskHelper; class TaskHelperTest extends Base { public function testSelectPriority() { - $helper = new Task($this->container); + $helper = new TaskHelper($this->container); $this->assertNotEmpty($helper->selectPriority(array('priority_end' => '3', 'priority_start' => '1', 'priority_default' => '2'), array())); $this->assertEmpty($helper->selectPriority(array('priority_end' => '3', 'priority_start' => '3', 'priority_default' => '2'), array())); } public function testFormatPriority() { - $helper = new Task($this->container); + $helper = new TaskHelper($this->container); $this->assertEquals( '<span class="task-board-priority" title="Task priority">P2</span>', diff --git a/tests/units/Helper/TextHelperTest.php b/tests/units/Helper/TextHelperTest.php index a4bdfa91..d7324dfd 100644 --- a/tests/units/Helper/TextHelperTest.php +++ b/tests/units/Helper/TextHelperTest.php @@ -2,13 +2,13 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Text; +use Kanboard\Helper\TextHelper; class TextHelperTest extends Base { public function testMarkdownTaskLink() { - $h = new Text($this->container); + $h = new TextHelper($this->container); $this->assertEquals('<p>Test</p>', $h->markdown('Test')); @@ -33,13 +33,13 @@ class TextHelperTest extends Base public function testMarkdownUserLink() { - $h = new Text($this->container); + $h = new TextHelper($this->container); $this->assertEquals('<p>Text <a href="?controller=user&action=profile&user_id=1" class="user-mention-link">@admin</a> @notfound</p>', $h->markdown('Text @admin @notfound')); } public function testFormatBytes() { - $h = new Text($this->container); + $h = new TextHelper($this->container); $this->assertEquals('1k', $h->bytes(1024)); $this->assertEquals('33.71k', $h->bytes(34520)); @@ -47,7 +47,7 @@ class TextHelperTest extends Base public function testContains() { - $h = new Text($this->container); + $h = new TextHelper($this->container); $this->assertTrue($h->contains('abc', 'b')); $this->assertFalse($h->contains('abc', 'd')); @@ -55,7 +55,7 @@ class TextHelperTest extends Base public function testInList() { - $h = new Text($this->container); + $h = new TextHelper($this->container); $this->assertEquals('?', $h->in('a', array('b' => 'c'))); $this->assertEquals('c', $h->in('b', array('b' => 'c'))); } diff --git a/tests/units/Helper/UrlHelperTest.php b/tests/units/Helper/UrlHelperTest.php index 9f26a802..34ab7604 100644 --- a/tests/units/Helper/UrlHelperTest.php +++ b/tests/units/Helper/UrlHelperTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\Url; +use Kanboard\Helper\UrlHelper; use Kanboard\Model\Config; use Kanboard\Core\Http\Request; @@ -10,9 +10,9 @@ class UrlHelperTest extends Base { public function testPluginLink() { - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals( - '<a href="?controller=a&action=b&d=e&plugin=something" class="f" title="g" target="_blank">label</a>', + '<a href="?controller=a&action=b&d=e&plugin=something" class="f" title=\'g\' target="_blank">label</a>', $h->link('label', 'a', 'b', array('d' => 'e', 'plugin' => 'something'), false, 'f', 'g', true) ); } @@ -22,25 +22,25 @@ class UrlHelperTest extends Base $this->container['route']->enable(); $this->container['route']->addRoute('/myplugin/something/:d', 'a', 'b', 'something'); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals( - '<a href="myplugin/something/e" class="f" title="g" target="_blank">label</a>', + '<a href="myplugin/something/e" class="f" title=\'g\' target="_blank">label</a>', $h->link('label', 'a', 'b', array('d' => 'e', 'plugin' => 'something'), false, 'f', 'g', true) ); } public function testAppLink() { - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals( - '<a href="?controller=a&action=b&d=e" class="f" title="g" target="_blank">label</a>', + '<a href="?controller=a&action=b&d=e" class="f" title=\'g\' target="_blank">label</a>', $h->link('label', 'a', 'b', array('d' => 'e'), false, 'f', 'g', true) ); } public function testHref() { - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals( '?controller=a&action=b&d=e', $h->href('a', 'b', array('d' => 'e')) @@ -49,7 +49,7 @@ class UrlHelperTest extends Base public function testTo() { - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals( '?controller=a&action=b&d=e', $h->to('a', 'b', array('d' => 'e')) @@ -64,7 +64,7 @@ class UrlHelperTest extends Base ) ); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('/kanboard/', $h->dir()); $this->container['request'] = new Request($this->container, array( @@ -73,7 +73,7 @@ class UrlHelperTest extends Base ) ); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('/', $h->dir()); } @@ -87,7 +87,7 @@ class UrlHelperTest extends Base ) ); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('http://localhost/', $h->server()); $this->container['request'] = new Request($this->container, array( @@ -98,7 +98,7 @@ class UrlHelperTest extends Base ) ); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('http://kb:1234/', $h->server()); } @@ -112,14 +112,14 @@ class UrlHelperTest extends Base ) ); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('http://kb:1234/', $h->base()); $c = new Config($this->container); $c->save(array('application_url' => 'https://mykanboard/')); $this->container['memoryCache']->flush(); - $h = new Url($this->container); + $h = new UrlHelper($this->container); $this->assertEquals('https://mykanboard/', $c->get('application_url')); $this->assertEquals('https://mykanboard/', $h->base()); } diff --git a/tests/units/Helper/UserHelperTest.php b/tests/units/Helper/UserHelperTest.php index 67ccee73..7ee6e8bb 100644 --- a/tests/units/Helper/UserHelperTest.php +++ b/tests/units/Helper/UserHelperTest.php @@ -2,7 +2,7 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Helper\User; +use Kanboard\Helper\UserHelper; use Kanboard\Model\Project; use Kanboard\Model\ProjectUserRole; use Kanboard\Model\User as UserModel; @@ -12,7 +12,7 @@ class UserHelperTest extends Base { public function testInitials() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $this->assertEquals('CN', $helper->getInitials('chuck norris')); $this->assertEquals('A', $helper->getInitials('admin')); @@ -20,7 +20,7 @@ class UserHelperTest extends Base public function testGetRoleName() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $this->assertEquals('Administrator', $helper->getRoleName(Role::APP_ADMIN)); $this->assertEquals('Manager', $helper->getRoleName(Role::APP_MANAGER)); $this->assertEquals('Project Viewer', $helper->getRoleName(Role::PROJECT_VIEWER)); @@ -28,7 +28,7 @@ class UserHelperTest extends Base public function testHasAccessForAdmins() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $this->container['sessionStorage']->user = array( 'id' => 2, @@ -36,13 +36,13 @@ class UserHelperTest extends Base ); $this->assertTrue($helper->hasAccess('user', 'create')); - $this->assertTrue($helper->hasAccess('project', 'create')); - $this->assertTrue($helper->hasAccess('project', 'createPrivate')); + $this->assertTrue($helper->hasAccess('ProjectCreation', 'create')); + $this->assertTrue($helper->hasAccess('ProjectCreation', 'createPrivate')); } public function testHasAccessForManagers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $this->container['sessionStorage']->user = array( 'id' => 2, @@ -50,13 +50,13 @@ class UserHelperTest extends Base ); $this->assertFalse($helper->hasAccess('user', 'create')); - $this->assertTrue($helper->hasAccess('project', 'create')); - $this->assertTrue($helper->hasAccess('project', 'createPrivate')); + $this->assertTrue($helper->hasAccess('ProjectCreation', 'create')); + $this->assertTrue($helper->hasAccess('ProjectCreation', 'createPrivate')); } public function testHasAccessForUsers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $this->container['sessionStorage']->user = array( 'id' => 2, @@ -64,13 +64,13 @@ class UserHelperTest extends Base ); $this->assertFalse($helper->hasAccess('user', 'create')); - $this->assertFalse($helper->hasAccess('project', 'create')); - $this->assertTrue($helper->hasAccess('project', 'createPrivate')); + $this->assertFalse($helper->hasAccess('ProjectCreation', 'create')); + $this->assertTrue($helper->hasAccess('ProjectCreation', 'createPrivate')); } public function testHasProjectAccessForAdmins() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $project = new Project($this->container); $this->container['sessionStorage']->user = array( @@ -86,7 +86,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForManagers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $project = new Project($this->container); $this->container['sessionStorage']->user = array( @@ -102,7 +102,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForUsers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $project = new Project($this->container); $this->container['sessionStorage']->user = array( @@ -118,7 +118,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForAppManagerAndProjectManagers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $user = new UserModel($this->container); $project = new Project($this->container); $projectUserRole = new ProjectUserRole($this->container); @@ -146,7 +146,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForProjectManagers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $user = new UserModel($this->container); $project = new Project($this->container); $projectUserRole = new ProjectUserRole($this->container); @@ -174,7 +174,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForProjectMembers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $user = new UserModel($this->container); $project = new Project($this->container); $projectUserRole = new ProjectUserRole($this->container); @@ -202,7 +202,7 @@ class UserHelperTest extends Base public function testHasProjectAccessForProjectViewers() { - $helper = new User($this->container); + $helper = new UserHelper($this->container); $user = new UserModel($this->container); $project = new Project($this->container); $projectUserRole = new ProjectUserRole($this->container); diff --git a/tests/units/Model/ActionTest.php b/tests/units/Model/ActionTest.php index 8d574115..ed687846 100644 --- a/tests/units/Model/ActionTest.php +++ b/tests/units/Model/ActionTest.php @@ -6,7 +6,7 @@ use Kanboard\Model\Action; use Kanboard\Model\Project; use Kanboard\Model\Task; use Kanboard\Model\User; -use Kanboard\Model\Board; +use Kanboard\Model\Column; use Kanboard\Model\Category; use Kanboard\Model\ProjectUserRole; use Kanboard\Core\Security\Role; @@ -260,12 +260,12 @@ class ActionTest extends Base { $projectModel = new Project($this->container); $actionModel = new Action($this->container); - $boardModel = new Board($this->container); + $columnModel = new Column($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); $this->assertEquals(2, $projectModel->create(array('name' => 'test2'))); - $this->assertTrue($boardModel->updateColumn(2, 'My unique column')); + $this->assertTrue($columnModel->update(2, 'My unique column')); $this->assertEquals(1, $actionModel->create(array( 'project_id' => 1, diff --git a/tests/units/Model/BoardTest.php b/tests/units/Model/BoardTest.php index bb6c4b76..bb0778ce 100644 --- a/tests/units/Model/BoardTest.php +++ b/tests/units/Model/BoardTest.php @@ -4,6 +4,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Project; use Kanboard\Model\Board; +use Kanboard\Model\Column; use Kanboard\Model\Config; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; @@ -15,12 +16,13 @@ class BoardTest extends Base { $p = new Project($this->container); $b = new Board($this->container); + $columnModel = new Column($this->container); $c = new Config($this->container); // Default columns $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - $columns = $b->getColumnsList(1); + $columns = $columnModel->getList(1); $this->assertTrue(is_array($columns)); $this->assertEquals(4, count($columns)); @@ -37,7 +39,7 @@ class BoardTest extends Base $this->assertEquals($input, $c->get('board_columns')); $this->assertEquals(2, $p->create(array('name' => 'UnitTest2'))); - $columns = $b->getColumnsList(2); + $columns = $columnModel->getList(2); $this->assertTrue(is_array($columns)); $this->assertEquals(2, count($columns)); @@ -161,225 +163,4 @@ class BoardTest extends Base $this->assertEquals(1, $board[1]['columns'][3]['tasks'][0]['position']); $this->assertEquals(1, $board[1]['columns'][3]['tasks'][0]['swimlane_id']); } - - public function testGetColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - - $column = $b->getColumn(3); - $this->assertNotEmpty($column); - $this->assertEquals('Work in progress', $column['title']); - - $column = $b->getColumn(33); - $this->assertEmpty($column); - } - - public function testRemoveColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - $this->assertTrue($b->removeColumn(3)); - $this->assertFalse($b->removeColumn(322)); - - $columns = $b->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals(3, count($columns)); - } - - public function testUpdateColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - - $this->assertTrue($b->updateColumn(3, 'blah', 5)); - $this->assertTrue($b->updateColumn(2, 'boo')); - - $column = $b->getColumn(3); - $this->assertNotEmpty($column); - $this->assertEquals('blah', $column['title']); - $this->assertEquals(5, $column['task_limit']); - - $column = $b->getColumn(2); - $this->assertNotEmpty($column); - $this->assertEquals('boo', $column['title']); - $this->assertEquals(0, $column['task_limit']); - } - - public function testAddColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - $this->assertNotFalse($b->addColumn(1, 'another column')); - $this->assertNotFalse($b->addColumn(1, 'one more', 3, 'one more description')); - - $columns = $b->getColumns(1); - $this->assertTrue(is_array($columns)); - $this->assertEquals(6, count($columns)); - - $this->assertEquals('another column', $columns[4]['title']); - $this->assertEquals(0, $columns[4]['task_limit']); - $this->assertEquals(5, $columns[4]['position']); - - $this->assertEquals('one more', $columns[5]['title']); - $this->assertEquals(3, $columns[5]['task_limit']); - $this->assertEquals(6, $columns[5]['position']); - $this->assertEquals('one more description', $columns[5]['description']); - } - - public function testMoveColumns() - { - $p = new Project($this->container); - $b = new Board($this->container); - - // We create 2 projects - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - $this->assertEquals(2, $p->create(array('name' => 'UnitTest2'))); - - // We get the columns of the project 2 - $columns = $b->getColumns(2); - $columns_id = array_keys($b->getColumnsList(2)); - $this->assertNotEmpty($columns); - - // Initial order: 5, 6, 7, 8 - - // Move the column 1 down - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals($columns_id[0], $columns[0]['id']); - - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals($columns_id[1], $columns[1]['id']); - - $this->assertTrue($b->moveDown(2, $columns[0]['id'])); - $columns = $b->getColumns(2); // Sorted by position - - // New order: 6, 5, 7, 8 - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals($columns_id[1], $columns[0]['id']); - - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals($columns_id[0], $columns[1]['id']); - - // Move the column 3 up - $this->assertTrue($b->moveUp(2, $columns[2]['id'])); - $columns = $b->getColumns(2); - - // New order: 6, 7, 5, 8 - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals($columns_id[1], $columns[0]['id']); - - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals($columns_id[2], $columns[1]['id']); - - $this->assertEquals(3, $columns[2]['position']); - $this->assertEquals($columns_id[0], $columns[2]['id']); - - // Move column 1 up (must do nothing because it's the first column) - $this->assertFalse($b->moveUp(2, $columns[0]['id'])); - $columns = $b->getColumns(2); - - // Order: 6, 7, 5, 8 - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals($columns_id[1], $columns[0]['id']); - - // Move column 4 down (must do nothing because it's the last column) - $this->assertFalse($b->moveDown(2, $columns[3]['id'])); - $columns = $b->getColumns(2); - - // Order: 6, 7, 5, 8 - - $this->assertEquals(4, $columns[3]['position']); - $this->assertEquals($columns_id[3], $columns[3]['id']); - } - - public function testMoveUpAndRemoveColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - // We create a project - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - - // We remove the second column - $this->assertTrue($b->removeColumn(2)); - - $columns = $b->getColumns(1); - $this->assertNotEmpty($columns); - $this->assertCount(3, $columns); - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals(3, $columns[1]['position']); - $this->assertEquals(4, $columns[2]['position']); - - $this->assertEquals(1, $columns[0]['id']); - $this->assertEquals(3, $columns[1]['id']); - $this->assertEquals(4, $columns[2]['id']); - - // We move up the second column - $this->assertTrue($b->moveUp(1, $columns[1]['id'])); - - // Check the new positions - $columns = $b->getColumns(1); - $this->assertNotEmpty($columns); - $this->assertCount(3, $columns); - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals(3, $columns[2]['position']); - - $this->assertEquals(3, $columns[0]['id']); - $this->assertEquals(1, $columns[1]['id']); - $this->assertEquals(4, $columns[2]['id']); - } - - public function testMoveDownAndRemoveColumn() - { - $p = new Project($this->container); - $b = new Board($this->container); - - // We create a project - $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); - - // We remove the second column - $this->assertTrue($b->removeColumn(2)); - - $columns = $b->getColumns(1); - $this->assertNotEmpty($columns); - $this->assertCount(3, $columns); - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals(3, $columns[1]['position']); - $this->assertEquals(4, $columns[2]['position']); - - $this->assertEquals(1, $columns[0]['id']); - $this->assertEquals(3, $columns[1]['id']); - $this->assertEquals(4, $columns[2]['id']); - - // We move up the second column - $this->assertTrue($b->moveDown(1, $columns[0]['id'])); - - // Check the new positions - $columns = $b->getColumns(1); - $this->assertNotEmpty($columns); - $this->assertCount(3, $columns); - - $this->assertEquals(1, $columns[0]['position']); - $this->assertEquals(2, $columns[1]['position']); - $this->assertEquals(3, $columns[2]['position']); - - $this->assertEquals(3, $columns[0]['id']); - $this->assertEquals(1, $columns[1]['id']); - $this->assertEquals(4, $columns[2]['id']); - } } diff --git a/tests/units/Model/CategoryTest.php b/tests/units/Model/CategoryTest.php index 85d9eaae..600007d0 100644 --- a/tests/units/Model/CategoryTest.php +++ b/tests/units/Model/CategoryTest.php @@ -2,66 +2,216 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; +use Kanboard\Model\Config; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\Project; use Kanboard\Model\Category; -use Kanboard\Model\User; class CategoryTest extends Base { public function testCreation() { - $tc = new TaskCreation($this->container); - $tf = new TaskFinder($this->container); - $p = new Project($this->container); - $c = new Category($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $c->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertEquals(2, $c->create(array('name' => 'Category #2', 'project_id' => 1))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); - - $task = $tf->getById(1); - $this->assertTrue(is_array($task)); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($this->container); + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $taskFinderModel->getById(1); $this->assertEquals(2, $task['category_id']); - $category = $c->getById(2); - $this->assertTrue(is_array($category)); + $category = $categoryModel->getById(2); $this->assertEquals(2, $category['id']); $this->assertEquals('Category #2', $category['name']); $this->assertEquals(1, $category['project_id']); + } + + public function testExists() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertTrue($categoryModel->exists(1)); + $this->assertFalse($categoryModel->exists(2)); + } + + public function testGetById() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $category = $categoryModel->getById(1); + $this->assertEquals(1, $category['id']); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); + } + + public function testGetNameById() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertEquals('Category #1', $categoryModel->getNameById(1)); + $this->assertEquals('', $categoryModel->getNameById(2)); + } + + public function testGetIdByName() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertSame(1, $categoryModel->getIdByName(1, 'Category #1')); + $this->assertSame(0, $categoryModel->getIdByName(1, 'Category #2')); + } + + public function testGetList() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + + $categories = $categoryModel->getList(1, false, false); + $this->assertCount(2, $categories); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + + $categories = $categoryModel->getList(1, true, false); + $this->assertCount(3, $categories); + $this->assertEquals('No category', $categories[0]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); - $this->assertEquals(2, $c->getIdByName(1, 'Category #2')); - $this->assertEquals(0, $c->getIdByName(2, 'Category #2')); + $categories = $categoryModel->getList(1, false, true); + $this->assertCount(3, $categories); + $this->assertEquals('All categories', $categories[-1]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); - $this->assertEquals('Category #2', $c->getNameById(2)); - $this->assertEquals('', $c->getNameById(23)); + $categories = $categoryModel->getList(1, true, true); + $this->assertCount(4, $categories); + $this->assertEquals('All categories', $categories[-1]); + $this->assertEquals('No category', $categories[0]); + $this->assertEquals('Category #1', $categories[1]); + $this->assertEquals('Category #2', $categories[2]); + } + + public function testGetAll() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + + $categories = $categoryModel->getAll(1); + $this->assertCount(2, $categories); + + $this->assertEquals('Category #1', $categories[0]['name']); + $this->assertEquals('test', $categories[0]['description']); + $this->assertEquals(1, $categories[0]['project_id']); + $this->assertEquals(1, $categories[0]['id']); + + $this->assertEquals('Category #2', $categories[1]['name']); + $this->assertEquals('', $categories[1]['description']); + $this->assertEquals(1, $categories[1]['project_id']); + $this->assertEquals(2, $categories[1]['id']); + } + + public function testCreateDefaultCategories() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + $configModel = new Config($this->container); + + $this->assertTrue($configModel->save(array('project_categories' => 'C1, C2, C3'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertTrue($categoryModel->createDefaultCategories(1)); + + $categories = $categoryModel->getAll(1); + $this->assertCount(3, $categories); + $this->assertEquals('C1', $categories[0]['name']); + $this->assertEquals('C2', $categories[1]['name']); + $this->assertEquals('C3', $categories[2]['name']); + } + + public function testUpdate() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertTrue($categoryModel->update(array('id' => 1, 'description' => 'test'))); + + $category = $categoryModel->getById(1); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); } public function testRemove() { - $tc = new TaskCreation($this->container); - $tf = new TaskFinder($this->container); - $p = new Project($this->container); - $c = new Category($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $c->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertEquals(2, $c->create(array('name' => 'Category #2', 'project_id' => 1))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); - - $task = $tf->getById(1); - $this->assertTrue(is_array($task)); + $taskCreationModel = new TaskCreation($this->container); + $taskFinderModel = new TaskFinder($this->container); + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $categoryModel->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $taskFinderModel->getById(1); $this->assertEquals(2, $task['category_id']); - $this->assertTrue($c->remove(1)); - $this->assertTrue($c->remove(2)); + $this->assertTrue($categoryModel->remove(1)); + $this->assertTrue($categoryModel->remove(2)); // Make sure tasks assigned with that category are reseted - $task = $tf->getById(1); - $this->assertTrue(is_array($task)); + $task = $taskFinderModel->getById(1); $this->assertEquals(0, $task['category_id']); } + + public function testDuplicate() + { + $projectModel = new Project($this->container); + $categoryModel = new Category($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); + $this->assertEquals(1, $categoryModel->create(array('name' => 'Category #1', 'project_id' => 1, 'description' => 'test'))); + + $this->assertTrue($categoryModel->duplicate(1, 2)); + + $category = $categoryModel->getById(1); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(1, $category['project_id']); + $this->assertEquals('test', $category['description']); + + $category = $categoryModel->getById(2); + $this->assertEquals('Category #1', $category['name']); + $this->assertEquals(2, $category['project_id']); + $this->assertEquals('test', $category['description']); + } } diff --git a/tests/units/Model/ColorTest.php b/tests/units/Model/ColorTest.php new file mode 100644 index 00000000..e96ecb6b --- /dev/null +++ b/tests/units/Model/ColorTest.php @@ -0,0 +1,93 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\Color; +use Kanboard\Model\Config; + +class ColorTest extends Base +{ + public function testFind() + { + $colorModel = new Color($this->container); + $this->assertEquals('yellow', $colorModel->find('yellow')); + $this->assertEquals('yellow', $colorModel->find('Yellow')); + $this->assertEquals('dark_grey', $colorModel->find('Dark Grey')); + $this->assertEquals('dark_grey', $colorModel->find('dark_grey')); + } + + public function testGetColorProperties() + { + $colorModel = new Color($this->container); + $expected = array( + 'name' => 'Light Green', + 'background' => '#dcedc8', + 'border' => '#689f38', + ); + + $this->assertEquals($expected, $colorModel->getColorProperties('light_green')); + + $expected = array( + 'name' => 'Yellow', + 'background' => 'rgb(245, 247, 196)', + 'border' => 'rgb(223, 227, 45)', + ); + + $this->assertEquals($expected, $colorModel->getColorProperties('foobar')); + } + + public function testGetList() + { + $colorModel = new Color($this->container); + + $colors = $colorModel->getList(); + $this->assertCount(16, $colors); + $this->assertEquals('Yellow', $colors['yellow']); + + $colors = $colorModel->getList(true); + $this->assertCount(17, $colors); + $this->assertEquals('All colors', $colors['']); + $this->assertEquals('Yellow', $colors['yellow']); + } + + public function testGetDefaultColor() + { + $colorModel = new Color($this->container); + $configModel = new Config($this->container); + + $this->assertEquals('yellow', $colorModel->getDefaultColor()); + + $this->container['memoryCache']->flush(); + $this->assertTrue($configModel->save(array('default_color' => 'red'))); + $this->assertEquals('red', $colorModel->getDefaultColor()); + } + + public function testGetDefaultColors() + { + $colorModel = new Color($this->container); + + $colors = $colorModel->getDefaultColors(); + $this->assertCount(16, $colors); + } + + public function testGetBorderColor() + { + $colorModel = new Color($this->container); + $this->assertEquals('rgb(74, 227, 113)', $colorModel->getBorderColor('green')); + } + + public function testGetBackgroundColor() + { + $colorModel = new Color($this->container); + $this->assertEquals('rgb(189, 244, 203)', $colorModel->getBackgroundColor('green')); + } + + public function testGetCss() + { + $colorModel = new Color($this->container); + $css = $colorModel->getCss(); + + $this->assertStringStartsWith('div.color-yellow {', $css); + $this->assertStringEndsWith('td.color-amber { background-color: #ffe082}', $css); + } +} diff --git a/tests/units/Model/ColumnTest.php b/tests/units/Model/ColumnTest.php new file mode 100644 index 00000000..e40f89c6 --- /dev/null +++ b/tests/units/Model/ColumnTest.php @@ -0,0 +1,236 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\Project; +use Kanboard\Model\Column; + +class ColumnTest extends Base +{ + public function testGetColumn() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $column = $columnModel->getById(3); + $this->assertNotEmpty($column); + $this->assertEquals('Work in progress', $column['title']); + + $column = $columnModel->getById(33); + $this->assertEmpty($column); + } + + public function testGetFirstColumnId() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertEquals(1, $columnModel->getFirstColumnId(1)); + } + + public function testGetLastColumnId() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertEquals(4, $columnModel->getLastColumnId(1)); + } + + public function testGetLastColumnPosition() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertEquals(4, $columnModel->getLastColumnPosition(1)); + } + + public function testGetColumnIdByTitle() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertEquals(2, $columnModel->getColumnIdByTitle(1, 'Ready')); + } + + public function testGetTitleByColumnId() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertEquals('Work in progress', $columnModel->getColumnTitleById(3)); + } + + public function testGetAll() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $columns = $columnModel->getAll(1); + $this->assertCount(4, $columns); + + $this->assertEquals(1, $columns[0]['id']); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals('Backlog', $columns[0]['title']); + + $this->assertEquals(2, $columns[1]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals('Ready', $columns[1]['title']); + + $this->assertEquals(3, $columns[2]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals('Work in progress', $columns[2]['title']); + + $this->assertEquals(4, $columns[3]['id']); + $this->assertEquals(4, $columns[3]['position']); + $this->assertEquals('Done', $columns[3]['title']); + } + + public function testGetList() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $columns = $columnModel->getList(1); + $this->assertCount(4, $columns); + $this->assertEquals('Backlog', $columns[1]); + $this->assertEquals('Ready', $columns[2]); + $this->assertEquals('Work in progress', $columns[3]); + $this->assertEquals('Done', $columns[4]); + + $columns = $columnModel->getList(1, true); + $this->assertCount(5, $columns); + $this->assertEquals('All columns', $columns[-1]); + $this->assertEquals('Backlog', $columns[1]); + $this->assertEquals('Ready', $columns[2]); + $this->assertEquals('Work in progress', $columns[3]); + $this->assertEquals('Done', $columns[4]); + } + + public function testAddColumn() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertNotFalse($columnModel->create(1, 'another column')); + $this->assertNotFalse($columnModel->create(1, 'one more', 3, 'one more description')); + + $columns = $columnModel->getAll(1); + $this->assertTrue(is_array($columns)); + $this->assertEquals(6, count($columns)); + + $this->assertEquals('another column', $columns[4]['title']); + $this->assertEquals(0, $columns[4]['task_limit']); + $this->assertEquals(5, $columns[4]['position']); + + $this->assertEquals('one more', $columns[5]['title']); + $this->assertEquals(3, $columns[5]['task_limit']); + $this->assertEquals(6, $columns[5]['position']); + $this->assertEquals('one more description', $columns[5]['description']); + } + + public function testUpdateColumn() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $this->assertTrue($columnModel->update(3, 'blah', 5)); + $this->assertTrue($columnModel->update(2, 'boo')); + + $column = $columnModel->getById(3); + $this->assertNotEmpty($column); + $this->assertEquals('blah', $column['title']); + $this->assertEquals(5, $column['task_limit']); + + $column = $columnModel->getById(2); + $this->assertNotEmpty($column); + $this->assertEquals('boo', $column['title']); + $this->assertEquals(0, $column['task_limit']); + } + + public function testRemoveColumn() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + $this->assertTrue($columnModel->remove(3)); + $this->assertFalse($columnModel->remove(322)); + + $columns = $columnModel->getAll(1); + $this->assertTrue(is_array($columns)); + $this->assertEquals(3, count($columns)); + } + + public function testChangePosition() + { + $projectModel = new Project($this->container); + $columnModel = new Column($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + + $columns = $columnModel->getAll(1); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals(1, $columns[0]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals(2, $columns[1]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals(3, $columns[2]['id']); + + $this->assertTrue($columnModel->changePosition(1, 3, 2)); + + $columns = $columnModel->getAll(1); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals(1, $columns[0]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals(3, $columns[1]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals(2, $columns[2]['id']); + + $this->assertTrue($columnModel->changePosition(1, 2, 1)); + + $columns = $columnModel->getAll(1); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals(2, $columns[0]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals(1, $columns[1]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals(3, $columns[2]['id']); + + $this->assertTrue($columnModel->changePosition(1, 2, 2)); + + $columns = $columnModel->getAll(1); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals(1, $columns[0]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals(2, $columns[1]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals(3, $columns[2]['id']); + + $this->assertTrue($columnModel->changePosition(1, 4, 1)); + + $columns = $columnModel->getAll(1); + $this->assertEquals(1, $columns[0]['position']); + $this->assertEquals(4, $columns[0]['id']); + $this->assertEquals(2, $columns[1]['position']); + $this->assertEquals(1, $columns[1]['id']); + $this->assertEquals(3, $columns[2]['position']); + $this->assertEquals(2, $columns[2]['id']); + + $this->assertFalse($columnModel->changePosition(1, 2, 0)); + $this->assertFalse($columnModel->changePosition(1, 2, 5)); + } +} diff --git a/tests/units/Model/CommentTest.php b/tests/units/Model/CommentTest.php index ec4e7a56..bb96e4f4 100644 --- a/tests/units/Model/CommentTest.php +++ b/tests/units/Model/CommentTest.php @@ -2,7 +2,6 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\Project; use Kanboard\Model\Comment; diff --git a/tests/units/Model/ConfigTest.php b/tests/units/Model/ConfigTest.php index 447c9238..6ccdbef9 100644 --- a/tests/units/Model/ConfigTest.php +++ b/tests/units/Model/ConfigTest.php @@ -3,7 +3,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Config; -use Kanboard\Core\Session\SessionManager; class ConfigTest extends Base { diff --git a/tests/units/Model/FileTest.php b/tests/units/Model/FileTest.php deleted file mode 100644 index 29f6ee93..00000000 --- a/tests/units/Model/FileTest.php +++ /dev/null @@ -1,263 +0,0 @@ -<?php - -require_once __DIR__.'/../Base.php'; - -use Kanboard\Model\Task; -use Kanboard\Model\File; -use Kanboard\Model\TaskCreation; -use Kanboard\Model\Project; - -class FileTest extends Base -{ - public function setUp() - { - parent::setUp(); - - $this->container['objectStorage'] = $this - ->getMockBuilder('\Kanboard\Core\ObjectStorage\FileStorage') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('put', 'moveFile', 'remove')) - ->getMock(); - } - - public function testCreation() - { - $p = new Project($this->container); - $f = new File($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertEquals(1, $f->create(1, 'test', '/tmp/foo', 10)); - - $file = $f->getById(1); - $this->assertNotEmpty($file); - $this->assertEquals('test', $file['name']); - $this->assertEquals('/tmp/foo', $file['path']); - $this->assertEquals(0, $file['is_image']); - $this->assertEquals(1, $file['task_id']); - $this->assertEquals(time(), $file['date'], '', 2); - $this->assertEquals(0, $file['user_id']); - $this->assertEquals(10, $file['size']); - - $this->assertEquals(2, $f->create(1, 'test2.png', '/tmp/foobar', 10)); - - $file = $f->getById(2); - $this->assertNotEmpty($file); - $this->assertEquals('test2.png', $file['name']); - $this->assertEquals('/tmp/foobar', $file['path']); - $this->assertEquals(1, $file['is_image']); - } - - public function testCreationFileNameTooLong() - { - $p = new Project($this->container); - $f = new File($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertNotFalse($f->create(1, 'test', '/tmp/foo', 10)); - $this->assertNotFalse($f->create(1, str_repeat('a', 1000), '/tmp/foo', 10)); - - $files = $f->getAll(1); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - - $this->assertEquals(str_repeat('a', 255), $files[0]['name']); - $this->assertEquals('test', $files[1]['name']); - } - - public function testIsImage() - { - $f = new File($this->container); - - $this->assertTrue($f->isImage('test.png')); - $this->assertTrue($f->isImage('test.jpeg')); - $this->assertTrue($f->isImage('test.gif')); - $this->assertTrue($f->isImage('test.jpg')); - $this->assertTrue($f->isImage('test.JPG')); - - $this->assertFalse($f->isImage('test.bmp')); - $this->assertFalse($f->isImage('test')); - $this->assertFalse($f->isImage('test.pdf')); - } - - public function testGeneratePath() - { - $f = new File($this->container); - - $this->assertStringStartsWith('12'.DIRECTORY_SEPARATOR.'34'.DIRECTORY_SEPARATOR, $f->generatePath(12, 34, 'test.png')); - $this->assertNotEquals($f->generatePath(12, 34, 'test1.png'), $f->generatePath(12, 34, 'test2.png')); - } - - public function testUploadScreenshot() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $data = base64_encode('image data'); - - $f = $this - ->getMockBuilder('\Kanboard\Model\File') - ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromData')) - ->getMock(); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('put') - ->with( - $this->stringContains('1'.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR), - $this->equalTo(base64_decode($data)) - ) - ->will($this->returnValue(true)); - - $f->expects($this->once()) - ->method('generateThumbnailFromData'); - - $this->assertEquals(1, $f->uploadScreenshot(1, 1, $data)); - - $file = $f->getById(1); - $this->assertNotEmpty($file); - $this->assertStringStartsWith('Screenshot taken ', $file['name']); - $this->assertStringStartsWith('1'.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR, $file['path']); - $this->assertEquals(1, $file['is_image']); - $this->assertEquals(1, $file['task_id']); - $this->assertEquals(time(), $file['date'], '', 2); - $this->assertEquals(0, $file['user_id']); - $this->assertEquals(10, $file['size']); - } - - public function testUploadFileContent() - { - $p = new Project($this->container); - $f = new File($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $data = base64_encode('file data'); - - $this->container['objectStorage'] - ->expects($this->once()) - ->method('put') - ->with( - $this->stringContains('1'.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR), - $this->equalTo(base64_decode($data)) - ) - ->will($this->returnValue(true)); - - $this->assertEquals(1, $f->uploadContent(1, 1, 'my file.pdf', $data)); - - $file = $f->getById(1); - $this->assertNotEmpty($file); - $this->assertEquals('my file.pdf', $file['name']); - $this->assertStringStartsWith('1'.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR, $file['path']); - $this->assertEquals(0, $file['is_image']); - $this->assertEquals(1, $file['task_id']); - $this->assertEquals(time(), $file['date'], '', 2); - $this->assertEquals(0, $file['user_id']); - $this->assertEquals(9, $file['size']); - } - - public function testGetAll() - { - $p = new Project($this->container); - $f = new File($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertEquals(1, $f->create(1, 'B.pdf', '/tmp/foo', 10)); - $this->assertEquals(2, $f->create(1, 'A.png', '/tmp/foo', 10)); - $this->assertEquals(3, $f->create(1, 'D.doc', '/tmp/foo', 10)); - $this->assertEquals(4, $f->create(1, 'C.JPG', '/tmp/foo', 10)); - - $files = $f->getAll(1); - $this->assertNotEmpty($files); - $this->assertCount(4, $files); - $this->assertEquals('A.png', $files[0]['name']); - $this->assertEquals('B.pdf', $files[1]['name']); - $this->assertEquals('C.JPG', $files[2]['name']); - $this->assertEquals('D.doc', $files[3]['name']); - - $files = $f->getAllImages(1); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - $this->assertEquals('A.png', $files[0]['name']); - $this->assertEquals('C.JPG', $files[1]['name']); - - $files = $f->getAllDocuments(1); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - $this->assertEquals('B.pdf', $files[0]['name']); - $this->assertEquals('D.doc', $files[1]['name']); - } - - public function testRemove() - { - $p = new Project($this->container); - $f = new File($this->container); - $tc = new TaskCreation($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertEquals(1, $f->create(1, 'B.pdf', DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo1', 10)); - $this->assertEquals(2, $f->create(1, 'A.png', DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo2', 10)); - $this->assertEquals(3, $f->create(1, 'D.doc', DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo3', 10)); - - $this->container['objectStorage'] - ->expects($this->at(0)) - ->method('remove') - ->with( - $this->equalTo(DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo2') - ) - ->will($this->returnValue(true)); - - $this->container['objectStorage'] - ->expects($this->at(1)) - ->method('remove') - ->with( - $this->equalTo('thumbnails'.DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo2') - ) - ->will($this->returnValue(true)); - - $this->container['objectStorage'] - ->expects($this->at(2)) - ->method('remove') - ->with( - $this->equalTo(DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo1') - ) - ->will($this->returnValue(true)); - - $this->container['objectStorage'] - ->expects($this->at(3)) - ->method('remove') - ->with( - $this->equalTo(DIRECTORY_SEPARATOR.'tmp'.DIRECTORY_SEPARATOR.'foo3') - ) - ->will($this->returnValue(true)); - - $this->assertTrue($f->remove(2)); - - $files = $f->getAll(1); - $this->assertNotEmpty($files); - $this->assertCount(2, $files); - $this->assertEquals('B.pdf', $files[0]['name']); - $this->assertEquals('D.doc', $files[1]['name']); - - $this->assertTrue($f->removeAll(1)); - - $files = $f->getAll(1); - $this->assertEmpty($files); - } -} diff --git a/tests/units/Model/NotificationTest.php b/tests/units/Model/NotificationTest.php index 7f9977ce..d5ec7735 100644 --- a/tests/units/Model/NotificationTest.php +++ b/tests/units/Model/NotificationTest.php @@ -6,8 +6,7 @@ use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskCreation; use Kanboard\Model\Subtask; use Kanboard\Model\Comment; -use Kanboard\Model\User; -use Kanboard\Model\File; +use Kanboard\Model\TaskFile; use Kanboard\Model\Task; use Kanboard\Model\Project; use Kanboard\Model\Notification; @@ -23,7 +22,7 @@ class NotificationTest extends Base $tc = new TaskCreation($this->container); $s = new Subtask($this->container); $c = new Comment($this->container); - $f = new File($this->container); + $f = new TaskFile($this->container); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1))); diff --git a/tests/units/Model/ProjectActivityTest.php b/tests/units/Model/ProjectActivityTest.php index 10201aa8..27ea039d 100644 --- a/tests/units/Model/ProjectActivityTest.php +++ b/tests/units/Model/ProjectActivityTest.php @@ -7,9 +7,6 @@ use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskCreation; use Kanboard\Model\ProjectActivity; use Kanboard\Model\Project; -use Kanboard\Model\Subtask; -use Kanboard\Model\Comment; -use Kanboard\Model\File; class ProjectActivityTest extends Base { diff --git a/tests/units/Model/ProjectDailyColumnStatsTest.php b/tests/units/Model/ProjectDailyColumnStatsTest.php index 5e8ec3e8..1a0e826c 100644 --- a/tests/units/Model/ProjectDailyColumnStatsTest.php +++ b/tests/units/Model/ProjectDailyColumnStatsTest.php @@ -7,7 +7,6 @@ use Kanboard\Model\ProjectDailyColumnStats; use Kanboard\Model\Config; use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; -use Kanboard\Model\TaskStatus; class ProjectDailyColumnStatsTest extends Base { diff --git a/tests/units/Model/ProjectDailyStatsTest.php b/tests/units/Model/ProjectDailyStatsTest.php index 9efdb199..c3b20cb9 100644 --- a/tests/units/Model/ProjectDailyStatsTest.php +++ b/tests/units/Model/ProjectDailyStatsTest.php @@ -4,7 +4,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Project; use Kanboard\Model\ProjectDailyStats; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskStatus; @@ -43,6 +42,13 @@ class ProjectDailyStatsTest extends Base ) ); - $this->assertEquals($expected, $metrics); + $this->assertEquals($expected[0]['day'], $metrics[0]['day']); + $this->assertEquals($expected[1]['day'], $metrics[1]['day']); + + $this->assertEquals($expected[0]['avg_lead_time'], $metrics[0]['avg_lead_time'], '', 2); + $this->assertEquals($expected[1]['avg_lead_time'], $metrics[1]['avg_lead_time'], '', 2); + + $this->assertEquals($expected[0]['avg_cycle_time'], $metrics[0]['avg_cycle_time'], '', 2); + $this->assertEquals($expected[1]['avg_cycle_time'], $metrics[1]['avg_cycle_time'], '', 2); } } diff --git a/tests/units/Model/ProjectDuplicationTest.php b/tests/units/Model/ProjectDuplicationTest.php index db5da525..ee5b4ce4 100644 --- a/tests/units/Model/ProjectDuplicationTest.php +++ b/tests/units/Model/ProjectDuplicationTest.php @@ -6,8 +6,11 @@ use Kanboard\Model\Action; use Kanboard\Model\Project; use Kanboard\Model\Category; use Kanboard\Model\ProjectUserRole; +use Kanboard\Model\ProjectGroupRole; use Kanboard\Model\ProjectDuplication; use Kanboard\Model\User; +use Kanboard\Model\Group; +use Kanboard\Model\GroupMember; use Kanboard\Model\Swimlane; use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; @@ -16,7 +19,14 @@ use Kanboard\Core\Security\Role; class ProjectDuplicationTest extends Base { - public function testProjectName() + public function testGetSelections() + { + $projectDuplicationModel = new ProjectDuplication($this->container); + $this->assertCount(5, $projectDuplicationModel->getOptionalSelection()); + $this->assertCount(6, $projectDuplicationModel->getPossibleSelection()); + } + + public function testGetClonedProjectName() { $pd = new ProjectDuplication($this->container); @@ -29,54 +39,142 @@ class ProjectDuplicationTest extends Base $this->assertEquals(str_repeat('a', 42).' (Clone)', $pd->getClonedProjectName(str_repeat('a', 60))); } - public function testCopyProjectWithLongName() + public function testClonePublicProject() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); - $this->assertEquals(1, $p->create(array('name' => str_repeat('a', 50)))); + $this->assertEquals(1, $p->create(array('name' => 'Public'))); $this->assertEquals(2, $pd->duplicate(1)); $project = $p->getById(2); $this->assertNotEmpty($project); - $this->assertEquals(str_repeat('a', 42).' (Clone)', $project['name']); + $this->assertEquals('Public (Clone)', $project['name']); + $this->assertEquals(1, $project['is_active']); + $this->assertEquals(0, $project['is_private']); + $this->assertEquals(0, $project['is_public']); + $this->assertEquals(0, $project['owner_id']); + $this->assertEmpty($project['token']); } - public function testClonePublicProject() + public function testClonePrivateProject() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); + $pp = new ProjectUserRole($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Public'))); + $this->assertEquals(1, $p->create(array('name' => 'Private', 'is_private' => 1), 1, true)); $this->assertEquals(2, $pd->duplicate(1)); $project = $p->getById(2); $this->assertNotEmpty($project); - $this->assertEquals('Public (Clone)', $project['name']); - $this->assertEquals(0, $project['is_private']); + $this->assertEquals('Private (Clone)', $project['name']); + $this->assertEquals(1, $project['is_active']); + $this->assertEquals(1, $project['is_private']); $this->assertEquals(0, $project['is_public']); + $this->assertEquals(0, $project['owner_id']); $this->assertEmpty($project['token']); + + $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 1)); } - public function testClonePrivateProject() + public function testCloneSharedProject() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Private', 'is_private' => 1), 1, true)); + $this->assertEquals(1, $p->create(array('name' => 'Shared'))); + $this->assertTrue($p->update(array('id' => 1, 'is_public' => 1, 'token' => 'test'))); + + $project = $p->getById(1); + $this->assertEquals('test', $project['token']); + $this->assertEquals(1, $project['is_public']); + $this->assertEquals(2, $pd->duplicate(1)); $project = $p->getById(2); $this->assertNotEmpty($project); - $this->assertEquals('Private (Clone)', $project['name']); - $this->assertEquals(1, $project['is_private']); + $this->assertEquals('Shared (Clone)', $project['name']); + $this->assertEquals('', $project['token']); $this->assertEquals(0, $project['is_public']); - $this->assertEmpty($project['token']); + } - $pp = new ProjectUserRole($this->container); + public function testCloneInactiveProject() + { + $p = new Project($this->container); + $pd = new ProjectDuplication($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'Inactive'))); + $this->assertTrue($p->update(array('id' => 1, 'is_active' => 0))); + + $project = $p->getById(1); + $this->assertEquals(0, $project['is_active']); + + $this->assertEquals(2, $pd->duplicate(1)); + + $project = $p->getById(2); + $this->assertNotEmpty($project); + $this->assertEquals('Inactive (Clone)', $project['name']); + $this->assertEquals(1, $project['is_active']); + } + + public function testCloneProjectWithOwner() + { + $p = new Project($this->container); + $pd = new ProjectDuplication($this->container); + $projectUserRoleModel = new ProjectUserRole($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'Owner'))); + + $project = $p->getById(1); + $this->assertEquals(0, $project['owner_id']); + + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission'), 1)); - $this->assertEquals(array(1 => 'admin'), $pp->getAssignableUsers(1)); - $this->assertEquals(array(1 => 'admin'), $pp->getAssignableUsers(2)); + $project = $p->getById(2); + $this->assertNotEmpty($project); + $this->assertEquals('Owner (Clone)', $project['name']); + $this->assertEquals(1, $project['owner_id']); + + $this->assertEquals(Role::PROJECT_MANAGER, $projectUserRoleModel->getUserRole(2, 1)); + } + + public function testCloneProjectWithDifferentName() + { + $p = new Project($this->container); + $pd = new ProjectDuplication($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'Owner'))); + + $project = $p->getById(1); + $this->assertEquals(0, $project['owner_id']); + + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission'), 1, 'Foobar')); + + $project = $p->getById(2); + $this->assertNotEmpty($project); + $this->assertEquals('Foobar', $project['name']); + $this->assertEquals(1, $project['owner_id']); + } + + public function testCloneProjectAndForceItToBePrivate() + { + $p = new Project($this->container); + $pd = new ProjectDuplication($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'Owner'))); + + $project = $p->getById(1); + $this->assertEquals(0, $project['owner_id']); + $this->assertEquals(0, $project['is_private']); + + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission'), 1, 'Foobar', true)); + + $project = $p->getById(2); + $this->assertNotEmpty($project); + $this->assertEquals('Foobar', $project['name']); + $this->assertEquals(1, $project['owner_id']); + $this->assertEquals(1, $project['is_private']); } public function testCloneProjectWithCategories() @@ -98,16 +196,9 @@ class ProjectDuplicationTest extends Base $this->assertEquals('P1 (Clone)', $project['name']); $categories = $c->getAll(2); - $this->assertNotempty($categories); - $this->assertEquals(3, count($categories)); - - $this->assertEquals(4, $categories[0]['id']); + $this->assertCount(3, $categories); $this->assertEquals('C1', $categories[0]['name']); - - $this->assertEquals(5, $categories[1]['id']); $this->assertEquals('C2', $categories[1]['name']); - - $this->assertEquals(6, $categories[2]['id']); $this->assertEquals('C3', $categories[2]['name']); } @@ -119,28 +210,115 @@ class ProjectDuplicationTest extends Base $u = new User($this->container); $pd = new ProjectDuplication($this->container); - $this->assertEquals(2, $u->create(array('username' => 'unittest1', 'password' => 'unittest'))); - $this->assertEquals(3, $u->create(array('username' => 'unittest2', 'password' => 'unittest'))); - $this->assertEquals(4, $u->create(array('username' => 'unittest3', 'password' => 'unittest'))); + $this->assertEquals(2, $u->create(array('username' => 'user1'))); + $this->assertEquals(3, $u->create(array('username' => 'user2'))); + $this->assertEquals(4, $u->create(array('username' => 'user3'))); $this->assertEquals(1, $p->create(array('name' => 'P1'))); - $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER)); + + $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MANAGER)); $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER)); - $this->assertTrue($pp->addUser(1, 4, Role::PROJECT_MANAGER)); - $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(1, 2)); - $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(1, 3)); - $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(1, 4)); + $this->assertTrue($pp->addUser(1, 4, Role::PROJECT_VIEWER)); $this->assertEquals(2, $pd->duplicate(1)); + $this->assertCount(3, $pp->getUsers(2)); + $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 2)); + $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(2, 3)); + $this->assertEquals(Role::PROJECT_VIEWER, $pp->getUserRole(2, 4)); + } + + public function testCloneProjectWithUsersAndOverrideOwner() + { + $p = new Project($this->container); + $c = new Category($this->container); + $pp = new ProjectUserRole($this->container); + $u = new User($this->container); + $pd = new ProjectDuplication($this->container); + + $this->assertEquals(2, $u->create(array('username' => 'user1'))); + $this->assertEquals(1, $p->create(array('name' => 'P1'), 2)); + + $project = $p->getById(1); + $this->assertEquals(2, $project['owner_id']); + + $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MANAGER)); + $this->assertTrue($pp->addUser(1, 1, Role::PROJECT_MEMBER)); + + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission'), 1)); + + $this->assertCount(2, $pp->getUsers(2)); + $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 2)); + $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 1)); + $project = $p->getById(2); - $this->assertNotEmpty($project); - $this->assertEquals('P1 (Clone)', $project['name']); + $this->assertEquals(1, $project['owner_id']); + } - $this->assertEquals(3, count($pp->getUsers(2))); - $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(2, 2)); - $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(2, 3)); - $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 4)); + public function testCloneTeamProjectToPrivatProject() + { + $p = new Project($this->container); + $c = new Category($this->container); + $pp = new ProjectUserRole($this->container); + $u = new User($this->container); + $pd = new ProjectDuplication($this->container); + + $this->assertEquals(2, $u->create(array('username' => 'user1'))); + $this->assertEquals(3, $u->create(array('username' => 'user2'))); + $this->assertEquals(1, $p->create(array('name' => 'P1'), 2)); + + $project = $p->getById(1); + $this->assertEquals(2, $project['owner_id']); + $this->assertEquals(0, $project['is_private']); + + $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MANAGER)); + $this->assertTrue($pp->addUser(1, 1, Role::PROJECT_MEMBER)); + + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission'), 3, 'My private project', true)); + + $this->assertCount(1, $pp->getUsers(2)); + $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 3)); + + $project = $p->getById(2); + $this->assertEquals(3, $project['owner_id']); + $this->assertEquals(1, $project['is_private']); + } + + public function testCloneProjectWithGroups() + { + $p = new Project($this->container); + $c = new Category($this->container); + $pd = new ProjectDuplication($this->container); + $userModel = new User($this->container); + $groupModel = new Group($this->container); + $groupMemberModel = new GroupMember($this->container); + $projectGroupRoleModel = new ProjectGroupRole($this->container); + $projectUserRoleModel = new ProjectUserRole($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'P1'))); + + $this->assertEquals(1, $groupModel->create('G1')); + $this->assertEquals(2, $groupModel->create('G2')); + $this->assertEquals(3, $groupModel->create('G3')); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user2'))); + $this->assertEquals(4, $userModel->create(array('username' => 'user3'))); + + $this->assertTrue($groupMemberModel->addUser(1, 2)); + $this->assertTrue($groupMemberModel->addUser(2, 3)); + $this->assertTrue($groupMemberModel->addUser(3, 4)); + + $this->assertTrue($projectGroupRoleModel->addGroup(1, 1, Role::PROJECT_MANAGER)); + $this->assertTrue($projectGroupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($projectGroupRoleModel->addGroup(1, 3, Role::PROJECT_VIEWER)); + + $this->assertEquals(2, $pd->duplicate(1)); + + $this->assertCount(3, $projectGroupRoleModel->getGroups(2)); + $this->assertEquals(Role::PROJECT_MANAGER, $projectUserRoleModel->getUserRole(2, 2)); + $this->assertEquals(Role::PROJECT_MEMBER, $projectUserRoleModel->getUserRole(2, 3)); + $this->assertEquals(Role::PROJECT_VIEWER, $projectUserRoleModel->getUserRole(2, 4)); } public function testCloneProjectWithActionTaskAssignCurrentUser() @@ -199,7 +377,7 @@ class ProjectDuplicationTest extends Base $this->assertEquals(5, $actions[0]['params']['category_id']); } - public function testCloneProjectWithSwimlanesAndTasks() + public function testCloneProjectWithSwimlanes() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); @@ -207,31 +385,22 @@ class ProjectDuplicationTest extends Base $tc = new TaskCreation($this->container); $tf = new TaskFinder($this->container); - $this->assertEquals(1, $p->create(array('name' => 'P1'))); + $this->assertEquals(1, $p->create(array('name' => 'P1', 'default_swimlane' => 'New Default'))); // create initial swimlanes $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'S1'))); $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'S2'))); $this->assertEquals(3, $s->create(array('project_id' => 1, 'name' => 'S3'))); - $default_swimlane1 = $s->getDefault(1); - $default_swimlane1['default_swimlane'] = 'New Default'; - - $this->assertTrue($s->updateDefault($default_swimlane1)); + // create initial tasks + $this->assertEquals(1, $tc->create(array('title' => 'T0', 'project_id' => 1, 'swimlane_id' => 0))); + $this->assertEquals(2, $tc->create(array('title' => 'T1', 'project_id' => 1, 'swimlane_id' => 1))); + $this->assertEquals(3, $tc->create(array('title' => 'T2', 'project_id' => 1, 'swimlane_id' => 2))); + $this->assertEquals(4, $tc->create(array('title' => 'T3', 'project_id' => 1, 'swimlane_id' => 3))); - //create initial tasks - $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - - $this->assertNotFalse($pd->duplicate(1, array('category', 'action', 'swimlane', 'task'))); - $project = $p->getByName('P1 (Clone)'); - $this->assertNotFalse($project); - $project_id = $project['id']; - - // Check if Swimlanes have been duplicated - $swimlanes = $s->getAll($project_id); + $this->assertEquals(2, $pd->duplicate(1, array('category', 'swimlane'))); + $swimlanes = $s->getAll(2); $this->assertCount(3, $swimlanes); $this->assertEquals(4, $swimlanes[0]['id']); $this->assertEquals('S1', $swimlanes[0]['name']); @@ -239,29 +408,15 @@ class ProjectDuplicationTest extends Base $this->assertEquals('S2', $swimlanes[1]['name']); $this->assertEquals(6, $swimlanes[2]['id']); $this->assertEquals('S3', $swimlanes[2]['name']); - $new_default = $s->getDefault($project_id); - $this->assertEquals('New Default', $new_default['default_swimlane']); - - // Check if Tasks have been duplicated - $tasks = $tf->getAll($project_id); + $swimlane = $s->getDefault(2); + $this->assertEquals('New Default', $swimlane['default_swimlane']); - $this->assertCount(3, $tasks); - // $this->assertEquals(4, $tasks[0]['id']); - $this->assertEquals('T1', $tasks[0]['title']); - // $this->assertEquals(5, $tasks[1]['id']); - $this->assertEquals('T2', $tasks[1]['title']); - // $this->assertEquals(6, $tasks[2]['id']); - $this->assertEquals('T3', $tasks[2]['title']); - - $p->remove($project_id); - - $this->assertFalse($p->exists($project_id)); - $this->assertCount(0, $s->getAll($project_id)); - $this->assertCount(0, $tf->getAll($project_id)); + // Check if tasks are NOT been duplicated + $this->assertCount(0, $tf->getAll(2)); } - public function testCloneProjectWithSwimlanes() + public function testCloneProjectWithTasks() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); @@ -271,43 +426,22 @@ class ProjectDuplicationTest extends Base $this->assertEquals(1, $p->create(array('name' => 'P1'))); - // create initial swimlanes - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'S1'))); - $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'S2'))); - $this->assertEquals(3, $s->create(array('project_id' => 1, 'name' => 'S3'))); + // create initial tasks + $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $tc->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(3, $tc->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3))); - $default_swimlane1 = $s->getDefault(1); - $default_swimlane1['default_swimlane'] = 'New Default'; - - $this->assertTrue($s->updateDefault($default_swimlane1)); - - //create initial tasks - $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); - $this->assertEquals(3, $tc->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - - $this->assertNotFalse($pd->duplicate(1, array('category', 'action', 'swimlane'))); - $project = $p->getByName('P1 (Clone)'); - $this->assertNotFalse($project); - $project_id = $project['id']; - - $swimlanes = $s->getAll($project_id); - - $this->assertCount(3, $swimlanes); - $this->assertEquals(4, $swimlanes[0]['id']); - $this->assertEquals('S1', $swimlanes[0]['name']); - $this->assertEquals(5, $swimlanes[1]['id']); - $this->assertEquals('S2', $swimlanes[1]['name']); - $this->assertEquals(6, $swimlanes[2]['id']); - $this->assertEquals('S3', $swimlanes[2]['name']); - $new_default = $s->getDefault($project_id); - $this->assertEquals('New Default', $new_default['default_swimlane']); + $this->assertEquals(2, $pd->duplicate(1, array('category', 'action', 'task'))); - // Check if Tasks have NOT been duplicated - $this->assertCount(0, $tf->getAll($project_id)); + // Check if Tasks have been duplicated + $tasks = $tf->getAll(2); + $this->assertCount(3, $tasks); + $this->assertEquals('T1', $tasks[0]['title']); + $this->assertEquals('T2', $tasks[1]['title']); + $this->assertEquals('T3', $tasks[2]['title']); } - public function testCloneProjectWithTasks() + public function testCloneProjectWithSwimlanesAndTasks() { $p = new Project($this->container); $pd = new ProjectDuplication($this->container); @@ -315,40 +449,39 @@ class ProjectDuplicationTest extends Base $tc = new TaskCreation($this->container); $tf = new TaskFinder($this->container); - $this->assertEquals(1, $p->create(array('name' => 'P1'))); + $this->assertEquals(1, $p->create(array('name' => 'P1', 'default_swimlane' => 'New Default'))); // create initial swimlanes $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'S1'))); $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'S2'))); $this->assertEquals(3, $s->create(array('project_id' => 1, 'name' => 'S3'))); - $default_swimlane1 = $s->getDefault(1); - $default_swimlane1['default_swimlane'] = 'New Default'; - - $this->assertTrue($s->updateDefault($default_swimlane1)); - - //create initial tasks + // create initial tasks $this->assertEquals(1, $tc->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); $this->assertEquals(2, $tc->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); $this->assertEquals(3, $tc->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); - $this->assertNotFalse($pd->duplicate(1, array('category', 'action', 'task'))); - $project = $p->getByName('P1 (Clone)'); - $this->assertNotFalse($project); - $project_id = $project['id']; + $this->assertEquals(2, $pd->duplicate(1, array('projectPermission', 'swimlane', 'task'))); + + // Check if Swimlanes have been duplicated + $swimlanes = $s->getAll(2); + $this->assertCount(3, $swimlanes); + $this->assertEquals(4, $swimlanes[0]['id']); + $this->assertEquals('S1', $swimlanes[0]['name']); + $this->assertEquals(5, $swimlanes[1]['id']); + $this->assertEquals('S2', $swimlanes[1]['name']); + $this->assertEquals(6, $swimlanes[2]['id']); + $this->assertEquals('S3', $swimlanes[2]['name']); - // Check if Swimlanes have NOT been duplicated - $this->assertCount(0, $s->getAll($project_id)); + $swimlane = $s->getDefault(2); + $this->assertEquals('New Default', $swimlane['default_swimlane']); // Check if Tasks have been duplicated - $tasks = $tf->getAll($project_id); + $tasks = $tf->getAll(2); $this->assertCount(3, $tasks); - //$this->assertEquals(4, $tasks[0]['id']); $this->assertEquals('T1', $tasks[0]['title']); - //$this->assertEquals(5, $tasks[1]['id']); $this->assertEquals('T2', $tasks[1]['title']); - //$this->assertEquals(6, $tasks[2]['id']); $this->assertEquals('T3', $tasks[2]['title']); } } diff --git a/tests/units/Model/ProjectFileTest.php b/tests/units/Model/ProjectFileTest.php new file mode 100644 index 00000000..d9b37fbe --- /dev/null +++ b/tests/units/Model/ProjectFileTest.php @@ -0,0 +1,311 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\ProjectFile; +use Kanboard\Model\Project; + +class ProjectFileTest extends Base +{ + public function testCreation() + { + $projectModel = new Project($this->container); + $fileModel = new ProjectFile($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals('/tmp/foo', $file['path']); + $this->assertEquals(0, $file['is_image']); + $this->assertEquals(1, $file['project_id']); + $this->assertEquals(time(), $file['date'], '', 2); + $this->assertEquals(0, $file['user_id']); + $this->assertEquals(10, $file['size']); + + $this->assertEquals(2, $fileModel->create(1, 'test2.png', '/tmp/foobar', 10)); + + $file = $fileModel->getById(2); + $this->assertEquals('test2.png', $file['name']); + $this->assertEquals('/tmp/foobar', $file['path']); + $this->assertEquals(1, $file['is_image']); + } + + public function testCreationWithFileNameTooLong() + { + $projectModel = new Project($this->container); + $fileModel = new ProjectFile($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertNotFalse($fileModel->create(1, 'test', '/tmp/foo', 10)); + $this->assertNotFalse($fileModel->create(1, str_repeat('a', 1000), '/tmp/foo', 10)); + + $files = $fileModel->getAll(1); + $this->assertNotEmpty($files); + $this->assertCount(2, $files); + + $this->assertEquals(str_repeat('a', 255), $files[0]['name']); + $this->assertEquals('test', $files[1]['name']); + } + + public function testCreationWithSessionOpen() + { + $this->container['sessionStorage']->user = array('id' => 1); + + $projectModel = new Project($this->container); + $fileModel = new ProjectFile($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals(1, $file['user_id']); + } + + public function testGetAll() + { + $projectModel = new Project($this->container); + $fileModel = new ProjectFile($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->assertEquals(1, $fileModel->create(1, 'B.pdf', '/tmp/foo', 10)); + $this->assertEquals(2, $fileModel->create(1, 'A.png', '/tmp/foo', 10)); + $this->assertEquals(3, $fileModel->create(1, 'D.doc', '/tmp/foo', 10)); + $this->assertEquals(4, $fileModel->create(1, 'C.JPG', '/tmp/foo', 10)); + + $fileModeliles = $fileModel->getAll(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(4, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('B.pdf', $fileModeliles[1]['name']); + $this->assertEquals('C.JPG', $fileModeliles[2]['name']); + $this->assertEquals('D.doc', $fileModeliles[3]['name']); + + $fileModeliles = $fileModel->getAllImages(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('C.JPG', $fileModeliles[1]['name']); + + $fileModeliles = $fileModel->getAllDocuments(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('B.pdf', $fileModeliles[0]['name']); + $this->assertEquals('D.doc', $fileModeliles[1]['name']); + } + + public function testGetThumbnailPath() + { + $fileModel = new ProjectFile($this->container); + $this->assertEquals('thumbnails'.DIRECTORY_SEPARATOR.'test', $fileModel->getThumbnailPath('test')); + } + + public function testGeneratePath() + { + $fileModel = new ProjectFile($this->container); + + $this->assertStringStartsWith('projects'.DIRECTORY_SEPARATOR.'34'.DIRECTORY_SEPARATOR, $fileModel->generatePath(34, 'test.png')); + $this->assertNotEquals($fileModel->generatePath(34, 'test1.png'), $fileModel->generatePath(34, 'test2.png')); + } + + public function testUploadFiles() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\ProjectFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromFile'); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()); + + $this->container['objectStorage'] + ->expects($this->at(1)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpeEwEWG'), $this->anything()); + + $this->assertTrue($fileModel->uploadFiles(1, $files)); + + $files = $fileModel->getAll(1); + $this->assertCount(2, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('file1.png', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['project_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(123, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + + $this->assertEquals(2, $files[1]['id']); + $this->assertEquals('file2.doc', $files[1]['name']); + $this->assertEquals(0, $files[1]['is_image']); + $this->assertEquals(1, $files[1]['project_id']); + $this->assertEquals(0, $files[1]['user_id']); + $this->assertEquals(456, $files[1]['size']); + $this->assertEquals(time(), $files[1]['date'], '', 2); + } + + public function testUploadFilesWithEmptyFiles() + { + $fileModel = new ProjectFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, array())); + } + + public function testUploadFilesWithUploadError() + { + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_CANT_WRITE, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel = new ProjectFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFilesWithObjectStorageError() + { + $files = array( + 'name' => array( + 'file1.csv', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()) + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $fileModel = new ProjectFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFileContent() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\ProjectFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadContent(1, 'test.doc', base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('test.doc', $files[0]['name']); + $this->assertEquals(0, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['project_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } + + public function testUploadImageContent() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\ProjectFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromFile'); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadContent(1, 'test.png', base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('test.png', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['project_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } +} diff --git a/tests/units/Model/ProjectGroupRoleTest.php b/tests/units/Model/ProjectGroupRoleTest.php index 29a9536b..e38e812a 100644 --- a/tests/units/Model/ProjectGroupRoleTest.php +++ b/tests/units/Model/ProjectGroupRoleTest.php @@ -204,6 +204,44 @@ class ProjectGroupRoleTest extends Base $this->assertEquals('', $users[1]['name']); } + public function testGetAssignableUsersWithDisabledUsers() + { + $userModel = new User($this->container); + $projectModel = new Project($this->container); + $groupModel = new Group($this->container); + $groupMemberModel = new GroupMember($this->container); + $groupRoleModel = new ProjectGroupRole($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2'))); + + $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user 2', 'is_active' => 0))); + $this->assertEquals(4, $userModel->create(array('username' => 'user 3'))); + + $this->assertEquals(1, $groupModel->create('Group A')); + $this->assertEquals(2, $groupModel->create('Group B')); + $this->assertEquals(3, $groupModel->create('Group C')); + + $this->assertTrue($groupMemberModel->addUser(1, 4)); + $this->assertTrue($groupMemberModel->addUser(2, 3)); + $this->assertTrue($groupMemberModel->addUser(3, 2)); + + $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER)); + $this->assertTrue($groupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER)); + + $users = $groupRoleModel->getAssignableUsers(2); + $this->assertCount(0, $users); + + $users = $groupRoleModel->getAssignableUsers(1); + $this->assertCount(1, $users); + + $this->assertEquals(2, $users[0]['id']); + $this->assertEquals('user 1', $users[0]['username']); + $this->assertEquals('User #1', $users[0]['name']); + } + public function testGetProjectsByUser() { $userModel = new User($this->container); diff --git a/tests/units/Model/ProjectPermissionTest.php b/tests/units/Model/ProjectPermissionTest.php index 035a1246..10fcdcc2 100644 --- a/tests/units/Model/ProjectPermissionTest.php +++ b/tests/units/Model/ProjectPermissionTest.php @@ -192,6 +192,28 @@ class ProjectPermissionTest extends Base $this->assertFalse($projectPermission->isAssignable(2, 5)); } + public function testIsAssignableWhenUserIsDisabled() + { + $userModel = new User($this->container); + $projectModel = new Project($this->container); + $groupModel = new Group($this->container); + $groupRoleModel = new ProjectGroupRole($this->container); + $groupMemberModel = new GroupMember($this->container); + $userRoleModel = new ProjectUserRole($this->container); + $projectPermission = new ProjectPermission($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'user 1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user 2', 'is_active' => 0))); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1'))); + + $this->assertTrue($userRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_MEMBER)); + + $this->assertTrue($projectPermission->isAssignable(1, 2)); + $this->assertFalse($projectPermission->isAssignable(1, 3)); + } + public function testIsMember() { $userModel = new User($this->container); diff --git a/tests/units/Model/ProjectTest.php b/tests/units/Model/ProjectTest.php index cadb42a6..5478fa40 100644 --- a/tests/units/Model/ProjectTest.php +++ b/tests/units/Model/ProjectTest.php @@ -8,8 +8,6 @@ use Kanboard\Model\Project; use Kanboard\Model\User; use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; -use Kanboard\Model\Acl; -use Kanboard\Model\Board; use Kanboard\Model\Config; use Kanboard\Model\Category; diff --git a/tests/units/Model/ProjectUserRoleTest.php b/tests/units/Model/ProjectUserRoleTest.php index c6b4eb7c..06cd1b70 100644 --- a/tests/units/Model/ProjectUserRoleTest.php +++ b/tests/units/Model/ProjectUserRoleTest.php @@ -8,6 +8,7 @@ use Kanboard\Model\Group; use Kanboard\Model\GroupMember; use Kanboard\Model\ProjectGroupRole; use Kanboard\Model\ProjectUserRole; +use Kanboard\Model\ProjectPermission; use Kanboard\Core\Security\Role; class ProjectUserRoleTest extends Base @@ -100,6 +101,36 @@ class ProjectUserRoleTest extends Base $this->assertEquals('', $userRoleModel->getUserRole(1, 2)); } + public function testGetAssignableUsersWithDisabledUsers() + { + $projectModel = new Project($this->container); + $userModel = new User($this->container); + $userRoleModel = new ProjectUserRole($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2'))); + + $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_MEMBER)); + $this->assertTrue($userRoleModel->addUser(1, 2, Role::PROJECT_MEMBER)); + $this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_MEMBER)); + + $users = $userRoleModel->getAssignableUsers(1); + $this->assertCount(3, $users); + + $this->assertEquals('admin', $users[1]); + $this->assertEquals('User1', $users[2]); + $this->assertEquals('User2', $users[3]); + + $this->assertTrue($userModel->disable(2)); + + $users = $userRoleModel->getAssignableUsers(1); + $this->assertCount(2, $users); + + $this->assertEquals('admin', $users[1]); + $this->assertEquals('User2', $users[3]); + } + public function testGetAssignableUsersWithoutGroups() { $projectModel = new Project($this->container); @@ -219,6 +250,36 @@ class ProjectUserRoleTest extends Base $this->assertEquals('User4', $users[5]); } + public function testGetAssignableUsersWithDisabledUsersAndEverybodyAllowed() + { + $projectModel = new Project($this->container); + $projectPermission = new ProjectPermission($this->container); + $userModel = new User($this->container); + $userRoleModel = new ProjectUserRole($this->container); + + $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1'))); + $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2'))); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1', 'is_everybody_allowed' => 1))); + + $this->assertTrue($projectPermission->isEverybodyAllowed(1)); + + $users = $userRoleModel->getAssignableUsers(1); + $this->assertCount(3, $users); + + $this->assertEquals('admin', $users[1]); + $this->assertEquals('User1', $users[2]); + $this->assertEquals('User2', $users[3]); + + $this->assertTrue($userModel->disable(2)); + + $users = $userRoleModel->getAssignableUsers(1); + $this->assertCount(2, $users); + + $this->assertEquals('admin', $users[1]); + $this->assertEquals('User2', $users[3]); + } + public function testGetProjectsByUser() { $userModel = new User($this->container); diff --git a/tests/units/Model/SubtaskTest.php b/tests/units/Model/SubtaskTest.php index 9be2dff4..eb9747d4 100644 --- a/tests/units/Model/SubtaskTest.php +++ b/tests/units/Model/SubtaskTest.php @@ -2,12 +2,9 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\Subtask; use Kanboard\Model\Project; -use Kanboard\Model\Category; -use Kanboard\Model\User; use Kanboard\Core\User\UserSession; class SubtaskTest extends Base @@ -159,7 +156,7 @@ class SubtaskTest extends Base $this->assertEquals(0, $subtask['user_id']); $this->assertEquals(1, $subtask['task_id']); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_INPROGRESS, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -167,7 +164,7 @@ class SubtaskTest extends Base $this->assertEquals(0, $subtask['user_id']); $this->assertEquals(1, $subtask['task_id']); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_DONE, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -175,7 +172,7 @@ class SubtaskTest extends Base $this->assertEquals(0, $subtask['user_id']); $this->assertEquals(1, $subtask['task_id']); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_TODO, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -205,7 +202,7 @@ class SubtaskTest extends Base // Set the current logged user $this->container['sessionStorage']->user = array('id' => 1); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_INPROGRESS, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -213,7 +210,7 @@ class SubtaskTest extends Base $this->assertEquals(1, $subtask['user_id']); $this->assertEquals(1, $subtask['task_id']); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_DONE, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -221,7 +218,7 @@ class SubtaskTest extends Base $this->assertEquals(1, $subtask['user_id']); $this->assertEquals(1, $subtask['task_id']); - $this->assertTrue($s->toggleStatus(1)); + $this->assertEquals(Subtask::STATUS_TODO, $s->toggleStatus(1)); $subtask = $s->getById(1); $this->assertNotEmpty($subtask); @@ -252,117 +249,6 @@ class SubtaskTest extends Base } } - public function testMoveUp() - { - $tc = new TaskCreation($this->container); - $s = new Subtask($this->container); - $p = new Project($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1))); - $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 1))); - - // Check positions - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['position']); - - $subtask = $s->getById(2); - $this->assertNotEmpty($subtask); - $this->assertEquals(2, $subtask['position']); - - $subtask = $s->getById(3); - $this->assertNotEmpty($subtask); - $this->assertEquals(3, $subtask['position']); - - // Move up - $this->assertTrue($s->moveUp(1, 2)); - - // Check positions - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(2, $subtask['position']); - - $subtask = $s->getById(2); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['position']); - - $subtask = $s->getById(3); - $this->assertNotEmpty($subtask); - $this->assertEquals(3, $subtask['position']); - - // We can't move up #2 - $this->assertFalse($s->moveUp(1, 2)); - - // Test remove - $this->assertTrue($s->remove(1)); - $this->assertTrue($s->moveUp(1, 3)); - - // Check positions - $subtask = $s->getById(1); - $this->assertEmpty($subtask); - - $subtask = $s->getById(2); - $this->assertNotEmpty($subtask); - $this->assertEquals(2, $subtask['position']); - - $subtask = $s->getById(3); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['position']); - } - - public function testMoveDown() - { - $tc = new TaskCreation($this->container); - $s = new Subtask($this->container); - $p = new Project($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1))); - $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 1))); - - // Move down #1 - $this->assertTrue($s->moveDown(1, 1)); - - // Check positions - $subtask = $s->getById(1); - $this->assertNotEmpty($subtask); - $this->assertEquals(2, $subtask['position']); - - $subtask = $s->getById(2); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['position']); - - $subtask = $s->getById(3); - $this->assertNotEmpty($subtask); - $this->assertEquals(3, $subtask['position']); - - // We can't move down #3 - $this->assertFalse($s->moveDown(1, 3)); - - // Test remove - $this->assertTrue($s->remove(1)); - $this->assertTrue($s->moveDown(1, 2)); - - // Check positions - $subtask = $s->getById(1); - $this->assertEmpty($subtask); - - $subtask = $s->getById(2); - $this->assertNotEmpty($subtask); - $this->assertEquals(2, $subtask['position']); - - $subtask = $s->getById(3); - $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['position']); - } - public function testDuplicate() { $tc = new TaskCreation($this->container); @@ -409,4 +295,69 @@ class SubtaskTest extends Base $this->assertEquals(1, $subtasks[0]['position']); $this->assertEquals(2, $subtasks[1]['position']); } + + public function testChangePosition() + { + $taskCreationModel = new TaskCreation($this->container); + $subtaskModel = new Subtask($this->container); + $projectModel = new Project($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)); + } } diff --git a/tests/units/Model/SubtaskTimeTrackingTest.php b/tests/units/Model/SubtaskTimeTrackingTest.php index 40461eea..9fa8d5b0 100644 --- a/tests/units/Model/SubtaskTimeTrackingTest.php +++ b/tests/units/Model/SubtaskTimeTrackingTest.php @@ -7,8 +7,6 @@ use Kanboard\Model\TaskCreation; use Kanboard\Model\Subtask; use Kanboard\Model\SubtaskTimeTracking; use Kanboard\Model\Project; -use Kanboard\Model\Category; -use Kanboard\Model\User; class SubtaskTimeTrackingTest extends Base { diff --git a/tests/units/Model/SwimlaneTest.php b/tests/units/Model/SwimlaneTest.php index 3d048abd..71c13e40 100644 --- a/tests/units/Model/SwimlaneTest.php +++ b/tests/units/Model/SwimlaneTest.php @@ -3,7 +3,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Project; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\Swimlane; @@ -86,7 +85,23 @@ class SwimlaneTest extends Base $this->assertEquals(0, $default['show_default_swimlane']); } - public function testDisable() + public function testDisableEnableDefaultSwimlane() + { + $projectModel = new Project($this->container); + $swimlaneModel = new Swimlane($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $this->assertTrue($swimlaneModel->disableDefault(1)); + $default = $swimlaneModel->getDefault(1); + $this->assertEquals(0, $default['show_default_swimlane']); + + $this->assertTrue($swimlaneModel->enableDefault(1)); + $default = $swimlaneModel->getDefault(1); + $this->assertEquals(1, $default['show_default_swimlane']); + } + + public function testDisableEnable() { $p = new Project($this->container); $s = new Swimlane($this->container); @@ -210,172 +225,6 @@ class SwimlaneTest extends Base $this->assertEquals(1, $swimlane['position']); } - public function testMoveUp() - { - $p = new Project($this->container); - $s = new Swimlane($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'Swimlane #1'))); - $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'Swimlane #2'))); - $this->assertEquals(3, $s->create(array('project_id' => 1, 'name' => 'Swimlane #3'))); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(3, $swimlane['position']); - - // Move the swimlane 3 up - $this->assertTrue($s->moveUp(1, 3)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(3, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - // First swimlane can be moved up - $this->assertFalse($s->moveUp(1, 1)); - - // Move with a disabled swimlane - $this->assertTrue($s->disable(1, 1)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(0, $swimlane['is_active']); - $this->assertEquals(0, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - // Move the 2nd swimlane up - $this->assertTrue($s->moveUp(1, 2)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(0, $swimlane['is_active']); - $this->assertEquals(0, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - } - - public function testMoveDown() - { - $p = new Project($this->container); - $s = new Swimlane($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'Swimlane #1'))); - $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'Swimlane #2'))); - $this->assertEquals(3, $s->create(array('project_id' => 1, 'name' => 'Swimlane #3'))); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(3, $swimlane['position']); - - // Move the swimlane 1 down - $this->assertTrue($s->moveDown(1, 1)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(3, $swimlane['position']); - - // Last swimlane can be moved down - $this->assertFalse($s->moveDown(1, 3)); - - // Move with a disabled swimlane - $this->assertTrue($s->disable(1, 3)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(0, $swimlane['is_active']); - $this->assertEquals(0, $swimlane['position']); - - // Move the 2st swimlane down - $this->assertTrue($s->moveDown(1, 2)); - - $swimlane = $s->getById(1); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(1, $swimlane['position']); - - $swimlane = $s->getById(2); - $this->assertNotEmpty($swimlane); - $this->assertEquals(1, $swimlane['is_active']); - $this->assertEquals(2, $swimlane['position']); - - $swimlane = $s->getById(3); - $this->assertNotEmpty($swimlane); - $this->assertEquals(0, $swimlane['is_active']); - $this->assertEquals(0, $swimlane['position']); - } - public function testDuplicateSwimlane() { $p = new Project($this->container); @@ -406,4 +255,93 @@ class SwimlaneTest extends Base $new_default = $s->getDefault(2); $this->assertEquals('New Default', $new_default['default_swimlane']); } + + public function testChangePosition() + { + $projectModel = new Project($this->container); + $swimlaneModel = new Swimlane($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1'))); + $this->assertEquals(2, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #2'))); + $this->assertEquals(3, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #3'))); + $this->assertEquals(4, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #4'))); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(1, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(2, $swimlanes[1]['id']); + $this->assertEquals(3, $swimlanes[2]['position']); + $this->assertEquals(3, $swimlanes[2]['id']); + + $this->assertTrue($swimlaneModel->changePosition(1, 3, 2)); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(1, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(3, $swimlanes[1]['id']); + $this->assertEquals(3, $swimlanes[2]['position']); + $this->assertEquals(2, $swimlanes[2]['id']); + + $this->assertTrue($swimlaneModel->changePosition(1, 2, 1)); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(2, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(1, $swimlanes[1]['id']); + $this->assertEquals(3, $swimlanes[2]['position']); + $this->assertEquals(3, $swimlanes[2]['id']); + + $this->assertTrue($swimlaneModel->changePosition(1, 2, 2)); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(1, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(2, $swimlanes[1]['id']); + $this->assertEquals(3, $swimlanes[2]['position']); + $this->assertEquals(3, $swimlanes[2]['id']); + + $this->assertTrue($swimlaneModel->changePosition(1, 4, 1)); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(4, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(1, $swimlanes[1]['id']); + $this->assertEquals(3, $swimlanes[2]['position']); + $this->assertEquals(2, $swimlanes[2]['id']); + + $this->assertFalse($swimlaneModel->changePosition(1, 2, 0)); + $this->assertFalse($swimlaneModel->changePosition(1, 2, 5)); + } + + public function testChangePositionWithInactiveSwimlane() + { + $projectModel = new Project($this->container); + $swimlaneModel = new Swimlane($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test1'))); + $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1'))); + $this->assertEquals(2, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #2', 'is_active' => 0))); + $this->assertEquals(3, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #3', 'is_active' => 0))); + $this->assertEquals(4, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #4'))); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(1, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(4, $swimlanes[1]['id']); + + $this->assertTrue($swimlaneModel->changePosition(1, 4, 1)); + + $swimlanes = $swimlaneModel->getAllByStatus(1); + $this->assertEquals(1, $swimlanes[0]['position']); + $this->assertEquals(4, $swimlanes[0]['id']); + $this->assertEquals(2, $swimlanes[1]['position']); + $this->assertEquals(1, $swimlanes[1]['id']); + } } diff --git a/tests/units/Model/TaskCreationTest.php b/tests/units/Model/TaskCreationTest.php index 19b3b0d1..04d23930 100644 --- a/tests/units/Model/TaskCreationTest.php +++ b/tests/units/Model/TaskCreationTest.php @@ -6,7 +6,6 @@ use Kanboard\Model\Config; use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; -use Kanboard\Model\TaskStatus; use Kanboard\Model\Project; class TaskCreationTest extends Base @@ -295,7 +294,7 @@ class TaskCreationTest extends Base $task = $tf->getById(2); $this->assertNotEmpty($task); $this->assertEquals(2, $task['id']); - $this->assertEquals($timestamp, $task['date_due']); + $this->assertEquals(date('Y-m-d 00:00', $timestamp), date('Y-m-d 00:00', $task['date_due'])); $task = $tf->getById(3); $this->assertEquals(3, $task['id']); @@ -422,6 +421,6 @@ class TaskCreationTest extends Base $task = $tf->getById(1); $this->assertNotEmpty($task); - $this->assertEquals('2050-01-10 12:30', date('Y-m-d H:i', $task['date_due'])); + $this->assertEquals('2050-01-10 00:00', date('Y-m-d H:i', $task['date_due'])); } } diff --git a/tests/units/Model/TaskDuplicationTest.php b/tests/units/Model/TaskDuplicationTest.php index 798b3835..c4b36e45 100644 --- a/tests/units/Model/TaskDuplicationTest.php +++ b/tests/units/Model/TaskDuplicationTest.php @@ -2,11 +2,11 @@ require_once __DIR__.'/../Base.php'; +use Kanboard\Core\DateParser; use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskDuplication; use Kanboard\Model\TaskFinder; -use Kanboard\Model\TaskStatus; use Kanboard\Model\Project; use Kanboard\Model\ProjectUserRole; use Kanboard\Model\Category; @@ -57,8 +57,8 @@ class TaskDuplicationTest extends Base // Some categories $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 1))); - $this->assertTrue($c->exists(1, 1)); - $this->assertTrue($c->exists(2, 1)); + $this->assertTrue($c->exists(1)); + $this->assertTrue($c->exists(2)); $this->assertEquals( 1, @@ -110,7 +110,7 @@ class TaskDuplicationTest extends Base $this->assertEquals(2, $p->create(array('name' => 'test2'))); $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); - $this->assertTrue($c->exists(1, 1)); + $this->assertTrue($c->exists(1)); // We create a task $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1, 'category_id' => 1))); @@ -151,8 +151,8 @@ class TaskDuplicationTest extends Base $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 2))); - $this->assertTrue($c->exists(1, 1)); - $this->assertTrue($c->exists(2, 2)); + $this->assertTrue($c->exists(1)); + $this->assertTrue($c->exists(2)); // We create a task $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1))); @@ -187,9 +187,9 @@ class TaskDuplicationTest extends Base $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 2))); $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 2))); - $this->assertTrue($c->exists(1, 1)); - $this->assertTrue($c->exists(2, 2)); - $this->assertTrue($c->exists(3, 2)); + $this->assertTrue($c->exists(1)); + $this->assertTrue($c->exists(2)); + $this->assertTrue($c->exists(3)); // We create a task $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1))); @@ -468,8 +468,8 @@ class TaskDuplicationTest extends Base $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 2))); - $this->assertTrue($c->exists(1, 1)); - $this->assertTrue($c->exists(2, 2)); + $this->assertTrue($c->exists(1)); + $this->assertTrue($c->exists(2)); // We create a task $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1))); @@ -665,6 +665,7 @@ class TaskDuplicationTest extends Base $tf = new TaskFinder($this->container); $p = new Project($this->container); $c = new Category($this->container); + $dp = new DateParser($this->container); $this->assertEquals(1, $p->create(array('name' => 'test1'))); @@ -685,7 +686,7 @@ class TaskDuplicationTest extends Base $this->assertNotEmpty($task); $this->assertEquals(Task::RECURRING_STATUS_PROCESSED, $task['recurrence_status']); $this->assertEquals(2, $task['recurrence_child']); - $this->assertEquals(1436561776, $task['date_due'], '', 2); + $this->assertEquals(1436486400, $task['date_due'], '', 2); $task = $tf->getById(2); $this->assertNotEmpty($task); @@ -695,6 +696,6 @@ class TaskDuplicationTest extends Base $this->assertEquals(Task::RECURRING_BASEDATE_TRIGGERDATE, $task['recurrence_basedate']); $this->assertEquals(1, $task['recurrence_parent']); $this->assertEquals(2, $task['recurrence_factor']); - $this->assertEquals(strtotime('+2 days'), $task['date_due'], '', 2); + $this->assertEquals($dp->removeTimeFromTimestamp(strtotime('+2 days')), $task['date_due'], '', 2); } } diff --git a/tests/units/Model/TaskExternalLinkTest.php b/tests/units/Model/TaskExternalLinkTest.php new file mode 100644 index 00000000..28ccab83 --- /dev/null +++ b/tests/units/Model/TaskExternalLinkTest.php @@ -0,0 +1,123 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\TaskCreation; +use Kanboard\Model\Project; +use Kanboard\Model\TaskExternalLink; +use Kanboard\Core\ExternalLink\ExternalLinkManager; +use Kanboard\ExternalLink\WebLinkProvider; + +class TaskExternalLinkTest extends Base +{ + public function testCreate() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskExternalLinkModel = new TaskExternalLink($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $taskExternalLinkModel->create(array('task_id' => 1, 'id' => '', 'url' => 'http://kanboard.net/', 'title' => 'My website', 'link_type' => 'weblink', 'dependency' => 'related'))); + + $link = $taskExternalLinkModel->getById(1); + $this->assertNotEmpty($link); + $this->assertEquals('My website', $link['title']); + $this->assertEquals('http://kanboard.net/', $link['url']); + $this->assertEquals('related', $link['dependency']); + $this->assertEquals('weblink', $link['link_type']); + $this->assertEquals(0, $link['creator_id']); + $this->assertEquals(time(), $link['date_modification'], '', 2); + $this->assertEquals(time(), $link['date_creation'], '', 2); + } + + public function testCreateWithUserSession() + { + $this->container['sessionStorage']->user = array('id' => 1); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskExternalLinkModel = new TaskExternalLink($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $taskExternalLinkModel->create(array('task_id' => 1, 'id' => '', 'url' => 'http://kanboard.net/', 'title' => 'My website', 'link_type' => 'weblink', 'dependency' => 'related'))); + + $link = $taskExternalLinkModel->getById(1); + $this->assertNotEmpty($link); + $this->assertEquals('My website', $link['title']); + $this->assertEquals('http://kanboard.net/', $link['url']); + $this->assertEquals('related', $link['dependency']); + $this->assertEquals('weblink', $link['link_type']); + $this->assertEquals(1, $link['creator_id']); + $this->assertEquals(time(), $link['date_modification'], '', 2); + $this->assertEquals(time(), $link['date_creation'], '', 2); + } + + public function testModification() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskExternalLinkModel = new TaskExternalLink($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $taskExternalLinkModel->create(array('task_id' => 1, 'id' => '', 'url' => 'http://kanboard.net/', 'title' => 'My website', 'link_type' => 'weblink', 'dependency' => 'related'))); + + sleep(1); + + $this->assertTrue($taskExternalLinkModel->update(array('id' => 1, 'url' => 'https://kanboard.net/'))); + + $link = $taskExternalLinkModel->getById(1); + $this->assertNotEmpty($link); + $this->assertEquals('https://kanboard.net/', $link['url']); + $this->assertEquals(time(), $link['date_modification'], '', 2); + } + + public function testRemove() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskExternalLinkModel = new TaskExternalLink($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $taskExternalLinkModel->create(array('task_id' => 1, 'id' => '', 'url' => 'http://kanboard.net/', 'title' => 'My website', 'link_type' => 'weblink', 'dependency' => 'related'))); + + $this->assertTrue($taskExternalLinkModel->remove(1)); + $this->assertFalse($taskExternalLinkModel->remove(1)); + + $this->assertEmpty($taskExternalLinkModel->getById(1)); + } + + public function testGetAll() + { + $this->container['sessionStorage']->user = array('id' => 1); + $this->container['externalLinkManager'] = new ExternalLinkManager($this->container); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $taskExternalLinkModel = new TaskExternalLink($this->container); + $webLinkProvider = new WebLinkProvider($this->container); + + $this->container['externalLinkManager']->register($webLinkProvider); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $taskExternalLinkModel->create(array('task_id' => 1, 'url' => 'https://miniflux.net/', 'title' => 'MX', 'link_type' => 'weblink', 'dependency' => 'related'))); + $this->assertEquals(2, $taskExternalLinkModel->create(array('task_id' => 1, 'url' => 'http://kanboard.net/', 'title' => 'KB', 'link_type' => 'weblink', 'dependency' => 'related'))); + + $links = $taskExternalLinkModel->getAll(1); + $this->assertCount(2, $links); + $this->assertEquals('KB', $links[0]['title']); + $this->assertEquals('MX', $links[1]['title']); + $this->assertEquals('Web Link', $links[0]['type']); + $this->assertEquals('Web Link', $links[1]['type']); + $this->assertEquals('Related', $links[0]['dependency_label']); + $this->assertEquals('Related', $links[1]['dependency_label']); + $this->assertEquals('admin', $links[0]['creator_username']); + $this->assertEquals('admin', $links[1]['creator_username']); + $this->assertEquals('', $links[0]['creator_name']); + $this->assertEquals('', $links[1]['creator_name']); + } +} diff --git a/tests/units/Model/TaskFileTest.php b/tests/units/Model/TaskFileTest.php new file mode 100644 index 00000000..b900e8f3 --- /dev/null +++ b/tests/units/Model/TaskFileTest.php @@ -0,0 +1,445 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\TaskFile; +use Kanboard\Model\TaskCreation; +use Kanboard\Model\Project; + +class TaskFileTest extends Base +{ + public function testCreation() + { + $projectModel = new Project($this->container); + $fileModel = new TaskFile($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals('/tmp/foo', $file['path']); + $this->assertEquals(0, $file['is_image']); + $this->assertEquals(1, $file['task_id']); + $this->assertEquals(time(), $file['date'], '', 2); + $this->assertEquals(0, $file['user_id']); + $this->assertEquals(10, $file['size']); + + $this->assertEquals(2, $fileModel->create(1, 'test2.png', '/tmp/foobar', 10)); + + $file = $fileModel->getById(2); + $this->assertEquals('test2.png', $file['name']); + $this->assertEquals('/tmp/foobar', $file['path']); + $this->assertEquals(1, $file['is_image']); + } + + public function testCreationWithFileNameTooLong() + { + $projectModel = new Project($this->container); + $fileModel = new TaskFile($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertNotFalse($fileModel->create(1, 'test', '/tmp/foo', 10)); + $this->assertNotFalse($fileModel->create(1, str_repeat('a', 1000), '/tmp/foo', 10)); + + $files = $fileModel->getAll(1); + $this->assertNotEmpty($files); + $this->assertCount(2, $files); + + $this->assertEquals(str_repeat('a', 255), $files[0]['name']); + $this->assertEquals('test', $files[1]['name']); + } + + public function testCreationWithSessionOpen() + { + $this->container['sessionStorage']->user = array('id' => 1); + + $projectModel = new Project($this->container); + $fileModel = new TaskFile($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', '/tmp/foo', 10)); + + $file = $fileModel->getById(1); + $this->assertEquals('test', $file['name']); + $this->assertEquals(1, $file['user_id']); + } + + public function testGetAll() + { + $projectModel = new Project($this->container); + $fileModel = new TaskFile($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->assertEquals(1, $fileModel->create(1, 'B.pdf', '/tmp/foo', 10)); + $this->assertEquals(2, $fileModel->create(1, 'A.png', '/tmp/foo', 10)); + $this->assertEquals(3, $fileModel->create(1, 'D.doc', '/tmp/foo', 10)); + $this->assertEquals(4, $fileModel->create(1, 'C.JPG', '/tmp/foo', 10)); + + $fileModeliles = $fileModel->getAll(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(4, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('B.pdf', $fileModeliles[1]['name']); + $this->assertEquals('C.JPG', $fileModeliles[2]['name']); + $this->assertEquals('D.doc', $fileModeliles[3]['name']); + + $fileModeliles = $fileModel->getAllImages(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('A.png', $fileModeliles[0]['name']); + $this->assertEquals('C.JPG', $fileModeliles[1]['name']); + + $fileModeliles = $fileModel->getAllDocuments(1); + $this->assertNotEmpty($fileModeliles); + $this->assertCount(2, $fileModeliles); + $this->assertEquals('B.pdf', $fileModeliles[0]['name']); + $this->assertEquals('D.doc', $fileModeliles[1]['name']); + } + + public function testIsImage() + { + $fileModel = new TaskFile($this->container); + + $this->assertTrue($fileModel->isImage('test.png')); + $this->assertTrue($fileModel->isImage('test.jpeg')); + $this->assertTrue($fileModel->isImage('test.gif')); + $this->assertTrue($fileModel->isImage('test.jpg')); + $this->assertTrue($fileModel->isImage('test.JPG')); + + $this->assertFalse($fileModel->isImage('test.bmp')); + $this->assertFalse($fileModel->isImage('test')); + $this->assertFalse($fileModel->isImage('test.pdf')); + } + + public function testGetThumbnailPath() + { + $fileModel = new TaskFile($this->container); + $this->assertEquals('thumbnails'.DIRECTORY_SEPARATOR.'test', $fileModel->getThumbnailPath('test')); + } + + public function testGeneratePath() + { + $fileModel = new TaskFile($this->container); + + $this->assertStringStartsWith('tasks'.DIRECTORY_SEPARATOR.'34'.DIRECTORY_SEPARATOR, $fileModel->generatePath(34, 'test.png')); + $this->assertNotEquals($fileModel->generatePath(34, 'test1.png'), $fileModel->generatePath(34, 'test2.png')); + } + + public function testUploadFiles() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromFile'); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()); + + $this->container['objectStorage'] + ->expects($this->at(1)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpeEwEWG'), $this->anything()); + + $this->assertTrue($fileModel->uploadFiles(1, $files)); + + $files = $fileModel->getAll(1); + $this->assertCount(2, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('file1.png', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(123, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + + $this->assertEquals(2, $files[1]['id']); + $this->assertEquals('file2.doc', $files[1]['name']); + $this->assertEquals(0, $files[1]['is_image']); + $this->assertEquals(1, $files[1]['task_id']); + $this->assertEquals(0, $files[1]['user_id']); + $this->assertEquals(456, $files[1]['size']); + $this->assertEquals(time(), $files[1]['date'], '', 2); + } + + public function testUploadFilesWithEmptyFiles() + { + $fileModel = new TaskFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, array())); + } + + public function testUploadFilesWithUploadError() + { + $files = array( + 'name' => array( + 'file1.png', + 'file2.doc', + ), + 'tmp_name' => array( + '', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_CANT_WRITE, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $fileModel = new TaskFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFilesWithObjectStorageError() + { + $files = array( + 'name' => array( + 'file1.csv', + 'file2.doc', + ), + 'tmp_name' => array( + '/tmp/phpYzdqkD', + '/tmp/phpeEwEWG', + ), + 'error' => array( + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ), + 'size' => array( + 123, + 456, + ), + ); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('moveUploadedFile') + ->with($this->equalTo('/tmp/phpYzdqkD'), $this->anything()) + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $fileModel = new TaskFile($this->container); + $this->assertFalse($fileModel->uploadFiles(1, $files)); + } + + public function testUploadFileContent() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadContent(1, 'test.doc', base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertEquals('test.doc', $files[0]['name']); + $this->assertEquals(0, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } + + public function testUploadFileContentWithObjectStorageError() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)) + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $this->assertFalse($fileModel->uploadContent(1, 'test.doc', base64_encode($data))); + } + + public function testUploadScreenshot() + { + $fileModel = $this + ->getMockBuilder('\Kanboard\Model\TaskFile') + ->setConstructorArgs(array($this->container)) + ->setMethods(array('generateThumbnailFromFile')) + ->getMock(); + + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $data = 'test'; + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $fileModel + ->expects($this->once()) + ->method('generateThumbnailFromFile'); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('put') + ->with($this->anything(), $this->equalTo($data)); + + $this->assertEquals(1, $fileModel->uploadScreenshot(1, base64_encode($data))); + + $files = $fileModel->getAll(1); + $this->assertCount(1, $files); + + $this->assertEquals(1, $files[0]['id']); + $this->assertStringStartsWith('Screenshot taken ', $files[0]['name']); + $this->assertEquals(1, $files[0]['is_image']); + $this->assertEquals(1, $files[0]['task_id']); + $this->assertEquals(0, $files[0]['user_id']); + $this->assertEquals(4, $files[0]['size']); + $this->assertEquals(time(), $files[0]['date'], '', 2); + } + + public function testRemove() + { + $fileModel = new TaskFile($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('remove') + ->with('tmp/foo'); + + $this->assertTrue($fileModel->remove(1)); + } + + public function testRemoveWithObjectStorageError() + { + $fileModel = new TaskFile($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->once()) + ->method('remove') + ->with('tmp/foo') + ->will($this->throwException(new \Kanboard\Core\ObjectStorage\ObjectStorageException('test'))); + + $this->assertFalse($fileModel->remove(1)); + } + + public function testRemoveImage() + { + $fileModel = new TaskFile($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'image.gif', 'tmp/image.gif', 10)); + + $this->container['objectStorage'] + ->expects($this->at(0)) + ->method('remove') + ->with('tmp/image.gif'); + + $this->container['objectStorage'] + ->expects($this->at(1)) + ->method('remove') + ->with('thumbnails'.DIRECTORY_SEPARATOR.'tmp/image.gif'); + + $this->assertTrue($fileModel->remove(1)); + } + + public function testRemoveAll() + { + $fileModel = new TaskFile($this->container); + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + $this->assertEquals(1, $fileModel->create(1, 'test', 'tmp/foo', 10)); + $this->assertEquals(2, $fileModel->create(1, 'test', 'tmp/foo', 10)); + + $this->container['objectStorage'] + ->expects($this->exactly(2)) + ->method('remove') + ->with('tmp/foo'); + + $this->assertTrue($fileModel->removeAll(1)); + } +} diff --git a/tests/units/Model/TaskFilterTest.php b/tests/units/Model/TaskFilterTest.php index daa193b2..9e291c31 100644 --- a/tests/units/Model/TaskFilterTest.php +++ b/tests/units/Model/TaskFilterTest.php @@ -10,7 +10,6 @@ use Kanboard\Model\TaskLink; use Kanboard\Core\DateParser; use Kanboard\Model\Category; use Kanboard\Model\Subtask; -use Kanboard\Model\Config; use Kanboard\Model\Swimlane; class TaskFilterTest extends Base diff --git a/tests/units/Model/TaskFinderTest.php b/tests/units/Model/TaskFinderTest.php index b21a4ea3..0ed211ed 100644 --- a/tests/units/Model/TaskFinderTest.php +++ b/tests/units/Model/TaskFinderTest.php @@ -2,12 +2,9 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\Project; -use Kanboard\Model\Category; -use Kanboard\Model\User; class TaskFinderTest extends Base { diff --git a/tests/units/Model/TaskLinkTest.php b/tests/units/Model/TaskLinkTest.php index 192a4298..8dd71830 100644 --- a/tests/units/Model/TaskLinkTest.php +++ b/tests/units/Model/TaskLinkTest.php @@ -2,7 +2,6 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Link; use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskLink; use Kanboard\Model\TaskCreation; diff --git a/tests/units/Model/TaskModificationTest.php b/tests/units/Model/TaskModificationTest.php index 119201f0..315125d5 100644 --- a/tests/units/Model/TaskModificationTest.php +++ b/tests/units/Model/TaskModificationTest.php @@ -6,7 +6,6 @@ use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskModification; use Kanboard\Model\TaskFinder; -use Kanboard\Model\TaskStatus; use Kanboard\Model\Project; class TaskModificationTest extends Base diff --git a/tests/units/Model/TaskPermissionTest.php b/tests/units/Model/TaskPermissionTest.php index 0b093bbb..82cd581e 100644 --- a/tests/units/Model/TaskPermissionTest.php +++ b/tests/units/Model/TaskPermissionTest.php @@ -2,12 +2,10 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskPermission; use Kanboard\Model\Project; -use Kanboard\Model\Category; use Kanboard\Model\User; use Kanboard\Core\User\UserSession; diff --git a/tests/units/Model/TaskPositionTest.php b/tests/units/Model/TaskPositionTest.php index 5f045768..28145a66 100644 --- a/tests/units/Model/TaskPositionTest.php +++ b/tests/units/Model/TaskPositionTest.php @@ -3,7 +3,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Task; -use Kanboard\Model\Board; +use Kanboard\Model\Column; use Kanboard\Model\TaskStatus; use Kanboard\Model\TaskPosition; use Kanboard\Model\TaskCreation; @@ -21,23 +21,23 @@ class TaskPositionTest extends Base $tc = new TaskCreation($this->container); $tf = new TaskFinder($this->container); $p = new Project($this->container); - $b = new Board($this->container); + $columnModel = new Column($this->container); $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(0, $t->getProgress($tf->getById(1), $b->getColumnsList(1))); + $this->assertEquals(0, $t->getProgress($tf->getById(1), $columnModel->getList(1))); $this->assertTrue($tp->movePosition(1, 1, 2, 1)); - $this->assertEquals(25, $t->getProgress($tf->getById(1), $b->getColumnsList(1))); + $this->assertEquals(25, $t->getProgress($tf->getById(1), $columnModel->getList(1))); $this->assertTrue($tp->movePosition(1, 1, 3, 1)); - $this->assertEquals(50, $t->getProgress($tf->getById(1), $b->getColumnsList(1))); + $this->assertEquals(50, $t->getProgress($tf->getById(1), $columnModel->getList(1))); $this->assertTrue($tp->movePosition(1, 1, 4, 1)); - $this->assertEquals(75, $t->getProgress($tf->getById(1), $b->getColumnsList(1))); + $this->assertEquals(75, $t->getProgress($tf->getById(1), $columnModel->getList(1))); $this->assertTrue($ts->close(1)); - $this->assertEquals(100, $t->getProgress($tf->getById(1), $b->getColumnsList(1))); + $this->assertEquals(100, $t->getProgress($tf->getById(1), $columnModel->getList(1))); } public function testMoveTaskToWrongPosition() diff --git a/tests/units/Model/TransitionTest.php b/tests/units/Model/TransitionTest.php new file mode 100644 index 00000000..0c262e78 --- /dev/null +++ b/tests/units/Model/TransitionTest.php @@ -0,0 +1,141 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\TaskCreation; +use Kanboard\Model\Transition; +use Kanboard\Model\Project; + +class TransitionTest extends Base +{ + public function testSave() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $transitionModel = new Transition($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $task_event = array( + 'project_id' => 1, + 'task_id' => 1, + 'src_column_id' => 1, + 'dst_column_id' => 2, + 'date_moved' => time() - 3600 + ); + + $this->assertTrue($transitionModel->save(1, $task_event)); + + $transitions = $transitionModel->getAllByTask(1); + $this->assertCount(1, $transitions); + $this->assertEquals('Backlog', $transitions[0]['src_column']); + $this->assertEquals('Ready', $transitions[0]['dst_column']); + $this->assertEquals('', $transitions[0]['name']); + $this->assertEquals('admin', $transitions[0]['username']); + $this->assertEquals(1, $transitions[0]['user_id']); + $this->assertEquals(time(), $transitions[0]['date'], '', 3); + $this->assertEquals(3600, $transitions[0]['time_spent']); + } + + public function testGetTimeSpentByTask() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $transitionModel = new Transition($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'test'))); + $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test'))); + + $task_event = array( + 'project_id' => 1, + 'task_id' => 1, + 'src_column_id' => 1, + 'dst_column_id' => 2, + 'date_moved' => time() - 3600 + ); + + $this->assertTrue($transitionModel->save(1, $task_event)); + + $task_event = array( + 'project_id' => 1, + 'task_id' => 1, + 'src_column_id' => 2, + 'dst_column_id' => 3, + 'date_moved' => time() - 1200 + ); + + $this->assertTrue($transitionModel->save(1, $task_event)); + + $expected = array( + '1' => 3600, + '2' => 1200, + ); + + $this->assertEquals($expected, $transitionModel->getTimeSpentByTask(1)); + } + + public function testGetAllByProject() + { + $projectModel = new Project($this->container); + $taskCreationModel = new TaskCreation($this->container); + $transitionModel = new Transition($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'))); + + $task_event = array( + 'project_id' => 1, + 'src_column_id' => 1, + 'dst_column_id' => 2, + 'date_moved' => time() - 3600 + ); + + $this->assertTrue($transitionModel->save(1, array('task_id' => 1) + $task_event)); + $this->assertTrue($transitionModel->save(1, array('task_id' => 2) + $task_event)); + + $task_event = array( + 'project_id' => 1, + 'src_column_id' => 2, + 'dst_column_id' => 3, + 'date_moved' => time() - 1200 + ); + + $this->assertTrue($transitionModel->save(1, array('task_id' => 1) + $task_event)); + $this->assertTrue($transitionModel->save(1, array('task_id' => 2) + $task_event)); + + $transitions = $transitionModel->getAllByProjectAndDate(1, date('Y-m-d'), date('Y-m-d')); + $this->assertCount(4, $transitions); + + $this->assertEquals(2, $transitions[0]['id']); + $this->assertEquals(1, $transitions[1]['id']); + $this->assertEquals(2, $transitions[2]['id']); + $this->assertEquals(1, $transitions[3]['id']); + + $this->assertEquals('test2', $transitions[0]['title']); + $this->assertEquals('test1', $transitions[1]['title']); + $this->assertEquals('test2', $transitions[2]['title']); + $this->assertEquals('test1', $transitions[3]['title']); + + $this->assertEquals('Ready', $transitions[0]['src_column']); + $this->assertEquals('Ready', $transitions[1]['src_column']); + $this->assertEquals('Backlog', $transitions[2]['src_column']); + $this->assertEquals('Backlog', $transitions[3]['src_column']); + + $this->assertEquals('Work in progress', $transitions[0]['dst_column']); + $this->assertEquals('Work in progress', $transitions[1]['dst_column']); + $this->assertEquals('Ready', $transitions[2]['dst_column']); + $this->assertEquals('Ready', $transitions[3]['dst_column']); + + $this->assertEquals('admin', $transitions[0]['username']); + $this->assertEquals('admin', $transitions[1]['username']); + $this->assertEquals('admin', $transitions[2]['username']); + $this->assertEquals('admin', $transitions[3]['username']); + + $this->assertEquals(1200, $transitions[0]['time_spent']); + $this->assertEquals(1200, $transitions[1]['time_spent']); + $this->assertEquals(3600, $transitions[2]['time_spent']); + $this->assertEquals(3600, $transitions[3]['time_spent']); + } +} diff --git a/tests/units/Model/UserNotificationTest.php b/tests/units/Model/UserNotificationTest.php index 8168a375..53034e20 100644 --- a/tests/units/Model/UserNotificationTest.php +++ b/tests/units/Model/UserNotificationTest.php @@ -4,12 +4,9 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskCreation; -use Kanboard\Model\Subtask; -use Kanboard\Model\Comment; use Kanboard\Model\User; use Kanboard\Model\Group; use Kanboard\Model\GroupMember; -use Kanboard\Model\File; use Kanboard\Model\Project; use Kanboard\Model\ProjectPermission; use Kanboard\Model\Task; @@ -17,7 +14,6 @@ use Kanboard\Model\ProjectUserRole; use Kanboard\Model\ProjectGroupRole; use Kanboard\Model\UserNotification; use Kanboard\Model\UserNotificationFilter; -use Kanboard\Model\UserNotificationType; use Kanboard\Subscriber\UserNotificationSubscriber; use Kanboard\Core\Security\Role; diff --git a/tests/units/Model/UserTest.php b/tests/units/Model/UserTest.php index 0987fa56..7501f2e0 100644 --- a/tests/units/Model/UserTest.php +++ b/tests/units/Model/UserTest.php @@ -5,7 +5,6 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\User; use Kanboard\Model\Subtask; use Kanboard\Model\Comment; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; use Kanboard\Model\TaskFinder; use Kanboard\Model\Project; @@ -96,13 +95,14 @@ class UserTest extends Base $this->assertEquals('you', $users[2]['username']); } - public function testGetList() + public function testGetActiveUsersList() { $u = new User($this->container); $this->assertEquals(2, $u->create(array('username' => 'you'))); $this->assertEquals(3, $u->create(array('username' => 'me', 'name' => 'Me too'))); + $this->assertEquals(4, $u->create(array('username' => 'foobar', 'is_active' => 0))); - $users = $u->getList(); + $users = $u->getActiveUsersList(); $expected = array( 1 => 'admin', @@ -112,7 +112,7 @@ class UserTest extends Base $this->assertEquals($expected, $users); - $users = $u->getList(true); + $users = $u->getActiveUsersList(true); $expected = array( User::EVERYBODY_ID => 'Everybody', @@ -391,4 +391,24 @@ class UserTest extends Base $this->assertEquals('toto', $user['username']); $this->assertEmpty($user['token']); } + + public function testEnableDisable() + { + $userModel = new User($this->container); + $this->assertEquals(2, $userModel->create(array('username' => 'toto'))); + + $this->assertTrue($userModel->isActive(2)); + $user = $userModel->getById(2); + $this->assertEquals(1, $user['is_active']); + + $this->assertTrue($userModel->disable(2)); + $user = $userModel->getById(2); + $this->assertEquals(0, $user['is_active']); + $this->assertFalse($userModel->isActive(2)); + + $this->assertTrue($userModel->enable(2)); + $user = $userModel->getById(2); + $this->assertEquals(1, $user['is_active']); + $this->assertTrue($userModel->isActive(2)); + } } diff --git a/tests/units/Model/UserUnreadNotificationTest.php b/tests/units/Model/UserUnreadNotificationTest.php index bf274d95..918ab411 100644 --- a/tests/units/Model/UserUnreadNotificationTest.php +++ b/tests/units/Model/UserUnreadNotificationTest.php @@ -4,10 +4,7 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\TaskFinder; use Kanboard\Model\TaskCreation; -use Kanboard\Model\Subtask; -use Kanboard\Model\Comment; use Kanboard\Model\User; -use Kanboard\Model\File; use Kanboard\Model\Task; use Kanboard\Model\Project; use Kanboard\Model\UserUnreadNotification; diff --git a/tests/units/Notification/MailTest.php b/tests/units/Notification/MailTest.php index 8f343ba3..7dc6aaef 100644 --- a/tests/units/Notification/MailTest.php +++ b/tests/units/Notification/MailTest.php @@ -7,7 +7,7 @@ use Kanboard\Model\TaskCreation; use Kanboard\Model\Subtask; use Kanboard\Model\Comment; use Kanboard\Model\User; -use Kanboard\Model\File; +use Kanboard\Model\TaskFile; use Kanboard\Model\Project; use Kanboard\Model\Task; use Kanboard\Notification\Mail; @@ -23,7 +23,7 @@ class MailTest extends Base $tc = new TaskCreation($this->container); $s = new Subtask($this->container); $c = new Comment($this->container); - $f = new File($this->container); + $f = new TaskFile($this->container); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1))); diff --git a/tests/units/Notification/WebhookTest.php b/tests/units/Notification/WebhookTest.php index b55cd6e2..5984f303 100644 --- a/tests/units/Notification/WebhookTest.php +++ b/tests/units/Notification/WebhookTest.php @@ -3,11 +3,8 @@ require_once __DIR__.'/../Base.php'; use Kanboard\Model\Config; -use Kanboard\Model\Task; use Kanboard\Model\TaskCreation; -use Kanboard\Model\TaskModification; use Kanboard\Model\Project; -use Kanboard\Model\Comment; use Kanboard\Subscriber\NotificationSubscriber; class WebhookTest extends Base @@ -21,92 +18,12 @@ class WebhookTest extends Base $c->save(array('webhook_url' => 'http://localhost/?task-creation')); - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - - $this->assertStringStartsWith('http://localhost/?task-creation&token=', $this->container['httpClient']->getUrl()); - - $event = $this->container['httpClient']->getData(); - $this->assertNotEmpty($event); - $this->assertArrayHasKey('event_name', $event); - $this->assertArrayHasKey('event_data', $event); - $this->assertEquals('task.create', $event['event_name']); - $this->assertNotEmpty($event['event_data']); - - $this->assertArrayHasKey('project_id', $event['event_data']['task']); - $this->assertArrayHasKey('id', $event['event_data']['task']); - $this->assertArrayHasKey('title', $event['event_data']['task']); - $this->assertArrayHasKey('column_id', $event['event_data']['task']); - $this->assertArrayHasKey('color_id', $event['event_data']['task']); - $this->assertArrayHasKey('swimlane_id', $event['event_data']['task']); - $this->assertArrayHasKey('date_creation', $event['event_data']['task']); - $this->assertArrayHasKey('date_modification', $event['event_data']['task']); - $this->assertArrayHasKey('date_moved', $event['event_data']['task']); - $this->assertArrayHasKey('position', $event['event_data']['task']); - } - - public function testTaskModification() - { - $c = new Config($this->container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tm = new TaskModification($this->container); - $this->container['dispatcher']->addSubscriber(new NotificationSubscriber($this->container)); - - $c->save(array('webhook_url' => 'http://localhost/modif/')); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test'))); - $this->assertTrue($tm->update(array('id' => 1, 'title' => 'test update'))); - - $this->assertStringStartsWith('http://localhost/modif/?token=', $this->container['httpClient']->getUrl()); - - $event = $this->container['httpClient']->getData(); - $this->assertNotEmpty($event); - $this->assertArrayHasKey('event_name', $event); - $this->assertArrayHasKey('event_data', $event); - $this->assertEquals('task.update', $event['event_name']); - $this->assertNotEmpty($event['event_data']); - - $this->assertArrayHasKey('project_id', $event['event_data']['task']); - $this->assertArrayHasKey('id', $event['event_data']['task']); - $this->assertArrayHasKey('title', $event['event_data']['task']); - $this->assertArrayHasKey('column_id', $event['event_data']['task']); - $this->assertArrayHasKey('color_id', $event['event_data']['task']); - $this->assertArrayHasKey('swimlane_id', $event['event_data']['task']); - $this->assertArrayHasKey('date_creation', $event['event_data']['task']); - $this->assertArrayHasKey('date_modification', $event['event_data']['task']); - $this->assertArrayHasKey('date_moved', $event['event_data']['task']); - $this->assertArrayHasKey('position', $event['event_data']['task']); - } - - public function testCommentCreation() - { - $c = new Config($this->container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $cm = new Comment($this->container); - $this->container['dispatcher']->addSubscriber(new NotificationSubscriber($this->container)); - - $c->save(array('webhook_url' => 'http://localhost/comment')); + $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'))); - $this->assertEquals(1, $cm->create(array('task_id' => 1, 'comment' => 'test comment', 'user_id' => 1))); - - $this->assertStringStartsWith('http://localhost/comment?token=', $this->container['httpClient']->getUrl()); - - $event = $this->container['httpClient']->getData(); - $this->assertNotEmpty($event); - $this->assertArrayHasKey('event_name', $event); - $this->assertArrayHasKey('event_data', $event); - $this->assertEquals('comment.create', $event['event_name']); - $this->assertNotEmpty($event['event_data']); - - $this->assertArrayHasKey('task_id', $event['event_data']['comment']); - $this->assertArrayHasKey('user_id', $event['event_data']['comment']); - $this->assertArrayHasKey('comment', $event['event_data']['comment']); - $this->assertArrayHasKey('id', $event['event_data']['comment']); - $this->assertEquals('test comment', $event['event_data']['comment']['comment']); } } diff --git a/tests/units/Validator/ExternalLinkValidatorTest.php b/tests/units/Validator/ExternalLinkValidatorTest.php new file mode 100644 index 00000000..b41b779a --- /dev/null +++ b/tests/units/Validator/ExternalLinkValidatorTest.php @@ -0,0 +1,63 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Validator\ExternalLinkValidator; + +class ExternalLinkValidatorTest extends Base +{ + public function testValidateCreation() + { + $validator = new ExternalLinkValidator($this->container); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertTrue($result[0]); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'task_id' => 'abc', 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink')); + $this->assertFalse($result[0]); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'task_id' => 1, 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateCreation(array('url' => 'http://somewhere', 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateCreation(array('task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + } + + public function testValidateModification() + { + $validator = new ExternalLinkValidator($this->container); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertTrue($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'task_id' => 'abc', 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'task_id' => 1, 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'url' => 'http://somewhere', 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('id' => 1, 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + + $result = $validator->validateModification(array('url' => 'http://somewhere', 'task_id' => 1, 'title' => 'Title', 'link_type' => 'weblink', 'dependency' => 'related')); + $this->assertFalse($result[0]); + } +} diff --git a/tests/units/Validator/PasswordResetValidatorTest.php b/tests/units/Validator/PasswordResetValidatorTest.php index 4af6c75e..d26ad422 100644 --- a/tests/units/Validator/PasswordResetValidatorTest.php +++ b/tests/units/Validator/PasswordResetValidatorTest.php @@ -2,7 +2,6 @@ require_once __DIR__.'/../Base.php'; -use Kanboard\Model\User; use Kanboard\Validator\PasswordResetValidator; class PasswordResetValidatorTest extends Base diff --git a/web.config b/web.config new file mode 100644 index 00000000..c86be8b4 --- /dev/null +++ b/web.config @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <rewrite> + <rules> + <clear /> + <rule name="Kanboard-nice-urls" stopProcessing="true"> + <match url="^" ignoreCase="false" /> + <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> + <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" appendQueryString="true" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> |