summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.docker/crontab/cronjob.alpine1
-rw-r--r--.docker/crontab/cronjob.debian1
-rw-r--r--.docker/nginx/nginx.conf72
-rw-r--r--.docker/php/conf.d/local.ini16
-rw-r--r--.docker/php/php-fpm.conf22
-rwxr-xr-x.docker/services.d/.s6-svscan/finish2
-rwxr-xr-x.docker/services.d/nginx/run2
-rwxr-xr-x.docker/services.d/php/run2
-rw-r--r--.dockerignore17
-rw-r--r--.gitignore2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--ChangeLog16
-rw-r--r--Dockerfile42
-rw-r--r--Makefile3
-rw-r--r--app/Action/TaskDuplicateAnotherProject.php2
-rw-r--r--app/Action/TaskMoveAnotherProject.php2
-rw-r--r--app/Api/Procedure/ProjectFileProcedure.php68
-rw-r--r--app/Api/Procedure/SubtaskTimeTrackingProcedure.php12
-rw-r--r--app/Api/Procedure/TaskExternalLinkProcedure.php106
-rw-r--r--app/Api/Procedure/TaskFileProcedure.php4
-rw-r--r--app/Api/Procedure/TaskProcedure.php4
-rw-r--r--app/Auth/ReverseProxyAuth.php3
-rw-r--r--app/Console/TaskOverdueNotificationCommand.php2
-rw-r--r--app/Controller/ActionCreationController.php2
-rw-r--r--app/Controller/BoardTooltipController.php6
-rw-r--r--app/Controller/ColumnController.php20
-rw-r--r--app/Controller/TaskDuplicationController.php4
-rw-r--r--app/Controller/TaskRecurrenceController.php8
-rw-r--r--app/Controller/WebNotificationController.php4
-rw-r--r--app/Core/Base.php6
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php24
-rw-r--r--app/Core/Filter/Lexer.php2
-rw-r--r--app/Core/Ldap/User.php8
-rw-r--r--app/Core/Queue/JobHandler.php1
-rw-r--r--app/Formatter/TaskICalFormatter.php17
-rw-r--r--app/Helper/TaskHelper.php25
-rw-r--r--app/Helper/UserHelper.php8
-rw-r--r--app/Locale/bs_BA/translations.php2
-rw-r--r--app/Locale/cs_CZ/translations.php136
-rw-r--r--app/Locale/da_DK/translations.php2
-rw-r--r--app/Locale/de_DE/translations.php40
-rw-r--r--app/Locale/el_GR/translations.php2
-rw-r--r--app/Locale/es_ES/translations.php2
-rw-r--r--app/Locale/fi_FI/translations.php2
-rw-r--r--app/Locale/fr_FR/translations.php4
-rw-r--r--app/Locale/hu_HU/translations.php2
-rw-r--r--app/Locale/id_ID/translations.php2
-rw-r--r--app/Locale/it_IT/translations.php38
-rw-r--r--app/Locale/ja_JP/translations.php2
-rw-r--r--app/Locale/ko_KR/translations.php2
-rw-r--r--app/Locale/my_MY/translations.php2
-rw-r--r--app/Locale/nb_NO/translations.php2
-rw-r--r--app/Locale/nl_NL/translations.php2
-rw-r--r--app/Locale/pl_PL/translations.php2
-rw-r--r--app/Locale/pt_BR/translations.php2
-rw-r--r--app/Locale/pt_PT/translations.php40
-rw-r--r--app/Locale/ru_RU/translations.php124
-rw-r--r--app/Locale/sr_Latn_RS/translations.php2
-rw-r--r--app/Locale/sv_SE/translations.php2
-rw-r--r--app/Locale/th_TH/translations.php2
-rw-r--r--app/Locale/tr_TR/translations.php2
-rw-r--r--app/Locale/zh_CN/translations.php2
-rw-r--r--app/Model/ColumnModel.php18
-rw-r--r--app/Model/NotificationModel.php37
-rw-r--r--app/Model/ProjectDuplicationModel.php24
-rw-r--r--app/Model/ProjectModel.php13
-rw-r--r--app/Model/ProjectTaskDuplicationModel.php35
-rw-r--r--app/Model/ProjectTaskPriorityModel.php74
-rw-r--r--app/Model/SwimlaneModel.php24
-rw-r--r--app/Model/TagDuplicationModel.php87
-rw-r--r--app/Model/TaskCreationModel.php2
-rw-r--r--app/Model/TaskDuplicationModel.php165
-rw-r--r--app/Model/TaskFinderModel.php8
-rw-r--r--app/Model/TaskModel.php143
-rw-r--r--app/Model/TaskModificationModel.php2
-rw-r--r--app/Model/TaskProjectDuplicationModel.php60
-rw-r--r--app/Model/TaskProjectMoveModel.php68
-rw-r--r--app/Model/TaskRecurrenceModel.php147
-rw-r--r--app/Model/TaskTagModel.php18
-rw-r--r--app/Schema/Mysql.php7
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sql/mysql.sql28
-rw-r--r--app/Schema/Sql/postgres.sql96
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/ServiceProvider/ApiProvider.php4
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php2
-rw-r--r--app/ServiceProvider/ClassProvider.php6
-rw-r--r--app/Subscriber/RecurringTaskSubscriber.php6
-rw-r--r--app/Template/board/task_footer.php2
-rw-r--r--app/Template/column/create.php2
-rw-r--r--app/Template/column/edit.php2
-rw-r--r--app/Template/dashboard/notifications.php6
-rw-r--r--app/Template/project_creation/create.php3
-rw-r--r--app/Template/project_view/duplicate.php3
-rw-r--r--app/Template/project_view/show.php10
-rw-r--r--app/Template/task_creation/show.php1
-rw-r--r--app/Template/task_gantt_creation/show.php1
-rw-r--r--app/Template/task_modification/show.php1
-rw-r--r--app/User/Avatar/LetterAvatarProvider.php2
-rw-r--r--app/User/ReverseProxyUserProvider.php21
-rw-r--r--app/constants.php2
-rw-r--r--assets/css/app.min.css2
-rw-r--r--assets/css/src/form.css4
-rw-r--r--assets/css/vendor.min.css2
-rw-r--r--assets/js/vendor.min.js5
-rw-r--r--bower.json2
-rw-r--r--doc/api-external-task-link-procedures.markdown221
-rw-r--r--doc/api-internal-task-link-procedures.markdown187
-rw-r--r--doc/api-json-rpc.markdown6
-rw-r--r--doc/api-link-procedures.markdown185
-rw-r--r--doc/api-project-file-procedures.markdown221
-rw-r--r--doc/api-project-procedures.markdown49
-rw-r--r--doc/api-subtask-time-tracking-procedures.markdown102
-rw-r--r--doc/api-task-file-procedures.markdown (renamed from doc/api-file-procedures.markdown)4
-rw-r--r--doc/creating-tasks.markdown19
-rw-r--r--doc/docker.markdown14
-rw-r--r--doc/env.markdown1
-rw-r--r--doc/fr_FR/creating-tasks.markdown14
-rw-r--r--doc/fr_FR/index.markdown1
-rw-r--r--doc/fr_FR/removing-projects.markdown10
-rw-r--r--doc/fr_FR/screenshots/task-creation-board.pngbin0 -> 3915 bytes
-rw-r--r--doc/fr_FR/screenshots/task-creation-form.pngbin0 -> 42123 bytes
-rw-r--r--doc/index.markdown2
-rw-r--r--doc/nice-urls.markdown18
-rw-r--r--doc/removing-projects.markdown10
-rw-r--r--doc/screenshots/project-remove.pngbin0 -> 4330 bytes
-rw-r--r--doc/screenshots/tags-board.pngbin0 -> 7405 bytes
-rw-r--r--doc/screenshots/tags-global.pngbin0 -> 9617 bytes
-rw-r--r--doc/screenshots/tags-projects.pngbin0 -> 13718 bytes
-rw-r--r--doc/screenshots/tags-search.pngbin0 -> 3904 bytes
-rw-r--r--doc/screenshots/tags-task.pngbin0 -> 4766 bytes
-rw-r--r--doc/screenshots/task-creation-board.pngbin0 -> 10242 bytes
-rw-r--r--doc/screenshots/task-creation-form.pngbin0 -> 34726 bytes
-rw-r--r--doc/tags.markdown28
-rw-r--r--doc/windows-iis-installation.markdown40
-rw-r--r--docker-compose.yml4
-rw-r--r--docker/crontab/cronjob.alpine1
-rw-r--r--docker/kanboard/config.php (renamed from .docker/kanboard/config.php)1
-rw-r--r--docker/php/env.conf2
-rwxr-xr-xdocker/services.d/cron/run (renamed from .docker/services.d/cron/run)0
-rwxr-xr-xtests/docker/entrypoint.sh2
-rw-r--r--tests/integration/BaseProcedureTest.php11
-rw-r--r--tests/integration/ProjectFileProcedureTest.php66
-rw-r--r--tests/integration/SubtaskProcedureTest.php11
-rw-r--r--tests/integration/SubtaskTimeTrackingProcedureTest.php46
-rw-r--r--tests/integration/TaskExternalLinkProcedureTest.php98
-rw-r--r--tests/integration/TaskFileProcedureTest.php2
-rw-r--r--tests/units/Auth/ReverseProxyAuthTest.php111
-rw-r--r--tests/units/Core/Filter/LexerTest.php44
-rw-r--r--tests/units/Core/Ldap/LdapUserTest.php60
-rw-r--r--tests/units/Helper/UserHelperTest.php15
-rw-r--r--tests/units/Model/NotificationModelTest.php109
-rw-r--r--tests/units/Model/NotificationTest.php68
-rw-r--r--tests/units/Model/ProjectDuplicationModelTest.php548
-rw-r--r--tests/units/Model/ProjectDuplicationTest.php482
-rw-r--r--tests/units/Model/ProjectModelTest.php (renamed from tests/units/Model/ProjectTest.php)196
-rw-r--r--tests/units/Model/ProjectTaskPriorityModelTest.php84
-rw-r--r--tests/units/Model/SwimlaneTest.php10
-rw-r--r--tests/units/Model/TagDuplicationModelTest.php31
-rw-r--r--tests/units/Model/TaskCreationModelTest.php403
-rw-r--r--tests/units/Model/TaskCreationTest.php397
-rw-r--r--tests/units/Model/TaskDuplicationModelTest.php142
-rw-r--r--tests/units/Model/TaskDuplicationTest.php700
-rw-r--r--tests/units/Model/TaskFinderModelTest.php142
-rw-r--r--tests/units/Model/TaskFinderTest.php115
-rw-r--r--tests/units/Model/TaskModelTest.php30
-rw-r--r--tests/units/Model/TaskModificationModelTest.php322
-rw-r--r--tests/units/Model/TaskModificationTest.php313
-rw-r--r--tests/units/Model/TaskProjectDuplicationModelTest.php377
-rw-r--r--tests/units/Model/TaskProjectMoveModelTest.php277
-rw-r--r--tests/units/Model/TaskRecurrenceModelTest.php126
-rw-r--r--tests/units/Model/TaskTagModelTest.php23
-rw-r--r--tests/units/Model/TaskTest.php58
-rw-r--r--tests/units/User/Avatar/LetterAvatarProviderTest.php2
-rw-r--r--web.config15
175 files changed, 5481 insertions, 3211 deletions
diff --git a/.docker/crontab/cronjob.alpine b/.docker/crontab/cronjob.alpine
deleted file mode 100644
index 91ad044e..00000000
--- a/.docker/crontab/cronjob.alpine
+++ /dev/null
@@ -1 +0,0 @@
-1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1
diff --git a/.docker/crontab/cronjob.debian b/.docker/crontab/cronjob.debian
deleted file mode 100644
index 40310d4f..00000000
--- a/.docker/crontab/cronjob.debian
+++ /dev/null
@@ -1 +0,0 @@
-@daily www-data cd /var/www/html/kanboard && ./kanboard cronjob >/dev/null 2>&1
diff --git a/.docker/nginx/nginx.conf b/.docker/nginx/nginx.conf
deleted file mode 100644
index a04fce74..00000000
--- a/.docker/nginx/nginx.conf
+++ /dev/null
@@ -1,72 +0,0 @@
-user nginx;
-worker_processes 1;
-pid /var/run/nginx.pid;
-
-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
deleted file mode 100644
index 3660bed8..00000000
--- a/.docker/php/conf.d/local.ini
+++ /dev/null
@@ -1,16 +0,0 @@
-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
-always_populate_raw_post_data = -1 \ No newline at end of file
diff --git a/.docker/php/php-fpm.conf b/.docker/php/php-fpm.conf
deleted file mode 100644
index c51c2212..00000000
--- a/.docker/php/php-fpm.conf
+++ /dev/null
@@ -1,22 +0,0 @@
-[global]
-error_log = /dev/stderr
-log_level = error
-daemonize = no
-
-[www]
-env[DATABASE_URL] = $DATABASE_URL
-env[DEBUG] = $DEBUG
-env[LOG_DRIVER] = stderr
-
-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
deleted file mode 100755
index fc3b1e93..00000000
--- a/.docker/services.d/.s6-svscan/finish
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-/bin/true \ No newline at end of file
diff --git a/.docker/services.d/nginx/run b/.docker/services.d/nginx/run
deleted file mode 100755
index 40a8b54a..00000000
--- a/.docker/services.d/nginx/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/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
deleted file mode 100755
index e7d2dadd..00000000
--- a/.docker/services.d/php/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/execlineb -P
-php-fpm -F \ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
index 569d36ca..19c8e14b 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,21 @@
.git
.git*
+.dockerignore
+.vagrant
+.idea
data/*
Makefile
.*.yml
-*.json \ No newline at end of file
+*.yml
+*.js
+*.md
+*.sh
+app.json
+bower.json
+nitrous.json
+package.json
+Vagrantfile
+web.config
+Makefile
+bower_components
+node_modules
diff --git a/.gitignore b/.gitignore
index 742ea5e7..6fb87517 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,6 @@ data/files
data/cache
/vendor
*.bak
-!.docker/kanboard/config.php
+!docker/kanboard/config.php
node_modules
bower_components
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 01ac3f2b..2ddc7146 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -48,6 +48,7 @@ Contributors:
- [Iterate From 0](https://github.com/freebsd-kanboard)
- [Jan Dittrich](https://github.com/jdittrich)
- [Janne Mäntyharju](https://github.com/JanneMantyharju)
+- [Jannik Winkel](https://github.com/kiney)
- [Jean-François Magnier](https://github.com/lefakir)
- [Jeff Guillou](https://github.com/jf-guillou)
- [Jesusaplsoft](https://github.com/jesusaplsoft)
diff --git a/ChangeLog b/ChangeLog
index 883cc6cf..7e6662d6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,22 +1,36 @@
-Version 1.0.31 (unreleased)
+Version 1.0.31
--------------
New features:
+* Added tags: global and specific by project
* Added application and project roles validation for API procedure calls
* Added new API call: "getProjectByIdentifier"
+* Added new API calls for external task links, project attachments and subtask time tracking
Improvements:
+* Use PHP 7 for the Docker image
+* Preserve role for existing users when using ReverseProxy authentication
+* Handle priority for task and project duplication
+* Expose task reference field to the user interface
+* Improve ICal export
* Added argument owner_id and identifier to project API calls
* Rewrite integration tests to run with Docker containers
* Use the same task form layout everywhere
* Removed some tasks dropdown menus that are now available with task edit form
* Make embedded documentation readable in multiple languages (if a translation is available)
+* Added acceptance tests (browser tests)
Bug fixes:
* Fixed broken CSV exports
+* Fixed identical background color for LetterAvatar on 32bits platforms (Hash greater than PHP_MAX_INT)
+* Fixed lexer issue with non word characters
+* Flush memory cache in worker to get latest config values
+* Fixed empty title for web notification with only one overdue task
+* Take default swimlane into consideration for SwimlaneModel::getFirstActiveSwimlane()
+* Fixed "due today" highlighting
Version 1.0.30
--------------
diff --git a/Dockerfile b/Dockerfile
index aa9eb9cf..99e940bb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,35 +1,13 @@
-FROM alpine:3.4
-MAINTAINER Frederic Guillot <fred@kanboard.net>
+FROM fguillot/alpine-nginx-php7
-RUN apk update && \
- apk add nginx bash ca-certificates s6 curl \
- php5-fpm php5-json php5-zlib php5-xml php5-dom php5-ctype php5-opcache php5-zip \
- php5-pdo php5-pdo_mysql php5-pdo_sqlite php5-pdo_pgsql php5-ldap \
- php5-gd php5-mcrypt php5-openssl php5-phar && \
- rm -rf /var/cache/apk/*
+COPY . /var/www/app
+COPY docker/kanboard/config.php /var/www/app/config.php
+COPY docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx
+COPY docker/services.d/cron /etc/services.d/cron
+COPY docker/php/env.conf /etc/php7/php-fpm.d/env.conf
-RUN curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer
+RUN cd /var/www/app && composer --prefer-dist --no-dev --optimize-autoloader --quiet install
+RUN chown -R nginx:nginx /var/www/app/data /var/www/app/plugins
-RUN cd /var/www \
- && curl -LO https://github.com/fguillot/kanboard/archive/master.zip \
- && unzip -qq master.zip \
- && rm -f *.zip \
- && mv kanboard-master kanboard \
- && cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install \
- && chown -R nginx:nginx /var/www/kanboard \
- && chown -R nginx:nginx /var/lib/nginx
-
-COPY .docker/services.d /etc/services.d
-COPY .docker/php/conf.d/local.ini /etc/php5/conf.d/
-COPY .docker/php/php-fpm.conf /etc/php5/
-COPY .docker/nginx/nginx.conf /etc/nginx/
-COPY .docker/kanboard/config.php /var/www/kanboard/
-COPY .docker/crontab/cronjob.alpine /var/spool/cron/crontabs/nginx
-
-EXPOSE 80
-
-VOLUME /var/www/kanboard/data
-VOLUME /var/www/kanboard/plugins
-
-ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
-CMD []
+VOLUME /var/www/app/data
+VOLUME /var/www/app/plugins
diff --git a/Makefile b/Makefile
index 47e9d389..4595481d 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,8 @@ archive:
@ rm -rf ${BUILD_DIR}/kanboard/*.lock
@ rm -rf ${BUILD_DIR}/kanboard/*.json
@ rm -rf ${BUILD_DIR}/kanboard/*.js
- @ rm -rf ${BUILD_DIR}/kanboard/.docker
+ @ rm -rf ${BUILD_DIR}/kanboard/.dockerignore
+ @ rm -rf ${BUILD_DIR}/kanboard/docker
@ rm -rf ${BUILD_DIR}/kanboard/nitrous*
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name doc -type d -exec rm -rf {} +;
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +;
diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php
index 1d4a2f13..d70d2ee8 100644
--- a/app/Action/TaskDuplicateAnotherProject.php
+++ b/app/Action/TaskDuplicateAnotherProject.php
@@ -76,7 +76,7 @@ class TaskDuplicateAnotherProject extends Base
public function doAction(array $data)
{
$destination_column_id = $this->columnModel->getFirstColumnId($this->getParam('project_id'));
- return (bool) $this->taskDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id);
+ return (bool) $this->taskProjectDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id);
}
/**
diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php
index 73ad4b69..66635a63 100644
--- a/app/Action/TaskMoveAnotherProject.php
+++ b/app/Action/TaskMoveAnotherProject.php
@@ -75,7 +75,7 @@ class TaskMoveAnotherProject extends Base
*/
public function doAction(array $data)
{
- return $this->taskDuplicationModel->moveToProject($data['task_id'], $this->getParam('project_id'));
+ return $this->taskProjectMoveModel->moveToProject($data['task_id'], $this->getParam('project_id'));
}
/**
diff --git a/app/Api/Procedure/ProjectFileProcedure.php b/app/Api/Procedure/ProjectFileProcedure.php
new file mode 100644
index 00000000..48466ce3
--- /dev/null
+++ b/app/Api/Procedure/ProjectFileProcedure.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Kanboard\Api\Procedure;
+
+use Kanboard\Api\Authorization\ProjectAuthorization;
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+
+/**
+ * Project File API controller
+ *
+ * @package Kanboard\Api\Procedure
+ * @author Frederic Guillot
+ */
+class ProjectFileProcedure extends BaseProcedure
+{
+ public function getProjectFile($project_id, $file_id)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectFile', $project_id);
+ return $this->projectFileModel->getById($file_id);
+ }
+
+ public function getAllProjectFiles($project_id)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllProjectFiles', $project_id);
+ return $this->projectFileModel->getAll($project_id);
+ }
+
+ public function downloadProjectFile($project_id, $file_id)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadProjectFile', $project_id);
+
+ try {
+ $file = $this->projectFileModel->getById($file_id);
+
+ if (! empty($file)) {
+ return base64_encode($this->objectStorage->get($file['path']));
+ }
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+
+ return '';
+ }
+
+ public function createProjectFile($project_id, $filename, $blob)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createProjectFile', $project_id);
+
+ try {
+ return $this->projectFileModel->uploadContent($project_id, $filename, $blob);
+ } catch (ObjectStorageException $e) {
+ $this->logger->error(__METHOD__.': '.$e->getMessage());
+ return false;
+ }
+ }
+
+ public function removeProjectFile($project_id, $file_id)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectFile', $project_id);
+ return $this->projectFileModel->remove($file_id);
+ }
+
+ public function removeAllProjectFiles($project_id)
+ {
+ ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAllProjectFiles', $project_id);
+ return $this->projectFileModel->removeAll($project_id);
+ }
+}
diff --git a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php
index 5d1988d6..5ceaa08d 100644
--- a/app/Api/Procedure/SubtaskTimeTrackingProcedure.php
+++ b/app/Api/Procedure/SubtaskTimeTrackingProcedure.php
@@ -5,7 +5,7 @@ namespace Kanboard\Api\Procedure;
use Kanboard\Api\Authorization\SubtaskAuthorization;
/**
- * Subtask Time Tracking API controller
+ * Subtask Time Tracking API controller
*
* @package Kanboard\Api\Procedure
* @author Frederic Guillot
@@ -19,19 +19,19 @@ class SubtaskTimeTrackingProcedure extends BaseProcedure
return $this->subtaskTimeTrackingModel->hasTimer($subtask_id, $user_id);
}
- public function logSubtaskStartTime($subtask_id, $user_id)
+ public function setSubtaskStartTime($subtask_id, $user_id)
{
- SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskStartTime', $subtask_id);
+ SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskStartTime', $subtask_id);
return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id);
}
- public function logSubtaskEndTime($subtask_id,$user_id)
+ public function setSubtaskEndTime($subtask_id, $user_id)
{
- SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'logSubtaskEndTime', $subtask_id);
+ SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskEndTime', $subtask_id);
return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id);
}
- public function getSubtaskTimeSpent($subtask_id,$user_id)
+ public function getSubtaskTimeSpent($subtask_id, $user_id)
{
SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSubtaskTimeSpent', $subtask_id);
return $this->subtaskTimeTrackingModel->getTimeSpent($subtask_id, $user_id);
diff --git a/app/Api/Procedure/TaskExternalLinkProcedure.php b/app/Api/Procedure/TaskExternalLinkProcedure.php
new file mode 100644
index 00000000..05ec6906
--- /dev/null
+++ b/app/Api/Procedure/TaskExternalLinkProcedure.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Kanboard\Api\Procedure;
+
+use Kanboard\Api\Authorization\TaskAuthorization;
+use Kanboard\Core\ExternalLink\ExternalLinkManager;
+use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound;
+
+/**
+ * Task External Link API controller
+ *
+ * @package Kanboard\Api\Procedure
+ * @author Frederic Guillot
+ */
+class TaskExternalLinkProcedure extends BaseProcedure
+{
+ public function getExternalTaskLinkTypes()
+ {
+ return $this->externalLinkManager->getTypes();
+ }
+
+ public function getExternalTaskLinkProviderDependencies($providerName)
+ {
+ try {
+ return $this->externalLinkManager->getProvider($providerName)->getDependencies();
+ } catch (ExternalLinkProviderNotFound $e) {
+ $this->logger->error(__METHOD__.': '.$e->getMessage());
+ return false;
+ }
+ }
+
+ public function getExternalTaskLinkById($task_id, $link_id)
+ {
+ TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLink', $task_id);
+ return $this->taskExternalLinkModel->getById($link_id);
+ }
+
+ public function getAllExternalTaskLinks($task_id)
+ {
+ TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLinks', $task_id);
+ return $this->taskExternalLinkModel->getAll($task_id);
+ }
+
+ public function createExternalTaskLink($task_id, $url, $dependency, $type = ExternalLinkManager::TYPE_AUTO, $title = '')
+ {
+ TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createExternalTaskLink', $task_id);
+
+ try {
+ $provider = $this->externalLinkManager
+ ->setUserInputText($url)
+ ->setUserInputType($type)
+ ->find();
+
+ $link = $provider->getLink();
+
+ $values = array(
+ 'task_id' => $task_id,
+ 'title' => $title ?: $link->getTitle(),
+ 'url' => $link->getUrl(),
+ 'link_type' => $provider->getType(),
+ 'dependency' => $dependency,
+ );
+
+ list($valid, $errors) = $this->externalLinkValidator->validateCreation($values);
+
+ if (! $valid) {
+ $this->logger->error(__METHOD__.': '.var_export($errors));
+ return false;
+ }
+
+ return $this->taskExternalLinkModel->create($values);
+ } catch (ExternalLinkProviderNotFound $e) {
+ $this->logger->error(__METHOD__.': '.$e->getMessage());
+ }
+
+ return false;
+ }
+
+ public function updateExternalTaskLink($task_id, $link_id, $title = null, $url = null, $dependency = null)
+ {
+ TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateExternalTaskLink', $task_id);
+
+ $link = $this->taskExternalLinkModel->getById($link_id);
+ $values = $this->filterValues(array(
+ 'title' => $title,
+ 'url' => $url,
+ 'dependency' => $dependency,
+ ));
+
+ $values = array_merge($link, $values);
+ list($valid, $errors) = $this->externalLinkValidator->validateModification($values);
+
+ if (! $valid) {
+ $this->logger->error(__METHOD__.': '.var_export($errors));
+ return false;
+ }
+
+ return $this->taskExternalLinkModel->update($values);
+ }
+
+ public function removeExternalTaskLink($task_id, $link_id)
+ {
+ TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeExternalTaskLink', $task_id);
+ return $this->taskExternalLinkModel->remove($link_id);
+ }
+}
diff --git a/app/Api/Procedure/TaskFileProcedure.php b/app/Api/Procedure/TaskFileProcedure.php
index 5aa7ea0b..bd006578 100644
--- a/app/Api/Procedure/TaskFileProcedure.php
+++ b/app/Api/Procedure/TaskFileProcedure.php
@@ -30,7 +30,7 @@ class TaskFileProcedure extends BaseProcedure
public function downloadTaskFile($file_id)
{
TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadTaskFile', $file_id);
-
+
try {
$file = $this->taskFileModel->getById($file_id);
@@ -51,7 +51,7 @@ class TaskFileProcedure extends BaseProcedure
try {
return $this->taskFileModel->uploadContent($task_id, $filename, $blob);
} catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
+ $this->logger->error(__METHOD__.': '.$e->getMessage());
return false;
}
}
diff --git a/app/Api/Procedure/TaskProcedure.php b/app/Api/Procedure/TaskProcedure.php
index 2d29a4ef..8661deef 100644
--- a/app/Api/Procedure/TaskProcedure.php
+++ b/app/Api/Procedure/TaskProcedure.php
@@ -77,13 +77,13 @@ class TaskProcedure extends BaseProcedure
public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'moveTaskToProject', $project_id);
- return $this->taskDuplicationModel->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+ return $this->taskProjectMoveModel->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
}
public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'duplicateTaskToProject', $project_id);
- return $this->taskDuplicationModel->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+ return $this->taskProjectDuplicationModel->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
}
public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0,
diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php
index b9730c5c..fdf936b1 100644
--- a/app/Auth/ReverseProxyAuth.php
+++ b/app/Auth/ReverseProxyAuth.php
@@ -45,7 +45,8 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac
$username = $this->request->getRemoteUser();
if (! empty($username)) {
- $this->userInfo = new ReverseProxyUserProvider($username);
+ $userProfile = $this->userModel->getByUsername($username);
+ $this->userInfo = new ReverseProxyUserProvider($username, $userProfile ?: array());
return true;
}
diff --git a/app/Console/TaskOverdueNotificationCommand.php b/app/Console/TaskOverdueNotificationCommand.php
index 225a6a1a..36276615 100644
--- a/app/Console/TaskOverdueNotificationCommand.php
+++ b/app/Console/TaskOverdueNotificationCommand.php
@@ -149,7 +149,7 @@ class TaskOverdueNotificationCommand extends BaseCommand
$this->userNotificationModel->sendUserNotification(
$user,
TaskModel::EVENT_OVERDUE,
- array('tasks' => $user_tasks, 'project_name' => implode(", ", $project_names))
+ array('tasks' => $user_tasks, 'project_name' => implode(', ', $project_names))
);
}
}
diff --git a/app/Controller/ActionCreationController.php b/app/Controller/ActionCreationController.php
index abd8abd3..9b228f28 100644
--- a/app/Controller/ActionCreationController.php
+++ b/app/Controller/ActionCreationController.php
@@ -81,7 +81,7 @@ class ActionCreationController extends BaseController
'colors_list' => $this->colorModel->getList(),
'categories_list' => $this->categoryModel->getList($project['id']),
'links_list' => $this->linkModel->getList(0, false),
- 'priorities_list' => $this->projectModel->getPriorities($project),
+ 'priorities_list' => $this->projectTaskPriorityModel->getPriorities($project),
'project' => $project,
'available_actions' => $this->actionManager->getAvailableActions(),
'events' => $this->actionManager->getCompatibleEvents($values['action_name']),
diff --git a/app/Controller/BoardTooltipController.php b/app/Controller/BoardTooltipController.php
index 2a947027..134d728e 100644
--- a/app/Controller/BoardTooltipController.php
+++ b/app/Controller/BoardTooltipController.php
@@ -107,9 +107,9 @@ class BoardTooltipController extends BaseController
$this->response->html($this->template->render('task_recurrence/info', array(
'task' => $task,
- 'recurrence_trigger_list' => $this->taskModel->getRecurrenceTriggerList(),
- 'recurrence_timeframe_list' => $this->taskModel->getRecurrenceTimeframeList(),
- 'recurrence_basedate_list' => $this->taskModel->getRecurrenceBasedateList(),
+ 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(),
)));
}
diff --git a/app/Controller/ColumnController.php b/app/Controller/ColumnController.php
index 95fbcaaa..d3f0e36e 100644
--- a/app/Controller/ColumnController.php
+++ b/app/Controller/ColumnController.php
@@ -66,7 +66,15 @@ class ColumnController extends BaseController
list($valid, $errors) = $this->columnValidator->validateCreation($values);
if ($valid) {
- if ($this->columnModel->create($project['id'], $values['title'], $values['task_limit'], $values['description']) !== false) {
+ $result = $this->columnModel->create(
+ $project['id'],
+ $values['title'],
+ $values['task_limit'],
+ $values['description'],
+ isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0
+ );
+
+ if ($result !== false) {
$this->flash->success(t('Column created successfully.'));
return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])), true);
} else {
@@ -111,7 +119,15 @@ class ColumnController extends BaseController
list($valid, $errors) = $this->columnValidator->validateModification($values);
if ($valid) {
- if ($this->columnModel->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
+ $result = $this->columnModel->update(
+ $values['id'],
+ $values['title'],
+ $values['task_limit'],
+ $values['description'],
+ isset($values['hide_in_dashboard']) ? $values['hide_in_dashboard'] : 0
+ );
+
+ if ($result) {
$this->flash->success(t('Board updated successfully.'));
return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])));
} else {
diff --git a/app/Controller/TaskDuplicationController.php b/app/Controller/TaskDuplicationController.php
index 6a475374..915bf8f8 100644
--- a/app/Controller/TaskDuplicationController.php
+++ b/app/Controller/TaskDuplicationController.php
@@ -50,7 +50,7 @@ class TaskDuplicationController extends BaseController
$values = $this->request->getValues();
list($valid, ) = $this->taskValidator->validateProjectModification($values);
- if ($valid && $this->taskDuplicationModel->moveToProject($task['id'],
+ if ($valid && $this->taskProjectMoveModel->moveToProject($task['id'],
$values['project_id'],
$values['swimlane_id'],
$values['column_id'],
@@ -80,7 +80,7 @@ class TaskDuplicationController extends BaseController
list($valid, ) = $this->taskValidator->validateProjectModification($values);
if ($valid) {
- $task_id = $this->taskDuplicationModel->duplicateToProject(
+ $task_id = $this->taskProjectDuplicationModel->duplicateToProject(
$task['id'], $values['project_id'], $values['swimlane_id'],
$values['column_id'], $values['category_id'], $values['owner_id']
);
diff --git a/app/Controller/TaskRecurrenceController.php b/app/Controller/TaskRecurrenceController.php
index dc7a0e1b..c6fdfa37 100644
--- a/app/Controller/TaskRecurrenceController.php
+++ b/app/Controller/TaskRecurrenceController.php
@@ -31,10 +31,10 @@ class TaskRecurrenceController extends BaseController
'values' => $values,
'errors' => $errors,
'task' => $task,
- 'recurrence_status_list' => $this->taskModel->getRecurrenceStatusList(),
- 'recurrence_trigger_list' => $this->taskModel->getRecurrenceTriggerList(),
- 'recurrence_timeframe_list' => $this->taskModel->getRecurrenceTimeframeList(),
- 'recurrence_basedate_list' => $this->taskModel->getRecurrenceBasedateList(),
+ 'recurrence_status_list' => $this->taskRecurrenceModel->getRecurrenceStatusList(),
+ 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(),
)));
}
diff --git a/app/Controller/WebNotificationController.php b/app/Controller/WebNotificationController.php
index 46a42063..30e317f8 100644
--- a/app/Controller/WebNotificationController.php
+++ b/app/Controller/WebNotificationController.php
@@ -54,14 +54,14 @@ class WebNotificationController extends BaseController
$this->response->redirect($this->helper->url->to(
'TaskViewController',
'show',
- array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id']),
+ array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data'])),
'comment-'.$notification['event_data']['comment']['id']
));
} else {
$this->response->redirect($this->helper->url->to(
'TaskViewController',
'show',
- array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id'])
+ array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data']))
));
}
}
diff --git a/app/Core/Base.php b/app/Core/Base.php
index eacca65d..8103ec14 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -86,15 +86,21 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectGroupRoleModel $projectGroupRoleModel
* @property \Kanboard\Model\ProjectNotificationModel $projectNotificationModel
* @property \Kanboard\Model\ProjectNotificationTypeModel $projectNotificationTypeModel
+ * @property \Kanboard\Model\ProjectTaskDuplicationModel $projectTaskDuplicationModel
+ * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel
* @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel
* @property \Kanboard\Model\SubtaskModel $subtaskModel
* @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel
* @property \Kanboard\Model\SwimlaneModel $swimlaneModel
+ * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel
* @property \Kanboard\Model\TagModel $tagModel
* @property \Kanboard\Model\TaskModel $taskModel
* @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel
* @property \Kanboard\Model\TaskCreationModel $taskCreationModel
* @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel
+ * @property \Kanboard\Model\TaskProjectDuplicationModel $taskProjectDuplicationModel
+ * @property \Kanboard\Model\TaskProjectMoveModel $taskProjectMoveModel
+ * @property \Kanboard\Model\TaskRecurrenceModel $taskRecurrenceModel
* @property \Kanboard\Model\TaskExternalLinkModel $taskExternalLinkModel
* @property \Kanboard\Model\TaskFinderModel $taskFinderModel
* @property \Kanboard\Model\TaskLinkModel $taskLinkModel
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
index 804e6b34..5a037999 100644
--- a/app/Core/ExternalLink/ExternalLinkManager.php
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -153,6 +153,30 @@ class ExternalLinkManager extends Base
}
/**
+ * Set provider type
+ *
+ * @access public
+ * @param string $userInputType
+ * @return ExternalLinkManager
+ */
+ public function setUserInputType($userInputType)
+ {
+ $this->userInputType = $userInputType;
+ return $this;
+ }
+
+ /**
+ * Set external link
+ * @param string $userInputText
+ * @return ExternalLinkManager
+ */
+ public function setUserInputText($userInputText)
+ {
+ $this->userInputText = $userInputText;
+ return $this;
+ }
+
+ /**
* Find a provider that user input
*
* @access private
diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php
index fa5b8d2d..3ff57641 100644
--- a/app/Core/Filter/Lexer.php
+++ b/app/Core/Filter/Lexer.php
@@ -30,7 +30,7 @@ class Lexer
'/^([<=>]{1,2}\w+)/u' => 'T_STRING',
'/^([<=>]{1,2}".+")/' => 'T_STRING',
'/^("(.+)")/' => 'T_STRING',
- '/^(\w+)/u' => 'T_STRING',
+ '/^(\S+)/u' => 'T_STRING',
'/^(#\d+)/' => 'T_STRING',
);
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index 91b48530..4bc1f5f9 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -116,7 +116,7 @@ class User
*/
protected function getRole(array $groupIds)
{
- if ($this->hasGroupsNotConfigured()) {
+ if (! $this->hasGroupsConfigured()) {
return null;
}
@@ -278,14 +278,14 @@ class User
}
/**
- * Return true if LDAP Group mapping is not configured
+ * Return true if LDAP Group mapping are configured
*
* @access public
* @return boolean
*/
- public function hasGroupsNotConfigured()
+ public function hasGroupsConfigured()
{
- return !$this->getGroupAdminDn() && !$this->getGroupManagerDn();
+ return $this->getGroupAdminDn() || $this->getGroupManagerDn();
}
/**
diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php
index f8736cce..7ca36328 100644
--- a/app/Core/Queue/JobHandler.php
+++ b/app/Core/Queue/JobHandler.php
@@ -40,6 +40,7 @@ class JobHandler extends Base
{
$payload = $job->getBody();
$className = $payload['class'];
+ $this->memoryCache->flush();
$this->prepareJobSession($payload['user_id']);
if (DEBUG) {
diff --git a/app/Formatter/TaskICalFormatter.php b/app/Formatter/TaskICalFormatter.php
index 890674c7..ad2a4449 100644
--- a/app/Formatter/TaskICalFormatter.php
+++ b/app/Formatter/TaskICalFormatter.php
@@ -6,6 +6,7 @@ use DateTime;
use Eluceo\iCal\Component\Calendar;
use Eluceo\iCal\Component\Event;
use Eluceo\iCal\Property\Event\Attendees;
+use Eluceo\iCal\Property\Event\Organizer;
use Kanboard\Core\Filter\FormatterInterface;
/**
@@ -117,16 +118,24 @@ class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterIn
$vEvent->setModified($dateModif);
$vEvent->setUseTimezone(true);
$vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']);
+ $vEvent->setDescription($task['description']);
+ $vEvent->setDescriptionHTML($this->helper->text->markdown($task['description']));
$vEvent->setUrl($this->helper->url->base().$this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
if (! empty($task['owner_id'])) {
- $vEvent->setOrganizer($task['assignee_name'] ?: $task['assignee_username'], $task['assignee_email']);
+ $attendees = new Attendees;
+ $attendees->add(
+ 'MAILTO:'.($task['assignee_email'] ?: $task['assignee_username'].'@kanboard.local'),
+ array('CN' => $task['assignee_name'] ?: $task['assignee_username'])
+ );
+ $vEvent->setAttendees($attendees);
}
if (! empty($task['creator_id'])) {
- $attendees = new Attendees;
- $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local'));
- $vEvent->setAttendees($attendees);
+ $vEvent->setOrganizer(new Organizer(
+ 'MAILTO:' . $task['creator_email'] ?: $task['creator_username'].'@kanboard.local',
+ array('CN' => $task['creator_name'] ?: $task['creator_username'])
+ ));
}
return $vEvent;
diff --git a/app/Helper/TaskHelper.php b/app/Helper/TaskHelper.php
index 58e9ce4f..e1d65cca 100644
--- a/app/Helper/TaskHelper.php
+++ b/app/Helper/TaskHelper.php
@@ -27,17 +27,17 @@ class TaskHelper extends Base
public function recurrenceTriggers()
{
- return $this->taskModel->getRecurrenceTriggerList();
+ return $this->taskRecurrenceModel->getRecurrenceTriggerList();
}
public function recurrenceTimeframes()
{
- return $this->taskModel->getRecurrenceTimeframeList();
+ return $this->taskRecurrenceModel->getRecurrenceTimeframeList();
}
public function recurrenceBasedates()
{
- return $this->taskModel->getRecurrenceBasedateList();
+ return $this->taskRecurrenceModel->getRecurrenceBasedateList();
}
public function selectTitle(array $values, array $errors)
@@ -70,6 +70,7 @@ class TaskHelper extends Base
$options = $this->tagModel->getAssignableList($project['id']);
$html = $this->helper->form->label(t('Tags'), 'tags[]');
+ $html .= '<input type="hidden" name="tags[]" value="">';
$html .= '<select name="tags[]" id="form-tags" class="tag-autocomplete" multiple>';
foreach ($options as $tag) {
@@ -167,10 +168,20 @@ class TaskHelper extends Base
return $html;
}
- public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array())
+ public function selectReference(array $values, array $errors = array(), array $attributes = array())
{
$attributes = array_merge(array('tabindex="9"'), $attributes);
+ $html = $this->helper->form->label(t('Reference'), 'reference');
+ $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small');
+
+ return $html;
+ }
+
+ public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="10"'), $attributes);
+
$html = $this->helper->form->label(t('Original estimate'), 'time_estimated');
$html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes);
$html .= ' '.t('hours');
@@ -180,7 +191,7 @@ class TaskHelper extends Base
public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array())
{
- $attributes = array_merge(array('tabindex="10"'), $attributes);
+ $attributes = array_merge(array('tabindex="11"'), $attributes);
$html = $this->helper->form->label(t('Time spent'), 'time_spent');
$html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes);
@@ -192,7 +203,7 @@ class TaskHelper extends Base
public function selectStartDate(array $values, array $errors = array(), array $attributes = array())
{
$placeholder = date($this->configModel->get('application_date_format', 'm/d/Y H:i'));
- $attributes = array_merge(array('tabindex="11"', 'placeholder="'.$placeholder.'"'), $attributes);
+ $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes);
$html = $this->helper->form->label(t('Start Date'), 'date_started');
$html .= $this->helper->form->text('date_started', $values, $errors, $attributes, 'form-datetime');
@@ -203,7 +214,7 @@ class TaskHelper extends Base
public function selectDueDate(array $values, array $errors = array(), array $attributes = array())
{
$placeholder = date($this->configModel->get('application_date_format', 'm/d/Y'));
- $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes);
+ $attributes = array_merge(array('tabindex="13"', 'placeholder="'.$placeholder.'"'), $attributes);
$html = $this->helper->form->label(t('Due Date'), 'date_due');
$html .= $this->helper->form->text('date_due', $values, $errors, $attributes, 'form-date');
diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php
index ae3efe1d..ab259a62 100644
--- a/app/Helper/UserHelper.php
+++ b/app/Helper/UserHelper.php
@@ -107,6 +107,10 @@ class UserHelper extends Base
*/
public function hasAccess($controller, $action)
{
+ if (! $this->userSession->isLogged()) {
+ return false;
+ }
+
$key = 'app_access:'.$controller.$action;
$result = $this->memoryCache->get($key);
@@ -128,6 +132,10 @@ class UserHelper extends Base
*/
public function hasProjectAccess($controller, $action, $project_id)
{
+ if (! $this->userSession->isLogged()) {
+ return false;
+ }
+
if ($this->userSession->isAdmin()) {
return true;
}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index 5a60829f..5f513347 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index f0fce806..1c28f4f9 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -100,7 +100,7 @@ return array(
'There is nobody assigned' => 'Není přiřazeno žádnému uživateli',
'Column on the board:' => 'Sloupec:',
'Close this task' => 'Uzavřít úkol',
- 'Open this task' => 'Aufgabe wieder öffnen',
+ 'Open this task' => 'Otevřít tento úkol',
'There is no description.' => 'Bez popisu',
'Add a new task' => 'Přidat nový úkol',
'The username is required' => 'Uživatelské jméno je vyžadováno',
@@ -393,8 +393,8 @@ return array(
'Default values are "%s"' => 'Standardní hodnoty jsou: "%s"',
'Default columns for new projects (Comma-separated)' => 'Výchozí sloupce pro nové projekty (odděleny čárkou)',
'Task assignee change' => 'Změna přiřazení uživatelů',
- '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s',
- '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s',
+ '%s change the assignee of the task #%d to %s' => '%s změnil přidělení úkolu #%d na uživatele %s',
+ '%s changed the assignee of the task %s to %s' => '%s změnil přidělení úkolu %s na uživatele %s',
'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"',
'Choose an event' => 'Vybrat událost',
'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem',
@@ -457,13 +457,13 @@ return array(
'The project id must be an integer' => 'ID projektu musí být celé číslo',
'The status must be an integer' => 'Status musí být celé číslo',
'The subtask id is required' => 'Je požadováno id dílčího úkolu',
- 'The subtask id must be an integer' => 'Die Teilaufgabenid muss eine ganze Zahl sein',
- 'The task id is required' => 'Die Aufgabenid ist benötigt',
- 'The task id must be an integer' => 'Die Aufgabenid muss eine ganze Zahl sein',
- 'The user id must be an integer' => 'Die Userid muss eine ganze Zahl sein',
- 'This value is required' => 'Dieser Wert ist erforderlich',
- 'This value must be numeric' => 'Dieser Wert muss numerisch sein',
- 'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden',
+ 'The subtask id must be an integer' => 'ID dílčího úkolu musí být číslo',
+ 'The task id is required' => 'ID úkolu je povinné',
+ 'The task id must be an integer' => 'ID úkolu musí být číslo',
+ 'The user id must be an integer' => 'ID uživatele musí být číslo',
+ 'This value is required' => 'Hodnota je povinná',
+ 'This value must be numeric' => 'Hodnota musí být číselná',
+ 'Unable to create this task.' => 'Nelze vytvořit tento úkol',
'Cumulative flow diagram' => 'Kumulativní diagram',
'Cumulative flow diagram for "%s"' => 'Kumulativní diagram pro "%s"',
'Daily project summary' => 'Denní přehledy',
@@ -471,26 +471,26 @@ return array(
'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"',
'Exports' => 'Exporty',
'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.',
- 'Active swimlanes' => 'Aktive Swimlane',
- 'Add a new swimlane' => 'Přidat nový řádek',
- 'Change default swimlane' => 'Standard Swimlane ändern',
- 'Default swimlane' => 'Výchozí Swimlane',
- 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?',
- 'Inactive swimlanes' => 'Inaktive Swimlane',
- 'Remove a swimlane' => 'Odstranit swimlane',
- 'Show default swimlane' => 'Standard Swimlane anzeigen',
- 'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "%s"',
- 'Swimlane removed successfully.' => 'Swimlane erfolgreich entfernt.',
- 'Swimlanes' => 'Swimlanes',
- 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.',
- 'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.',
- 'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.',
- 'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.',
- 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.',
- 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"',
+ 'Active swimlanes' => 'Aktivní dráhy',
+ 'Add a new swimlane' => 'Přidat novou dráhu',
+ 'Change default swimlane' => 'Změnit výchozí dráhu',
+ 'Default swimlane' => 'Výchozí dráha',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Opravdu si přejete odstranit tuto dráhu: "%s"?',
+ 'Inactive swimlanes' => 'Neaktivní dráha',
+ 'Remove a swimlane' => 'Odstranit dráhu',
+ 'Show default swimlane' => 'Zobrazit výchozí dráhu',
+ 'Swimlane modification for the project "%s"' => 'Změny dráhy pro projekt "%s"',
+ 'Swimlane removed successfully.' => 'Dráha byla odstraněna.',
+ 'Swimlanes' => 'Dráhy',
+ 'Swimlane updated successfully.' => 'Dráha byla upravena.',
+ 'The default swimlane have been updated successfully.' => 'Výchozí dráha byla upravena',
+ 'Unable to remove this swimlane.' => 'Tuto dráhu nelze odstranit.',
+ 'Unable to update this swimlane.' => 'Tuto dráhu nelze upravit.',
+ 'Your swimlane have been created successfully.' => 'Dráha byla vytvořena.',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Například: "Chyba", "Nápad", "Požadavek"...',
'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)',
'Integrations' => 'Integrace',
- 'Integration with third-party services' => 'Integration von Fremdleistungen',
+ 'Integration with third-party services' => 'Integrace se službami třetích stran',
'Subtask Id' => 'Dílčí úkol Id',
'Subtasks' => 'Dílčí úkoly',
'Subtasks Export' => 'Export dílčích úkolů',
@@ -510,7 +510,7 @@ return array(
'User dashboard' => 'Nástěnka uživatele',
'Allow only one subtask in progress at the same time for a user' => 'Umožnit uživateli práci pouze na jednom dílčím úkolu ve stejném čase',
'Edit column "%s"' => 'Upravit sloupec "%s" ',
- 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"',
+ 'Select the new status of the subtask: "%s"' => 'Vyberte nový stav pro podúkol: "%s"',
'Subtask timesheet' => 'Časový rozvrh dílčích úkolů',
'There is nothing to show.' => 'Žádná položka k zobrazení',
'Time Tracking' => 'Sledování času',
@@ -523,9 +523,9 @@ return array(
'Days in this column' => 'Dní v tomto sloupci',
'%dd' => '%d d',
'Add a new link' => 'Přidat nový odkaz',
- 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
- 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
- 'Field required' => 'Feld erforderlich',
+ 'Do you really want to remove this link: "%s"?' => 'Opravdu chcete odstranit odkaz "%s"?',
+ 'Do you really want to remove this link with task #%d?' => 'Opravdu chcete odstranit odkaz na úkol #%d ?',
+ 'Field required' => 'Povinné pole',
'Link added successfully.' => 'Propojení bylo úspěšně přidáno.',
'Link updated successfully.' => 'Propojení bylo úspěšně aktualizováno.',
'Link removed successfully.' => 'Propojení bylo úspěšně odebráno.',
@@ -549,8 +549,8 @@ return array(
'is duplicated by' => 'je duplikován',
'is a child of' => 'je podřízený',
'is a parent of' => 'je nadřízený',
- 'targets milestone' => 'targets milestone',
- 'is a milestone of' => 'is a milestone of',
+ 'targets milestone' => 'patří k milníku',
+ 'is a milestone of' => 'je milníkem',
'fixes' => 'nahrazuje',
'is fixed by' => 'je nahrazen',
'This task' => 'Tento úkol',
@@ -592,7 +592,7 @@ return array(
'Time spent in the column' => 'Trvání jednotlivých etap',
'Task transitions' => 'Přesuny úkolů',
'Task transitions export' => 'Export přesunů mezi sloupci',
- 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Tento seznam obsahuje všechny pohyby úkolů s daty, uživateli a časy strávenými na úkolu.',
'Currency rates' => 'Aktuální kurzy',
'Rate' => 'Kurz',
'Change reference currency' => 'Změnit referenční měnu',
@@ -604,28 +604,28 @@ return array(
'%s remove the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ',
'Enable Gravatar images' => 'Aktiviere Gravatar Bilder',
'Information' => 'Informace',
- 'Check two factor authentication code' => 'Prüfe Zwei-Faktor-Authentifizierungscode',
- 'The two factor authentication code is not valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist ungültig.',
- 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.',
- 'Code' => 'Code',
+ 'Check two factor authentication code' => 'Zkontrolujte dvouúrovňový autentifikační klíč',
+ 'The two factor authentication code is not valid.' => 'Dvouúrovňový autentifikační klíč není platný.',
+ 'The two factor authentication code is valid.' => 'Dvouúrovňový autentifikační klíč je platný.',
+ 'Code' => 'Klíč',
'Two factor authentication' => 'Dvouúrovňová autorizace',
- 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI',
+ 'This QR code contains the key URI: ' => 'Tento QR kód obsahuje adresu s klíčem',
'Check my code' => 'Kontrola mého kódu',
'Secret key: ' => 'Tajný klíč',
'Test your device' => 'Test Vašeho zařízení',
'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce',
'%s via Kanboard' => '%s via Kanboard',
- 'Burndown chart for "%s"' => 'Burndown-Chart für "%s"',
+ 'Burndown chart for "%s"' => 'Burndown-Chart pro "%s"',
'Burndown chart' => 'Burndown-Chart',
'This chart show the task complexity over the time (Work Remaining).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).',
'Screenshot taken %s' => 'Screenshot aufgenommen %s ',
'Add a screenshot' => 'Přidat snímek obrazovky',
- 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Nimm einen Screenshot auf und drücke STRG+V oder ⌘+V um ihn hier einzufügen.',
- 'Screenshot uploaded successfully.' => 'Screenshot erfolgreich hochgeladen.',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Pořiďte snímek obrazovky a v tomto poli stiskněte Ctrl+V nebo ⌘+V ',
+ 'Screenshot uploaded successfully.' => 'Snímek obrazovky byl úspěšně nahrán.',
'SEK - Swedish Krona' => 'SEK - Schwedische Kronen',
- 'Identifier' => 'Identifikator',
- 'Disable two factor authentication' => 'Zrušit dvou stupňovou autorizaci',
- 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?',
+ 'Identifier' => 'Identifikátor',
+ 'Disable two factor authentication' => 'Zrušit dvouúrovňovou autorizaci',
+ 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Opravdu chcete vypnout dvouúrovňovou autentifikaci pro uživatele: "%s"?',
// 'Edit link' => '',
// 'Start to type task title...' => '',
// 'A task cannot be linked to itself' => '',
@@ -680,27 +680,27 @@ return array(
'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie',
'Send a task by email to someone' => 'Poslat někomu úkol poštou',
'Reopen a task' => 'Znovu otevřít úkol',
- 'Column change' => 'Spalte geändert',
- 'Position change' => 'Position geändert',
- 'Swimlane change' => 'Swimlane geändert',
- 'Assignee change' => 'Zuordnung geändert',
- '[%s] Overdue tasks' => '[%s] überfallige Aufgaben',
- 'Notification' => 'Benachrichtigungen',
- '%s moved the task #%d to the first swimlane' => '%s hat die Aufgabe #%d in die erste Swimlane verschoben',
- '%s moved the task #%d to the swimlane "%s"' => '%s hat die Aufgabe #%d in die Swimlane "%s" verschoben',
+ 'Column change' => 'Změna sloupce',
+ 'Position change' => 'Změna pozice',
+ 'Swimlane change' => 'Změna dráhy',
+ 'Assignee change' => 'Změna přidělení',
+ '[%s] Overdue tasks' => '[%s] přetažených úkolů',
+ 'Notification' => 'Upozornění',
+ '%s moved the task #%d to the first swimlane' => '%s přesunul úkol #%d do první dráhy',
+ '%s moved the task #%d to the swimlane "%s"' => '%s přesunul úkol #%d do dráhy "%s"',
// 'Swimlane' => '',
// 'Gravatar' => '',
- '%s moved the task %s to the first swimlane' => '%s hat die Aufgabe %s in die erste Swimlane verschoben',
- '%s moved the task %s to the swimlane "%s"' => '%s hat die Aufgaben %s in die Swimlane "%s" verschoben',
+ '%s moved the task %s to the first swimlane' => '%s přesunul úkol %s do první dráhy',
+ '%s moved the task %s to the swimlane "%s"' => '%s přesunul úkol %s do dráhy "%s"',
'This report contains all subtasks information for the given date range.' => 'Report obsahuje všechny informace o dílčích úkolech pro daný časový úsek',
'This report contains all tasks information for the given date range.' => 'Report obsahuje informace o všech úkolech pro daný časový úsek.',
'Project activities for %s' => 'Aktivity projektu %s',
- 'view the board on Kanboard' => 'Pinnwand in Kanboard anzeigen',
- 'The task have been moved to the first swimlane' => 'Die Aufgabe wurde in die erste Swimlane verschoben',
- 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in ene andere Swimlane verschoben',
- 'New title: %s' => 'Neuer Titel: %s',
- 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen',
- 'New assignee: %s' => 'Neue Zuordnung: %s',
+ 'view the board on Kanboard' => 'Zobrazit nástěnku',
+ 'The task have been moved to the first swimlane' => 'Úkol byl přesunut do první dráhy',
+ 'The task have been moved to another swimlane:' => 'Úkol byl přesunut do další dráhy',
+ 'New title: %s' => 'Nový název: %s',
+ 'The task is not assigned anymore' => 'Úkol již není přidělen',
+ 'New assignee: %s' => 'přidělení: %s',
'There is no category now' => 'Nyní neexistuje žádná kategorie',
'New category: %s' => 'Nová kategorie: %s',
'New color: %s' => 'Nová barva: %s',
@@ -708,11 +708,11 @@ return array(
'The due date have been removed' => 'Datum dokončení byl odstraněn',
'There is no description anymore' => 'Ještě neexistuje žádný popis',
'Recurrence settings have been modified' => 'Nastavení opakování bylo změněno',
- 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh',
- 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh',
- 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert',
- 'The description has been modified:' => 'Die Beschreibung wurde geändert:',
- 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)',
+ 'Time spent changed: %sh' => 'Strávený čas se změnil: %sh',
+ 'Time estimated changed: %sh' => 'Odhadovaný čas se změnil: %sh',
+ 'The field "%s" have been updated' => 'Sloupec "%s" byl upraven',
+ 'The description has been modified:' => 'Popis byl upraven:',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => 'Opravdu si přejete úkol "%s" uzavřít? (včetně podúkolů)',
'I want to receive notifications for:' => 'Chci dostávat upozornění na:',
'All tasks' => 'Všechny úkoly',
'Only for tasks assigned to me' => 'pouze pro moje úkoly',
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index afe14d7f..abebd394 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 232b9f56..f569206b 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1196,23 +1196,25 @@ return array(
'Email transport' => 'E-Mail Verkehr',
'Webhook token' => 'Webhook Token',
'Imports' => 'Importe',
- // 'Project tags management' => '',
- // 'Tag created successfully.' => '',
- // 'Unable to create this tag.' => '',
- // 'Tag updated successfully.' => '',
- // 'Unable to update this tag.' => '',
- // 'Tag removed successfully.' => '',
- // 'Unable to remove this tag.' => '',
- // 'Global tags management' => '',
- // 'Tags' => '',
- // 'Tags management' => '',
- // 'Add new tag' => '',
- // 'Edit a tag' => '',
- // 'Project tags' => '',
- // 'There is no specific tag for this project at the moment.' => '',
- // 'Tag' => '',
- // 'Remove a tag' => '',
- // 'Do you really want to remove this tag: "%s"?' => '',
- // 'Global tags' => '',
- // 'There is no global tag at the moment.' => '',
+ 'Project tags management' => 'Projektbezogenes Schlagwort-Management',
+ 'Tag created successfully.' => 'Schlagwort erfolgreich erstellt.',
+ 'Unable to create this tag.' => 'Das Schlagwort kann nicht erstellt werden.',
+ 'Tag updated successfully.' => 'Schlagwort erfolgreich aktualisiert.',
+ 'Unable to update this tag.' => 'Das Schlagwort kann nicht aktualisiert werden.',
+ 'Tag removed successfully.' => 'Schlagwort erfolgreich entfernt.',
+ 'Unable to remove this tag.' => 'Das Schlagwort kann nicht entfernt werden.',
+ 'Global tags management' => 'Globales Schlagwort-Management',
+ 'Tags' => 'Schlagworte',
+ 'Tags management' => 'Schlagwort-Management',
+ 'Add new tag' => 'Neues Schlagwort hinzufügen',
+ 'Edit a tag' => 'Schlagwort bearbeiten',
+ 'Project tags' => 'Projektbezogene Schlagwörter',
+ 'There is no specific tag for this project at the moment.' => 'Es gibt zur Zeit kein spezifisches Schlagwort.',
+ 'Tag' => 'Schlagwort',
+ 'Remove a tag' => 'Schlagwort entfernen',
+ 'Do you really want to remove this tag: "%s"?' => 'Soll dieses Schlagwort wirklich entfernt werden: "%s"?',
+ 'Global tags' => 'Globale Schlagwörter',
+ 'There is no global tag at the moment.' => 'Es gibt zur Zeit kein globales Schlagwort',
+ 'This field cannot be empty' => 'Dieses Feld kann nicht leer sein',
+ 'Hide tasks in this column in the dashboard' => 'Aufgaben in dieser Spalte im Dashboard ausblenden',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index 6454af12..c1d7c579 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index 6ba86f8f..5699ce6f 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index cb2a8d9a..6fe4852c 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index cac1c73a..7663da0f 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -944,7 +944,7 @@ return array(
'Project Manager' => 'Chef de projet',
'Project Member' => 'Membre du projet',
'Project Viewer' => 'Visualiseur de projet',
- 'Your account is locked for %d minutes' => 'Votre compte est vérouillé pour %d minutes',
+ 'Your account is locked for %d minutes' => 'Votre compte est verrouillé pour %d minutes',
'Invalid captcha' => 'Captcha invalid',
'The name must be unique' => 'Le nom doit être unique',
'View all groups' => 'Voir tous les groupes',
@@ -1216,4 +1216,6 @@ return array(
'Do you really want to remove this tag: "%s"?' => 'Voulez-vous vraiment supprimer ce libellé : « %s » ?',
'Global tags' => 'Libellés globaux',
'There is no global tag at the moment.' => 'Il n\'y a aucun libellé global pour le moment.',
+ 'This field cannot be empty' => 'Ce champ ne peut être vide',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index e032be66..96db72ef 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index 29d11b8d..2d6e5aa3 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index c5710a4e..e10b61da 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -1196,23 +1196,25 @@ return array(
'Email transport' => 'Trasporto Email',
// 'Webhook token' => '',
'Imports' => 'Importa',
- // 'Project tags management' => '',
- // 'Tag created successfully.' => '',
- // 'Unable to create this tag.' => '',
- // 'Tag updated successfully.' => '',
- // 'Unable to update this tag.' => '',
- // 'Tag removed successfully.' => '',
- // 'Unable to remove this tag.' => '',
- // 'Global tags management' => '',
- // 'Tags' => '',
- // 'Tags management' => '',
- // 'Add new tag' => '',
- // 'Edit a tag' => '',
- // 'Project tags' => '',
- // 'There is no specific tag for this project at the moment.' => '',
+ 'Project tags management' => 'Gestione tag di progetto',
+ 'Tag created successfully.' => 'Tag creato con successo.',
+ 'Unable to create this tag.' => 'Impossibile creare questo tag.',
+ 'Tag updated successfully.' => 'Tag aggiornato con successo.',
+ 'Unable to update this tag.' => 'Impossibile aggiornare questo tag.',
+ 'Tag removed successfully.' => 'Tag rimosso con successo.',
+ 'Unable to remove this tag.' => 'Impossibile rimuovere questo tag.',
+ 'Global tags management' => 'Gestione dei tag globali',
+ 'Tags' => 'Tag',
+ 'Tags management' => 'Gestione dei tag',
+ 'Add new tag' => 'Aggiungi un nuovo tag',
+ 'Edit a tag' => 'Modifica un tag',
+ 'Project tags' => 'Tag di progetto',
+ 'There is no specific tag for this project at the moment.' => 'Non è definito nessun tag specifico per questo progetto al momento.',
// 'Tag' => '',
- // 'Remove a tag' => '',
- // 'Do you really want to remove this tag: "%s"?' => '',
- // 'Global tags' => '',
- // 'There is no global tag at the moment.' => '',
+ 'Remove a tag' => 'Rimuovi un tag',
+ 'Do you really want to remove this tag: "%s"?' => 'Vuoi davvero rimuovere questo tag: "%s"?',
+ 'Global tags' => 'Tag globali',
+ 'There is no global tag at the moment.' => 'Non sono definiti tag globali al momento.',
+ 'This field cannot be empty' => 'Questo campo non può essere vuoto',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 589917f0..2fe13ac9 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
index 2f142b48..bd25d11f 100644
--- a/app/Locale/ko_KR/translations.php
+++ b/app/Locale/ko_KR/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index 02fa16f8..ff8960aa 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 6bb30642..8752a159 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 8dddbe7d..e07ea32c 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ //' Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 46c3b034..896d2ed4 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 6a0ebd65..40f3bb4d 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 42826240..08375ad0 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1196,23 +1196,25 @@ return array(
'Email transport' => 'Transportador de Email',
'Webhook token' => 'Token do Webhook',
'Imports' => 'Importados',
- // 'Project tags management' => '',
- // 'Tag created successfully.' => '',
- // 'Unable to create this tag.' => '',
- // 'Tag updated successfully.' => '',
- // 'Unable to update this tag.' => '',
- // 'Tag removed successfully.' => '',
- // 'Unable to remove this tag.' => '',
- // 'Global tags management' => '',
- // 'Tags' => '',
- // 'Tags management' => '',
- // 'Add new tag' => '',
- // 'Edit a tag' => '',
- // 'Project tags' => '',
- // 'There is no specific tag for this project at the moment.' => '',
- // 'Tag' => '',
- // 'Remove a tag' => '',
- // 'Do you really want to remove this tag: "%s"?' => '',
- // 'Global tags' => '',
- // 'There is no global tag at the moment.' => '',
+ 'Project tags management' => 'Gestão de etiquetas do Projecto',
+ 'Tag created successfully.' => 'Etiqueta criada com sucesso.',
+ 'Unable to create this tag.' => 'Não foi possivel criar esta etiqueta.',
+ 'Tag updated successfully.' => 'Etiqueta actualizada com sucesso.',
+ 'Unable to update this tag.' => 'Não foi possivel actualizar esta etiqueta.',
+ 'Tag removed successfully.' => 'Etiqueta removida com sucesso.',
+ 'Unable to remove this tag.' => 'Não foi possivel remover esta etiqueta.',
+ 'Global tags management' => 'Gestão de etiquetas globais',
+ 'Tags' => 'Etiquetas',
+ 'Tags management' => 'Gestão de Etiquetas',
+ 'Add new tag' => 'Adicionar etiqueta nova',
+ 'Edit a tag' => 'Editar a etiqueta',
+ 'Project tags' => 'Etiquetas do Projecto',
+ 'There is no specific tag for this project at the moment.' => 'De momento não existe nenhuma etiqueta para este projecto.',
+ 'Tag' => 'Etiqueta',
+ 'Remove a tag' => 'Remover etiqueta',
+ 'Do you really want to remove this tag: "%s"?' => 'Tem a certeza que pretende remover esta etiqueta: "%s"?',
+ 'Global tags' => 'Etiquetas globais',
+ 'There is no global tag at the moment.' => 'De momento não existe nenhuma etiqueta global.',
+ // 'This field cannot be empty' => '',
+ //'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index e7b73ef0..c6285f6a 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -715,9 +715,9 @@ return array(
'Do you really want to close the task "%s" as well as all subtasks?' => 'Вы действительно хотите закрыть задачу "%s", а также все подзадачи?',
'I want to receive notifications for:' => 'Я хочу получать уведомления для:',
'All tasks' => 'Все задачи',
- 'Only for tasks assigned to me' => 'Только для задач, назначенных на меня',
+ 'Only for tasks assigned to me' => 'Только для задач, назначенных мне',
'Only for tasks created by me' => 'Только для задач, созданных мной',
- 'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мной',
+ 'Only for tasks created by me and assigned to me' => 'Только для задач, созданных мной и назначенных мне',
'%%Y-%%m-%%d' => '%%Y-%%m-%%d',
'Total for all columns' => 'Суммарно для всех колонок',
'You need at least 2 days of data to show the chart.' => 'Для отображения диаграммы нужно по крайней мере 2 дня.',
@@ -1154,65 +1154,67 @@ return array(
'Projects where "%s" is member' => 'Проекты, где членом является "%s"',
'Open tasks assigned to "%s"' => 'Открытые задачи, назначенные на "%s"',
'Closed tasks assigned to "%s"' => 'Закрытые задачи, назначенные на "%s"',
- // 'Assign automatically a color based on a priority' => '',
- // 'Overdue tasks for the project(s) "%s"' => '',
- // 'Upload files' => '',
- // 'Installed Plugins' => '',
- // 'Plugin Directory' => '',
- // 'Plugin installed successfully.' => '',
- // 'Plugin updated successfully.' => '',
- // 'Plugin removed successfully.' => '',
- // 'Subtask converted to task successfully.' => '',
- // 'Unable to convert the subtask.' => '',
- // 'Unable to extract plugin archive.' => '',
- // 'Plugin not found.' => '',
- // 'You don\'t have the permission to remove this plugin.' => '',
- // 'Unable to download plugin archive.' => '',
- // 'Unable to write temporary file for plugin.' => '',
- // 'Unable to open plugin archive.' => '',
- // 'There is no file in the plugin archive.' => '',
- // 'Create tasks in bulk' => '',
- // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '',
- // 'There is no plugin available.' => '',
- // 'Install' => '',
- // 'Update' => '',
- // 'Up to date' => '',
- // 'Not available' => '',
- // 'Remove plugin' => '',
- // 'Do you really want to remove this plugin: "%s"?' => '',
- // 'Uninstall' => '',
- // 'Listing' => '',
- // 'Metadata' => '',
- // 'Manage projects' => '',
- // 'Convert to task' => '',
- // 'Convert sub-task to task' => '',
- // 'Do you really want to convert this sub-task to a task?' => '',
- // 'My task title' => '',
- // 'Enter one task by line.' => '',
- // 'Number of failed login:' => '',
- // 'Account locked until:' => '',
- // 'Email settings' => '',
- // 'Email sender address' => '',
- // 'Email transport' => '',
+ 'Assign automatically a color based on a priority' => 'Автоматически назначить цвет в зависимости от категории',
+ 'Overdue tasks for the project(s) "%s"' => 'Просроченные задачи для проекта(ов) "%s"',
+ 'Upload files' => 'Загрузить файлы',
+ 'Installed Plugins' => 'Установленные плагины',
+ 'Plugin Directory' => 'Доступные плагины',
+ 'Plugin installed successfully.' => 'Плагин успешно установлен.',
+ 'Plugin updated successfully.' => 'Плагин успешно обновлен.',
+ 'Plugin removed successfully.' => 'Плагин успешно удален.',
+ 'Subtask converted to task successfully.' => 'Подзадача успешно преобразована в задачу.',
+ 'Unable to convert the subtask.' => 'Невозможно преобразовать подзадачу.',
+ 'Unable to extract plugin archive.' => 'Невозможно распаковать архив с плагином.',
+ 'Plugin not found.' => 'Плагин не найден.',
+ 'You don\'t have the permission to remove this plugin.' => 'У Вас нет прав на удаление этого плагина.',
+ 'Unable to download plugin archive.' => 'Невозможно загрузить архив с плагином.',
+ 'Unable to write temporary file for plugin.' => 'Невозможно записать временный файл для плагина.',
+ 'Unable to open plugin archive.' => 'Невозможно открыть архив плагина.',
+ 'There is no file in the plugin archive.' => 'В арзиве плагина нет файлов.',
+ 'Create tasks in bulk' => 'Массовое создание задач',
+ 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Ваш Kanboard не сконфигурирова для установки плагинов через пользовательский интерфейс.',
+ 'There is no plugin available.' => 'Нет доступных плагинов.',
+ 'Install' => 'Установить',
+ 'Update' => 'Обновить',
+ 'Up to date' => 'Самый новый',
+ 'Not available' => 'Недоступен',
+ 'Remove plugin' => 'Удалить плагин',
+ 'Do you really want to remove this plugin: "%s"?' => 'Вы действительно хотите удалить плагин: "%s"?',
+ 'Uninstall' => 'Деинсталлировать',
+ 'Listing' => 'Список',
+ 'Metadata' => 'Метаданные',
+ 'Manage projects' => 'Управление проектами',
+ 'Convert to task' => 'Преобразовать в задачу',
+ 'Convert sub-task to task' => 'Преобразовать подзадачу в задачу',
+ 'Do you really want to convert this sub-task to a task?' => 'Вы действительно хотите преобразовать эту подзадачу в задачу?',
+ 'My task title' => 'Заголовок задачи',
+ 'Enter one task by line.' => 'Указывайте одну задачу на строке',
+ 'Number of failed login:' => 'Число неудачных попыток входа:',
+ 'Account locked until:' => 'Аккаунт заблокирован до:',
+ 'Email settings' => 'Настройки почты',
+ 'Email sender address' => 'Адрес отправителя',
+ 'Email transport' => 'Почтовый транспорт',
// 'Webhook token' => '',
// 'Imports' => '',
- // 'Project tags management' => '',
- // 'Tag created successfully.' => '',
- // 'Unable to create this tag.' => '',
- // 'Tag updated successfully.' => '',
- // 'Unable to update this tag.' => '',
- // 'Tag removed successfully.' => '',
- // 'Unable to remove this tag.' => '',
- // 'Global tags management' => '',
- // 'Tags' => '',
- // 'Tags management' => '',
- // 'Add new tag' => '',
- // 'Edit a tag' => '',
- // 'Project tags' => '',
- // 'There is no specific tag for this project at the moment.' => '',
- // 'Tag' => '',
- // 'Remove a tag' => '',
- // 'Do you really want to remove this tag: "%s"?' => '',
- // 'Global tags' => '',
- // 'There is no global tag at the moment.' => '',
+ 'Project tags management' => 'Управление метками проекта',
+ 'Tag created successfully.' => 'Метка успешно создана.',
+ 'Unable to create this tag.' => 'Невозможно создать эту метку.',
+ 'Tag updated successfully.' => 'Метак успешно обновлена.',
+ 'Unable to update this tag.' => 'Невозможно обновить эту метку.',
+ 'Tag removed successfully.' => 'Метка успешно удалена.',
+ 'Unable to remove this tag.' => 'Невозможно удалить эту метку.',
+ 'Global tags management' => 'Управление глоабльными метками',
+ 'Tags' => 'Метки',
+ 'Tags management' => 'Управление метками',
+ 'Add new tag' => 'Добавить новую метку',
+ 'Edit a tag' => 'Редактировать метку',
+ 'Project tags' => 'Метки проекта',
+ 'There is no specific tag for this project at the moment.' => 'Нет меток для этого проекта.',
+ 'Tag' => 'Метка',
+ 'Remove a tag' => 'Удалить метку',
+ 'Do you really want to remove this tag: "%s"?' => 'Вы действительно хотите удалить метку: "%s"?',
+ 'Global tags' => 'Глобальные метка',
+ 'There is no global tag at the moment.' => 'Нет глобальных меток.',
+ 'This field cannot be empty' => 'Это поле не может быть пустым',
+ 'Hide tasks in this column in the dashboard' => 'Не показывать задачи из этой колонки в кабинете',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 96a463d1..92ed3424 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index cdfb36f6..eedcf0fc 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index fd59c003..a6de8bce 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index ab888266..35e29649 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index cafb8c55..0ef01ef7 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1215,4 +1215,6 @@ return array(
// 'Do you really want to remove this tag: "%s"?' => '',
// 'Global tags' => '',
// 'There is no global tag at the moment.' => '',
+ // 'This field cannot be empty' => '',
+ // 'Hide tasks in this column in the dashboard' => '',
);
diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php
index 795fe692..5498ef54 100644
--- a/app/Model/ColumnModel.php
+++ b/app/Model/ColumnModel.php
@@ -138,19 +138,21 @@ class ColumnModel extends Base
* Add a new column to the board
*
* @access public
- * @param integer $project_id Project id
- * @param string $title Column title
- * @param integer $task_limit Task limit
- * @param string $description Column description
- * @return boolean|integer
+ * @param integer $project_id Project id
+ * @param string $title Column title
+ * @param integer $task_limit Task limit
+ * @param string $description Column description
+ * @param integer $hide_in_dashboard
+ * @return bool|int
*/
- public function create($project_id, $title, $task_limit = 0, $description = '')
+ public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0)
{
$values = array(
'project_id' => $project_id,
'title' => $title,
'task_limit' => intval($task_limit),
'position' => $this->getLastColumnPosition($project_id) + 1,
+ 'hide_in_dashboard' => $hide_in_dashboard,
'description' => $description,
);
@@ -165,13 +167,15 @@ class ColumnModel extends Base
* @param string $title Column title
* @param integer $task_limit Task limit
* @param string $description Optional description
+ * @param integer $hide_in_dashboard
* @return boolean
*/
- public function update($column_id, $title, $task_limit = 0, $description = '')
+ public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
'title' => $title,
'task_limit' => intval($task_limit),
+ 'hide_in_dashboard' => $hide_in_dashboard,
'description' => $description,
));
}
diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php
index 8937b77e..4d697b5e 100644
--- a/app/Model/NotificationModel.php
+++ b/app/Model/NotificationModel.php
@@ -133,4 +133,41 @@ class NotificationModel extends Base
return e('Notification');
}
}
+
+ /**
+ * Get task id from event
+ *
+ * @access public
+ * @param string $event_name
+ * @param array $event_data
+ * @return integer
+ */
+ public function getTaskIdFromEvent($event_name, array $event_data)
+ {
+ switch ($event_name) {
+ case TaskFileModel::EVENT_CREATE:
+ return $event_data['file']['task_id'];
+ case CommentModel::EVENT_CREATE:
+ case CommentModel::EVENT_UPDATE:
+ return $event_data['comment']['task_id'];
+ case SubtaskModel::EVENT_CREATE:
+ case SubtaskModel::EVENT_UPDATE:
+ return $event_data['subtask']['task_id'];
+ case TaskModel::EVENT_CREATE:
+ case TaskModel::EVENT_UPDATE:
+ case TaskModel::EVENT_CLOSE:
+ case TaskModel::EVENT_OPEN:
+ case TaskModel::EVENT_MOVE_COLUMN:
+ case TaskModel::EVENT_MOVE_POSITION:
+ case TaskModel::EVENT_MOVE_SWIMLANE:
+ case TaskModel::EVENT_ASSIGNEE_CHANGE:
+ case CommentModel::EVENT_USER_MENTION:
+ case TaskModel::EVENT_USER_MENTION:
+ return $event_data['task']['id'];
+ case TaskModel::EVENT_OVERDUE:
+ return $event_data['tasks'][0]['id'];
+ default:
+ return 0;
+ }
+ }
}
diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php
index b67f8302..94b83c80 100644
--- a/app/Model/ProjectDuplicationModel.php
+++ b/app/Model/ProjectDuplicationModel.php
@@ -22,7 +22,15 @@ class ProjectDuplicationModel extends Base
*/
public function getOptionalSelection()
{
- return array('categoryModel', 'projectPermissionModel', 'actionModel', 'swimlaneModel', 'taskModel', 'projectMetadataModel');
+ return array(
+ 'categoryModel',
+ 'projectPermissionModel',
+ 'actionModel',
+ 'swimlaneModel',
+ 'tagDuplicationModel',
+ 'projectMetadataModel',
+ 'projectTaskDuplicationModel',
+ );
}
/**
@@ -33,7 +41,16 @@ class ProjectDuplicationModel extends Base
*/
public function getPossibleSelection()
{
- return array('boardModel', 'categoryModel', 'projectPermissionModel', 'actionModel', 'swimlaneModel', 'taskModel', 'projectMetadataModel');
+ return array(
+ 'boardModel',
+ 'categoryModel',
+ 'projectPermissionModel',
+ 'actionModel',
+ 'swimlaneModel',
+ 'tagDuplicationModel',
+ 'projectMetadataModel',
+ 'projectTaskDuplicationModel',
+ );
}
/**
@@ -129,6 +146,9 @@ class ProjectDuplicationModel extends Base
'is_public' => 0,
'is_private' => $private ? 1 : $is_private,
'owner_id' => $owner_id,
+ 'priority_default' => $project['priority_default'],
+ 'priority_start' => $project['priority_start'],
+ 'priority_end' => $project['priority_end'],
);
if (! $this->db->table(ProjectModel::TABLE)->save($values)) {
diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php
index 7382537e..850531c9 100644
--- a/app/Model/ProjectModel.php
+++ b/app/Model/ProjectModel.php
@@ -246,19 +246,6 @@ class ProjectModel extends Base
}
/**
- * Get Priority range from a project
- *
- * @access public
- * @param array $project
- * @return array
- */
- public function getPriorities(array $project)
- {
- $range = range($project['priority_start'], $project['priority_end']);
- return array_combine($range, $range);
- }
-
- /**
* Gather some task metrics for a given project
*
* @access public
diff --git a/app/Model/ProjectTaskDuplicationModel.php b/app/Model/ProjectTaskDuplicationModel.php
new file mode 100644
index 00000000..5d2e1322
--- /dev/null
+++ b/app/Model/ProjectTaskDuplicationModel.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Base;
+
+/**
+ * Project Task Duplication Model
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class ProjectTaskDuplicationModel extends Base
+{
+ /**
+ * Duplicate all tasks to another project
+ *
+ * @access public
+ * @param integer $src_project_id
+ * @param integer $dst_project_id
+ * @return boolean
+ */
+ public function duplicate($src_project_id, $dst_project_id)
+ {
+ $task_ids = $this->taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED));
+
+ foreach ($task_ids as $task_id) {
+ if (! $this->taskProjectDuplicationModel->duplicateToProject($task_id, $dst_project_id)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectTaskPriorityModel.php b/app/Model/ProjectTaskPriorityModel.php
new file mode 100644
index 00000000..c1a0257a
--- /dev/null
+++ b/app/Model/ProjectTaskPriorityModel.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Base;
+
+/**
+ * Project Task Priority Model
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class ProjectTaskPriorityModel extends Base
+{
+ /**
+ * Get Priority range from a project
+ *
+ * @access public
+ * @param array $project
+ * @return array
+ */
+ public function getPriorities(array $project)
+ {
+ $range = range($project['priority_start'], $project['priority_end']);
+ return array_combine($range, $range);
+ }
+
+ /**
+ * Get task priority settings
+ *
+ * @access public
+ * @param int $project_id
+ * @return array|null
+ */
+ public function getPrioritySettings($project_id)
+ {
+ return $this->db
+ ->table(ProjectModel::TABLE)
+ ->columns('priority_default', 'priority_start', 'priority_end')
+ ->eq('id', $project_id)
+ ->findOne();
+ }
+
+ /**
+ * Get default task priority
+ *
+ * @access public
+ * @param int $project_id
+ * @return int
+ */
+ public function getDefaultPriority($project_id)
+ {
+ return $this->db->table(ProjectModel::TABLE)->eq('id', $project_id)->findOneColumn('priority_default') ?: 0;
+ }
+
+ /**
+ * Get priority for a destination project
+ *
+ * @access public
+ * @param integer $dst_project_id
+ * @param integer $priority
+ * @return integer
+ */
+ public function getPriorityForProject($dst_project_id, $priority)
+ {
+ $settings = $this->getPrioritySettings($dst_project_id);
+
+ if ($priority >= $settings['priority_start'] && $priority <= $settings['priority_end']) {
+ return $priority;
+ }
+
+ return $settings['priority_default'];
+ }
+}
diff --git a/app/Model/SwimlaneModel.php b/app/Model/SwimlaneModel.php
index 35e39879..f20bfa2f 100644
--- a/app/Model/SwimlaneModel.php
+++ b/app/Model/SwimlaneModel.php
@@ -94,15 +94,17 @@ class SwimlaneModel extends Base
*
* @access public
* @param integer $project_id
- * @return array
+ * @return array|null
*/
public function getFirstActiveSwimlane($project_id)
{
- return $this->db->table(self::TABLE)
- ->eq('is_active', self::ACTIVE)
- ->eq('project_id', $project_id)
- ->orderBy('position', 'asc')
- ->findOne();
+ $swimlanes = $this->getSwimlanes($project_id);
+
+ if (empty($swimlanes)) {
+ return null;
+ }
+
+ return $swimlanes[0];
}
/**
@@ -184,18 +186,18 @@ class SwimlaneModel extends Base
->orderBy('position', 'asc')
->findAll();
- $default_swimlane = $this->db
+ $defaultSwimlane = $this->db
->table(ProjectModel::TABLE)
->eq('id', $project_id)
->eq('show_default_swimlane', 1)
->findOneColumn('default_swimlane');
- if ($default_swimlane) {
- if ($default_swimlane === 'Default swimlane') {
- $default_swimlane = t($default_swimlane);
+ if ($defaultSwimlane) {
+ if ($defaultSwimlane === 'Default swimlane') {
+ $defaultSwimlane = t($defaultSwimlane);
}
- array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane));
+ array_unshift($swimlanes, array('id' => 0, 'name' => $defaultSwimlane));
}
return $swimlanes;
diff --git a/app/Model/TagDuplicationModel.php b/app/Model/TagDuplicationModel.php
new file mode 100644
index 00000000..fb0d8170
--- /dev/null
+++ b/app/Model/TagDuplicationModel.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Base;
+
+/**
+ * Tag Duplication
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class TagDuplicationModel extends Base
+{
+ /**
+ * Duplicate project tags to another project
+ *
+ * @access public
+ * @param integer $src_project_id
+ * @param integer $dst_project_id
+ * @return bool
+ */
+ public function duplicate($src_project_id, $dst_project_id)
+ {
+ $tags = $this->tagModel->getAllByProject($src_project_id);
+ $results = array();
+
+ foreach ($tags as $tag) {
+ $results[] = $this->tagModel->create($dst_project_id, $tag['name']);
+ }
+
+ return ! in_array(false, $results, true);
+ }
+
+ /**
+ * Link tags to the new tasks
+ *
+ * @access public
+ * @param integer $src_task_id
+ * @param integer $dst_task_id
+ * @param integer $dst_project_id
+ */
+ public function duplicateTaskTagsToAnotherProject($src_task_id, $dst_task_id, $dst_project_id)
+ {
+ $tags = $this->taskTagModel->getTagsByTask($src_task_id);
+
+ foreach ($tags as $tag) {
+ $tag_id = $this->tagModel->getIdByName($dst_project_id, $tag['name']);
+
+ if ($tag_id) {
+ $this->taskTagModel->associateTag($dst_task_id, $tag_id);
+ }
+ }
+ }
+
+ /**
+ * Duplicate tags to the new task
+ *
+ * @access public
+ * @param integer $src_task_id
+ * @param integer $dst_task_id
+ */
+ public function duplicateTaskTags($src_task_id, $dst_task_id)
+ {
+ $tags = $this->taskTagModel->getTagsByTask($src_task_id);
+
+ foreach ($tags as $tag) {
+ $this->taskTagModel->associateTag($dst_task_id, $tag['id']);
+ }
+ }
+
+ /**
+ * Remove tags that are not available in destination project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $dst_project_id
+ */
+ public function syncTaskTagsToAnotherProject($task_id, $dst_project_id)
+ {
+ $tag_ids = $this->taskTagModel->getTagIdsByTaskNotAvailableInProject($task_id, $dst_project_id);
+
+ foreach ($tag_ids as $tag_id) {
+ $this->taskTagModel->dissociateTag($task_id, $tag_id);
+ }
+ }
+}
diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php
index fa2d32c6..cd70a028 100644
--- a/app/Model/TaskCreationModel.php
+++ b/app/Model/TaskCreationModel.php
@@ -60,7 +60,7 @@ class TaskCreationModel extends Base
$values = $this->dateParser->convert($values, array('date_started'), true);
$this->helper->model->removeFields($values, array('another_task'));
- $this->helper->model->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated'));
+ $this->helper->model->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
if (empty($values['column_id'])) {
$values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
diff --git a/app/Model/TaskDuplicationModel.php b/app/Model/TaskDuplicationModel.php
index 9a4613e2..c9079653 100644
--- a/app/Model/TaskDuplicationModel.php
+++ b/app/Model/TaskDuplicationModel.php
@@ -2,10 +2,7 @@
namespace Kanboard\Model;
-use DateTime;
-use DateInterval;
use Kanboard\Core\Base;
-use Kanboard\Event\TaskEvent;
/**
* Task Duplication
@@ -18,10 +15,10 @@ class TaskDuplicationModel extends Base
/**
* Fields to copy when duplicating a task
*
- * @access private
- * @var array
+ * @access protected
+ * @var string[]
*/
- private $fields_to_duplicate = array(
+ protected $fieldsToDuplicate = array(
'title',
'description',
'date_due',
@@ -30,6 +27,7 @@ class TaskDuplicationModel extends Base
'column_id',
'owner_id',
'score',
+ 'priority',
'category_id',
'time_estimated',
'swimlane_id',
@@ -49,106 +47,13 @@ class TaskDuplicationModel extends Base
*/
public function duplicate($task_id)
{
- return $this->save($task_id, $this->copyFields($task_id));
- }
+ $new_task_id = $this->save($task_id, $this->copyFields($task_id));
- /**
- * Duplicate recurring task
- *
- * @access public
- * @param integer $task_id Task id
- * @return boolean|integer Recurrence task id
- */
- public function duplicateRecurringTask($task_id)
- {
- $values = $this->copyFields($task_id);
-
- if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
- $values['recurrence_parent'] = $task_id;
- $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
- $this->calculateRecurringTaskDueDate($values);
-
- $recurring_task_id = $this->save($task_id, $values);
-
- if ($recurring_task_id > 0) {
- $parent_update = $this->db
- ->table(TaskModel::TABLE)
- ->eq('id', $task_id)
- ->update(array(
- 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED,
- 'recurrence_child' => $recurring_task_id,
- ));
-
- if ($parent_update) {
- return $recurring_task_id;
- }
- }
+ if ($new_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id);
}
- return false;
- }
-
- /**
- * Duplicate a task to another project
- *
- * @access public
- * @param integer $task_id
- * @param integer $project_id
- * @param integer $swimlane_id
- * @param integer $column_id
- * @param integer $category_id
- * @param integer $owner_id
- * @return boolean|integer
- */
- public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
- {
- $values = $this->copyFields($task_id);
- $values['project_id'] = $project_id;
- $values['column_id'] = $column_id !== null ? $column_id : $values['column_id'];
- $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
- $values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
- $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
-
- $this->checkDestinationProjectValues($values);
-
- return $this->save($task_id, $values);
- }
-
- /**
- * Move a task to another project
- *
- * @access public
- * @param integer $task_id
- * @param integer $project_id
- * @param integer $swimlane_id
- * @param integer $column_id
- * @param integer $category_id
- * @param integer $owner_id
- * @return boolean
- */
- public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
- {
- $task = $this->taskFinderModel->getById($task_id);
-
- $values = array();
- $values['is_active'] = 1;
- $values['project_id'] = $project_id;
- $values['column_id'] = $column_id !== null ? $column_id : $task['column_id'];
- $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1;
- $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
- $values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
- $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
-
- $this->checkDestinationProjectValues($values);
-
- if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) {
- $this->container['dispatcher']->dispatch(
- TaskModel::EVENT_MOVE_PROJECT,
- new TaskEvent(array_merge($task, $values, array('task_id' => $task['id'])))
- );
- }
-
- return true;
+ return $new_task_id;
}
/**
@@ -191,58 +96,28 @@ class TaskDuplicationModel extends Base
$values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']);
}
- return $values;
- }
+ // Check if priority exists for destination project
+ $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject(
+ $values['project_id'],
+ empty($values['priority']) ? 0 : $values['priority']
+ );
- /**
- * Calculate new due date for new recurrence task
- *
- * @access public
- * @param array $values Task fields
- */
- public function calculateRecurringTaskDueDate(array &$values)
- {
- if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
- if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) {
- $values['date_due'] = time();
- }
-
- $factor = abs($values['recurrence_factor']);
- $subtract = $values['recurrence_factor'] < 0;
-
- switch ($values['recurrence_timeframe']) {
- case TaskModel::RECURRING_TIMEFRAME_MONTHS:
- $interval = 'P' . $factor . 'M';
- break;
- case TaskModel::RECURRING_TIMEFRAME_YEARS:
- $interval = 'P' . $factor . 'Y';
- break;
- default:
- $interval = 'P' . $factor . 'D';
- }
-
- $date_due = new DateTime();
- $date_due->setTimestamp($values['date_due']);
-
- $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
-
- $values['date_due'] = $date_due->getTimestamp();
- }
+ return $values;
}
/**
* Duplicate fields for the new task
*
- * @access private
+ * @access protected
* @param integer $task_id Task id
* @return array
*/
- private function copyFields($task_id)
+ protected function copyFields($task_id)
{
$task = $this->taskFinderModel->getById($task_id);
$values = array();
- foreach ($this->fields_to_duplicate as $field) {
+ foreach ($this->fieldsToDuplicate as $field) {
$values[$field] = $task[$field];
}
@@ -252,16 +127,16 @@ class TaskDuplicationModel extends Base
/**
* Create the new task and duplicate subtasks
*
- * @access private
+ * @access protected
* @param integer $task_id Task id
* @param array $values Form values
* @return boolean|integer
*/
- private function save($task_id, array $values)
+ protected function save($task_id, array $values)
{
$new_task_id = $this->taskCreationModel->create($values);
- if ($new_task_id) {
+ if ($new_task_id !== false) {
$this->subtaskModel->duplicate($task_id, $new_task_id);
}
diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php
index 0e99c407..7268052c 100644
--- a/app/Model/TaskFinderModel.php
+++ b/app/Model/TaskFinderModel.php
@@ -81,7 +81,8 @@ class TaskFinderModel extends Base
->join(ColumnModel::TABLE, 'id', 'column_id')
->eq(TaskModel::TABLE.'.owner_id', $user_id)
->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
- ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE);
+ ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
+ ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
}
/**
@@ -166,6 +167,7 @@ class TaskFinderModel extends Base
->table(TaskModel::TABLE)
->eq(TaskModel::TABLE.'.project_id', $project_id)
->eq(TaskModel::TABLE.'.is_active', $status_id)
+ ->asc(TaskModel::TABLE.'.id')
->findAll();
}
@@ -183,7 +185,8 @@ class TaskFinderModel extends Base
->table(TaskModel::TABLE)
->eq(TaskModel::TABLE.'.project_id', $project_id)
->in(TaskModel::TABLE.'.is_active', $status)
- ->findAllByColumn('id');
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAllByColumn(TaskModel::TABLE.'.id');
}
/**
@@ -367,6 +370,7 @@ class TaskFinderModel extends Base
'ua.name AS assignee_name',
'ua.username AS assignee_username',
'uc.email AS creator_email',
+ 'uc.name AS creator_name',
'uc.username AS creator_username'
);
}
diff --git a/app/Model/TaskModel.php b/app/Model/TaskModel.php
index b0e7772a..5cddb509 100644
--- a/app/Model/TaskModel.php
+++ b/app/Model/TaskModel.php
@@ -5,7 +5,7 @@ namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
- * Task model
+ * Task Model
*
* @package Kanboard\Model
* @author Frederic Guillot
@@ -17,80 +17,80 @@ class TaskModel extends Base
*
* @var string
*/
- const TABLE = 'tasks';
+ const TABLE = 'tasks';
/**
* Task status
*
* @var integer
*/
- const STATUS_OPEN = 1;
- const STATUS_CLOSED = 0;
+ const STATUS_OPEN = 1;
+ const STATUS_CLOSED = 0;
/**
* Events
*
* @var string
*/
- const EVENT_MOVE_PROJECT = 'task.move.project';
- const EVENT_MOVE_COLUMN = 'task.move.column';
- const EVENT_MOVE_POSITION = 'task.move.position';
- const EVENT_MOVE_SWIMLANE = 'task.move.swimlane';
- const EVENT_UPDATE = 'task.update';
- const EVENT_CREATE = 'task.create';
- const EVENT_CLOSE = 'task.close';
- const EVENT_OPEN = 'task.open';
- const EVENT_CREATE_UPDATE = 'task.create_update';
+ const EVENT_MOVE_PROJECT = 'task.move.project';
+ const EVENT_MOVE_COLUMN = 'task.move.column';
+ const EVENT_MOVE_POSITION = 'task.move.position';
+ const EVENT_MOVE_SWIMLANE = 'task.move.swimlane';
+ const EVENT_UPDATE = 'task.update';
+ const EVENT_CREATE = 'task.create';
+ const EVENT_CLOSE = 'task.close';
+ const EVENT_OPEN = 'task.open';
+ const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
- const EVENT_OVERDUE = 'task.overdue';
- const EVENT_USER_MENTION = 'task.user.mention';
- const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
+ const EVENT_OVERDUE = 'task.overdue';
+ const EVENT_USER_MENTION = 'task.user.mention';
+ const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
/**
* Recurrence: status
*
* @var integer
*/
- const RECURRING_STATUS_NONE = 0;
- const RECURRING_STATUS_PENDING = 1;
- const RECURRING_STATUS_PROCESSED = 2;
+ const RECURRING_STATUS_NONE = 0;
+ const RECURRING_STATUS_PENDING = 1;
+ const RECURRING_STATUS_PROCESSED = 2;
/**
* Recurrence: trigger
*
* @var integer
*/
- const RECURRING_TRIGGER_FIRST_COLUMN = 0;
- const RECURRING_TRIGGER_LAST_COLUMN = 1;
- const RECURRING_TRIGGER_CLOSE = 2;
+ const RECURRING_TRIGGER_FIRST_COLUMN = 0;
+ const RECURRING_TRIGGER_LAST_COLUMN = 1;
+ const RECURRING_TRIGGER_CLOSE = 2;
/**
* Recurrence: timeframe
*
* @var integer
*/
- const RECURRING_TIMEFRAME_DAYS = 0;
- const RECURRING_TIMEFRAME_MONTHS = 1;
- const RECURRING_TIMEFRAME_YEARS = 2;
+ const RECURRING_TIMEFRAME_DAYS = 0;
+ const RECURRING_TIMEFRAME_MONTHS = 1;
+ const RECURRING_TIMEFRAME_YEARS = 2;
/**
* Recurrence: base date used to calculate new due date
*
* @var integer
*/
- const RECURRING_BASEDATE_DUEDATE = 0;
- const RECURRING_BASEDATE_TRIGGERDATE = 1;
+ const RECURRING_BASEDATE_DUEDATE = 0;
+ const RECURRING_BASEDATE_TRIGGERDATE = 1;
/**
* Remove a task
*
* @access public
- * @param integer $task_id Task id
+ * @param integer $task_id Task id
* @return boolean
*/
public function remove($task_id)
{
- if (! $this->taskFinderModel->exists($task_id)) {
+ if (!$this->taskFinderModel->exists($task_id)) {
return false;
}
@@ -105,7 +105,7 @@ class TaskModel extends Base
* Example: "Fix bug #1234" will return 1234
*
* @access public
- * @param string $message Text
+ * @param string $message Text
* @return integer
*/
public function getTaskIdFromText($message)
@@ -118,69 +118,11 @@ class TaskModel extends Base
}
/**
- * Return the list user selectable recurrence status
- *
- * @access public
- * @return array
- */
- public function getRecurrenceStatusList()
- {
- return array(
- TaskModel::RECURRING_STATUS_NONE => t('No'),
- TaskModel::RECURRING_STATUS_PENDING => t('Yes'),
- );
- }
-
- /**
- * Return the list recurrence triggers
- *
- * @access public
- * @return array
- */
- public function getRecurrenceTriggerList()
- {
- return array(
- TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
- TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
- TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
- );
- }
-
- /**
- * Return the list options to calculate recurrence due date
- *
- * @access public
- * @return array
- */
- public function getRecurrenceBasedateList()
- {
- return array(
- TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
- TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
- );
- }
-
- /**
- * Return the list recurrence timeframes
- *
- * @access public
- * @return array
- */
- public function getRecurrenceTimeframeList()
- {
- return array(
- TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
- TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
- TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
- );
- }
-
- /**
* Get task progress based on the column position
*
* @access public
- * @param array $task
- * @param array $columns
+ * @param array $task
+ * @param array $columns
* @return integer
*/
public function getProgress(array $task, array $columns)
@@ -201,25 +143,4 @@ class TaskModel extends Base
return round(($position * 100) / count($columns), 1);
}
-
- /**
- * Helper method to duplicate all tasks to another project
- *
- * @access public
- * @param integer $src_project_id
- * @param integer $dst_project_id
- * @return boolean
- */
- public function duplicate($src_project_id, $dst_project_id)
- {
- $task_ids = $this->taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED));
-
- foreach ($task_ids as $task_id) {
- if (! $this->taskDuplicationModel->duplicateToProject($task_id, $dst_project_id)) {
- return false;
- }
- }
-
- return true;
- }
}
diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php
index 1b176a41..be5f53c8 100644
--- a/app/Model/TaskModificationModel.php
+++ b/app/Model/TaskModificationModel.php
@@ -108,8 +108,6 @@ class TaskModificationModel extends Base
if (isset($values['tags'])) {
$this->taskTagModel->save($original_task['project_id'], $values['id'], $values['tags']);
unset($values['tags']);
- } else {
- $this->taskTagModel->save($original_task['project_id'], $values['id'], array());
}
}
}
diff --git a/app/Model/TaskProjectDuplicationModel.php b/app/Model/TaskProjectDuplicationModel.php
new file mode 100644
index 00000000..8ebed255
--- /dev/null
+++ b/app/Model/TaskProjectDuplicationModel.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task Project Duplication
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class TaskProjectDuplicationModel extends TaskDuplicationModel
+{
+ /**
+ * Duplicate a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean|integer
+ */
+ public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ $values = $this->prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+ $this->checkDestinationProjectValues($values);
+ $new_task_id = $this->save($task_id, $values);
+
+ if ($new_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id);
+ }
+
+ return $new_task_id;
+ }
+
+ /**
+ * Prepare values before duplication
+ *
+ * @access protected
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return array
+ */
+ protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $values['column_id'];
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
+ return $values;
+ }
+}
diff --git a/app/Model/TaskProjectMoveModel.php b/app/Model/TaskProjectMoveModel.php
new file mode 100644
index 00000000..eda23c0b
--- /dev/null
+++ b/app/Model/TaskProjectMoveModel.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Event\TaskEvent;
+
+/**
+ * Task Project Move
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class TaskProjectMoveModel extends TaskDuplicationModel
+{
+ /**
+ * Move a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean
+ */
+ public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task);
+
+ $this->checkDestinationProjectValues($values);
+ $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id);
+
+ if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) {
+ $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id'])));
+ $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event);
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare new task values
+ *
+ * @access protected
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @param array $task
+ * @return array
+ */
+ protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task)
+ {
+ $values = array();
+ $values['is_active'] = 1;
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $task['column_id'];
+ $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1;
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
+ $values['priority'] = $task['priority'];
+ return $values;
+ }
+}
diff --git a/app/Model/TaskRecurrenceModel.php b/app/Model/TaskRecurrenceModel.php
new file mode 100644
index 00000000..ffe43f8c
--- /dev/null
+++ b/app/Model/TaskRecurrenceModel.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Kanboard\Model;
+
+use DateInterval;
+use DateTime;
+
+/**
+ * Task Recurrence
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class TaskRecurrenceModel extends TaskDuplicationModel
+{
+ /**
+ * Return the list user selectable recurrence status
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceStatusList()
+ {
+ return array(
+ TaskModel::RECURRING_STATUS_NONE => t('No'),
+ TaskModel::RECURRING_STATUS_PENDING => t('Yes'),
+ );
+ }
+
+ /**
+ * Return the list recurrence triggers
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTriggerList()
+ {
+ return array(
+ TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
+ TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
+ TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
+ );
+ }
+
+ /**
+ * Return the list options to calculate recurrence due date
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceBasedateList()
+ {
+ return array(
+ TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
+ TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
+ );
+ }
+
+ /**
+ * Return the list recurrence timeframes
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTimeframeList()
+ {
+ return array(
+ TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
+ TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
+ TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
+ );
+ }
+
+ /**
+ * Duplicate recurring task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Recurrence task id
+ */
+ public function duplicateRecurringTask($task_id)
+ {
+ $values = $this->copyFields($task_id);
+
+ if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
+ $values['recurrence_parent'] = $task_id;
+ $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
+ $this->calculateRecurringTaskDueDate($values);
+
+ $recurring_task_id = $this->save($task_id, $values);
+
+ if ($recurring_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id);
+
+ $parent_update = $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED,
+ 'recurrence_child' => $recurring_task_id,
+ ));
+
+ if ($parent_update) {
+ return $recurring_task_id;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculate new due date for new recurrence task
+ *
+ * @access public
+ * @param array $values Task fields
+ */
+ public function calculateRecurringTaskDueDate(array &$values)
+ {
+ if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
+ if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) {
+ $values['date_due'] = time();
+ }
+
+ $factor = abs($values['recurrence_factor']);
+ $subtract = $values['recurrence_factor'] < 0;
+
+ switch ($values['recurrence_timeframe']) {
+ case TaskModel::RECURRING_TIMEFRAME_MONTHS:
+ $interval = 'P' . $factor . 'M';
+ break;
+ case TaskModel::RECURRING_TIMEFRAME_YEARS:
+ $interval = 'P' . $factor . 'Y';
+ break;
+ default:
+ $interval = 'P' . $factor . 'D';
+ }
+
+ $date_due = new DateTime();
+ $date_due->setTimestamp($values['date_due']);
+
+ $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
+
+ $values['date_due'] = $date_due->getTimestamp();
+ }
+ }
+}
diff --git a/app/Model/TaskTagModel.php b/app/Model/TaskTagModel.php
index 91dfd224..0553cc6c 100644
--- a/app/Model/TaskTagModel.php
+++ b/app/Model/TaskTagModel.php
@@ -20,6 +20,23 @@ class TaskTagModel extends Base
const TABLE = 'task_has_tags';
/**
+ * Get all tags not available in a project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @return array
+ */
+ public function getTagIdsByTaskNotAvailableInProject($task_id, $project_id)
+ {
+ return $this->db->table(TagModel::TABLE)
+ ->eq(self::TABLE.'.task_id', $task_id)
+ ->notIn(TagModel::TABLE.'.project_id', array(0, $project_id))
+ ->join(self::TABLE, 'tag_id', 'id')
+ ->findAllByColumn(TagModel::TABLE.'.id');
+ }
+
+ /**
* Get all tags associated to a task
*
* @access public
@@ -82,6 +99,7 @@ class TaskTagModel extends Base
public function save($project_id, $task_id, array $tags)
{
$task_tags = $this->getList($task_id);
+ $tags = array_filter($tags);
return $this->associateTags($project_id, $task_id, $task_tags, $tags) &&
$this->dissociateTags($task_id, $task_tags, $tags);
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 82ccb8c8..99fed66f 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,12 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 111;
+const VERSION = 112;
+
+function version_112(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE columns ADD COLUMN hide_in_dashboard INT DEFAULT 0 NOT NULL');
+}
function version_111(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 229cbd25..b982bcae 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,12 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 90;
+const VERSION = 91;
+
+function version_91(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE columns ADD COLUMN hide_in_dashboard BOOLEAN DEFAULT '0'");
+}
function version_90(PDO $pdo)
{
diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql
index 92ca3686..67dd170a 100644
--- a/app/Schema/Sql/mysql.sql
+++ b/app/Schema/Sql/mysql.sql
@@ -45,6 +45,7 @@ CREATE TABLE `columns` (
`project_id` int(11) NOT NULL,
`task_limit` int(11) DEFAULT '0',
`description` text,
+ `hide_in_dashboard` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_title_project` (`title`,`project_id`),
KEY `columns_project_idx` (`project_id`),
@@ -414,6 +415,17 @@ CREATE TABLE `swimlanes` (
CONSTRAINT `swimlanes_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `tags` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `project_id` int(11) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `project_id` (`project_id`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `task_has_external_links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -479,6 +491,18 @@ CREATE TABLE `task_has_metadata` (
CONSTRAINT `task_has_metadata_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `task_has_tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `task_has_tags` (
+ `task_id` int(11) NOT NULL,
+ `tag_id` int(11) NOT NULL,
+ UNIQUE KEY `tag_id` (`tag_id`,`task_id`),
+ KEY `task_id` (`task_id`),
+ CONSTRAINT `task_has_tags_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `task_has_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
@@ -647,7 +671,7 @@ CREATE TABLE `users` (
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
-INSERT INTO `settings` VALUES ('api_token','e8a7a983f25efa80e203d44a832c9570a5083d3fefa91366989c00e931d0',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','296892f9c821909a92df539b028fdb384e47c9f7a34a8f9cad598e0edbba',0,0),('webhook_url','',0,0);
+INSERT INTO `settings` VALUES ('api_token','19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','1d62395a742260738a406789366a84138ced50a1be62e8862c5cf8d0a561',0,0),('webhook_url','',0,0);
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -676,4 +700,4 @@ UNLOCK TABLES;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$kliMGeKgDYtx9Igek9jGDu0eZM.KXivgzvqtnMuWMkjvZiIc.8p8S', 'app-admin');INSERT INTO schema_version VALUES ('110');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$g28mYPBdsf3/gX/ayd7A8.HSPBRQ/zM/PXlfijelJhXwhnukCRIDi', 'app-admin');INSERT INTO schema_version VALUES ('112');
diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql
index 6c17c1b1..5b4142b7 100644
--- a/app/Schema/Sql/postgres.sql
+++ b/app/Schema/Sql/postgres.sql
@@ -98,7 +98,8 @@ CREATE TABLE "columns" (
"position" integer,
"project_id" integer NOT NULL,
"task_limit" integer DEFAULT 0,
- "description" "text"
+ "description" "text",
+ "hide_in_dashboard" boolean DEFAULT false
);
@@ -740,6 +741,36 @@ ALTER SEQUENCE "swimlanes_id_seq" OWNED BY "swimlanes"."id";
--
+-- Name: tags; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE "tags" (
+ "id" integer NOT NULL,
+ "name" character varying(255) NOT NULL,
+ "project_id" integer NOT NULL
+);
+
+
+--
+-- Name: tags_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE "tags_id_seq"
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE "tags_id_seq" OWNED BY "tags"."id";
+
+
+--
-- Name: task_has_external_links; Type: TABLE; Schema: public; Owner: -
--
@@ -874,6 +905,16 @@ ALTER SEQUENCE "task_has_subtasks_id_seq" OWNED BY "subtasks"."id";
--
+-- Name: task_has_tags; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE "task_has_tags" (
+ "task_id" integer NOT NULL,
+ "tag_id" integer NOT NULL
+);
+
+
+--
-- Name: tasks; Type: TABLE; Schema: public; Owner: -
--
@@ -1236,6 +1277,13 @@ ALTER TABLE ONLY "swimlanes" ALTER COLUMN "id" SET DEFAULT "nextval"('"swimlanes
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
+ALTER TABLE ONLY "tags" ALTER COLUMN "id" SET DEFAULT "nextval"('"tags_id_seq"'::"regclass");
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
ALTER TABLE ONLY "task_has_external_links" ALTER COLUMN "id" SET DEFAULT "nextval"('"task_has_external_links_id_seq"'::"regclass");
@@ -1545,6 +1593,22 @@ ALTER TABLE ONLY "swimlanes"
--
+-- Name: tags_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY "tags"
+ ADD CONSTRAINT "tags_pkey" PRIMARY KEY ("id");
+
+
+--
+-- Name: tags_project_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY "tags"
+ ADD CONSTRAINT "tags_project_id_name_key" UNIQUE ("project_id", "name");
+
+
+--
-- Name: task_has_external_links_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1585,6 +1649,14 @@ ALTER TABLE ONLY "subtasks"
--
+-- Name: task_has_tags_tag_id_task_id_key; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY "task_has_tags"
+ ADD CONSTRAINT "task_has_tags_tag_id_task_id_key" UNIQUE ("tag_id", "task_id");
+
+
+--
-- Name: tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -2031,6 +2103,22 @@ ALTER TABLE ONLY "subtasks"
--
+-- Name: task_has_tags_tag_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY "task_has_tags"
+ ADD CONSTRAINT "task_has_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE;
+
+
+--
+-- Name: task_has_tags_task_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY "task_has_tags"
+ ADD CONSTRAINT "task_has_tags_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE;
+
+
+--
-- Name: tasks_column_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -2155,8 +2243,8 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_high
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0);
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0);
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0);
-INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '85b9f242e49f4c50176591a2f9b812c626384b89ff985a02068455a5be07', 0, 0);
-INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '207d1aaeb9d6d5c01f9ef1e6d61baca86c4c66fdd0b95e76b5c5953681e4', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', 'c9a7c2a4523f1724b2ca047c5685f8e2b26bba47eb69baf4f22d5d50d837', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c57a6cb1789269547b616454e4e2f06d3de0514f83baf8fa5b5a8af44a08', 0, 0);
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0);
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0);
INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0);
@@ -2225,4 +2313,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true);
-- PostgreSQL database dump complete
--
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$kliMGeKgDYtx9Igek9jGDu0eZM.KXivgzvqtnMuWMkjvZiIc.8p8S', 'app-admin');INSERT INTO schema_version VALUES ('89');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$g28mYPBdsf3/gX/ayd7A8.HSPBRQ/zM/PXlfijelJhXwhnukCRIDi', 'app-admin');INSERT INTO schema_version VALUES ('91');
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index dac348d4..2a7735ee 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 102;
+const VERSION = 103;
+
+function version_103(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE columns ADD COLUMN hide_in_dashboard INTEGER DEFAULT 0 NOT NULL");
+}
function version_102(PDO $pdo)
{
diff --git a/app/ServiceProvider/ApiProvider.php b/app/ServiceProvider/ApiProvider.php
index f88d9b4f..5cf6231c 100644
--- a/app/ServiceProvider/ApiProvider.php
+++ b/app/ServiceProvider/ApiProvider.php
@@ -9,6 +9,8 @@ use Kanboard\Api\Procedure\BoardProcedure;
use Kanboard\Api\Procedure\CategoryProcedure;
use Kanboard\Api\Procedure\ColumnProcedure;
use Kanboard\Api\Procedure\CommentProcedure;
+use Kanboard\Api\Procedure\ProjectFileProcedure;
+use Kanboard\Api\Procedure\TaskExternalLinkProcedure;
use Kanboard\Api\Procedure\TaskFileProcedure;
use Kanboard\Api\Procedure\GroupProcedure;
use Kanboard\Api\Procedure\GroupMemberProcedure;
@@ -57,6 +59,7 @@ class ApiProvider implements ServiceProviderInterface
->withObject(new CategoryProcedure($container))
->withObject(new CommentProcedure($container))
->withObject(new TaskFileProcedure($container))
+ ->withObject(new ProjectFileProcedure($container))
->withObject(new LinkProcedure($container))
->withObject(new ProjectProcedure($container))
->withObject(new ProjectPermissionProcedure($container))
@@ -65,6 +68,7 @@ class ApiProvider implements ServiceProviderInterface
->withObject(new SwimlaneProcedure($container))
->withObject(new TaskProcedure($container))
->withObject(new TaskLinkProcedure($container))
+ ->withObject(new TaskExternalLinkProcedure($container))
->withObject(new UserProcedure($container))
->withObject(new GroupProcedure($container))
->withObject(new GroupMemberProcedure($container))
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index 751fe514..978bc05b 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -202,8 +202,10 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('SubtaskProcedure', '*', Role::PROJECT_MEMBER);
$acl->add('SubtaskTimeTrackingProcedure', '*', Role::PROJECT_MEMBER);
$acl->add('SwimlaneProcedure', '*', Role::PROJECT_MANAGER);
+ $acl->add('ProjectFileProcedure', '*', Role::PROJECT_MEMBER);
$acl->add('TaskFileProcedure', '*', Role::PROJECT_MEMBER);
$acl->add('TaskLinkProcedure', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskExternalLinkProcedure', array('createExternalTaskLink', 'updateExternalTaskLink', 'removeExternalTaskLink'), Role::PROJECT_MEMBER);
$acl->add('TaskProcedure', '*', Role::PROJECT_MEMBER);
return $acl;
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index c0fb93bd..e32c0d43 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -55,16 +55,22 @@ class ClassProvider implements ServiceProviderInterface
'ProjectNotificationModel',
'ProjectMetadataModel',
'ProjectGroupRoleModel',
+ 'ProjectTaskDuplicationModel',
+ 'ProjectTaskPriorityModel',
'ProjectUserRoleModel',
'RememberMeSessionModel',
'SubtaskModel',
'SubtaskTimeTrackingModel',
'SwimlaneModel',
+ 'TagDuplicationModel',
'TagModel',
'TaskModel',
'TaskAnalyticModel',
'TaskCreationModel',
'TaskDuplicationModel',
+ 'TaskProjectDuplicationModel',
+ 'TaskProjectMoveModel',
+ 'TaskRecurrenceModel',
'TaskExternalLinkModel',
'TaskFinderModel',
'TaskFileModel',
diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php
index 75b7ff76..21cd3996 100644
--- a/app/Subscriber/RecurringTaskSubscriber.php
+++ b/app/Subscriber/RecurringTaskSubscriber.php
@@ -22,9 +22,9 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI
if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
if ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_FIRST_COLUMN && $this->columnModel->getFirstColumnId($event['project_id']) == $event['src_column_id']) {
- $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']);
+ $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']);
} elseif ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_LAST_COLUMN && $this->columnModel->getLastColumnId($event['project_id']) == $event['dst_column_id']) {
- $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']);
+ $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']);
}
}
}
@@ -34,7 +34,7 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI
$this->logger->debug('Subscriber executed: '.__METHOD__);
if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_CLOSE) {
- $this->taskDuplicationModel->duplicateRecurringTask($event['task_id']);
+ $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']);
}
}
}
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index dbe775d5..bc34363c 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -37,7 +37,7 @@
<?php endif ?>
<?php if (! empty($task['date_due'])): ?>
- <?php if (date('d') == date('d', $task['date_due'])): ?>
+ <?php if (date('Y-m-d') == date('Y-m-d', $task['date_due'])): ?>
<span class="task-board-date task-board-date-today">
<?php elseif (time() > $task['date_due']): ?>
<span class="task-board-date task-board-date-overdue">
diff --git a/app/Template/column/create.php b/app/Template/column/create.php
index 023de525..812e9139 100644
--- a/app/Template/column/create.php
+++ b/app/Template/column/create.php
@@ -13,6 +13,8 @@
<?= $this->form->label(t('Task limit'), 'task_limit') ?>
<?= $this->form->number('task_limit', $values, $errors) ?>
+ <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?>
+
<?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?>
diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php
index a742e4b9..89487298 100644
--- a/app/Template/column/edit.php
+++ b/app/Template/column/edit.php
@@ -15,6 +15,8 @@
<?= $this->form->label(t('Task limit'), 'task_limit') ?>
<?= $this->form->number('task_limit', $values, $errors) ?>
+ <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?>
+
<?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?>
diff --git a/app/Template/dashboard/notifications.php b/app/Template/dashboard/notifications.php
index e0e9b878..3b70b49f 100644
--- a/app/Template/dashboard/notifications.php
+++ b/app/Template/dashboard/notifications.php
@@ -36,10 +36,8 @@
<i class="fa fa-file-o fa-fw"></i>
<?php endif ?>
- <?php if ($this->text->contains($notification['event_name'], 'task.overdue')): ?>
- <?php if (count($notification['event_data']['tasks']) > 1): ?>
- <?= $notification['title'] ?>
- <?php endif ?>
+ <?php if ($this->text->contains($notification['event_name'], 'task.overdue') && count($notification['event_data']['tasks']) > 1): ?>
+ <?= $notification['title'] ?>
<?php else: ?>
<?= $this->url->link($notification['title'], 'WebNotificationController', 'redirect', array('notification_id' => $notification['id'], 'user_id' => $user['id'])) ?>
<?php endif ?>
diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php
index 01d06bab..d00883ba 100644
--- a/app/Template/project_creation/create.php
+++ b/app/Template/project_creation/create.php
@@ -23,9 +23,10 @@
<?php endif ?>
<?= $this->form->checkbox('categoryModel', t('Categories'), 1, true) ?>
+ <?= $this->form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?>
<?= $this->form->checkbox('actionModel', t('Actions'), 1, true) ?>
<?= $this->form->checkbox('swimlaneModel', t('Swimlanes'), 1, true) ?>
- <?= $this->form->checkbox('taskModel', t('Tasks'), 1, false) ?>
+ <?= $this->form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?>
</div>
<div class="form-actions">
diff --git a/app/Template/project_view/duplicate.php b/app/Template/project_view/duplicate.php
index d2cd127a..d66ff591 100644
--- a/app/Template/project_view/duplicate.php
+++ b/app/Template/project_view/duplicate.php
@@ -15,10 +15,11 @@
<?php endif ?>
<?= $this->form->checkbox('categoryModel', t('Categories'), 1, true) ?>
+ <?= $this->form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?>
<?= $this->form->checkbox('actionModel', t('Actions'), 1, true) ?>
<?= $this->form->checkbox('swimlaneModel', t('Swimlanes'), 1, false) ?>
- <?= $this->form->checkbox('taskModel', t('Tasks'), 1, false) ?>
<?= $this->form->checkbox('projectMetadataModel', t('Metadata'), 1, false) ?>
+ <?= $this->form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?>
<div class="form-actions">
<button type="submit" class="btn btn-red"><?= t('Duplicate') ?></button>
diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php
index 5efe8ce6..667a576c 100644
--- a/app/Template/project_view/show.php
+++ b/app/Template/project_view/show.php
@@ -54,9 +54,10 @@
</div>
<table class="table-stripped">
<tr>
- <th class="column-60"><?= t('Column') ?></th>
+ <th class="column-40"><?= t('Column') ?></th>
<th class="column-20"><?= t('Task limit') ?></th>
<th class="column-20"><?= t('Active tasks') ?></th>
+ <th class="column-20"><?= t('Hide tasks in this column in the dashboard') ?></th>
</tr>
<?php foreach ($stats['columns'] as $column): ?>
<tr>
@@ -70,6 +71,13 @@
</td>
<td><?= $column['task_limit'] ?: '∞' ?></td>
<td><?= $column['nb_active_tasks'] ?></td>
+ <td>
+ <?php if ($column['hide_in_dashboard'] == 1): ?>
+ <?= t('Yes') ?>
+ <?php else: ?>
+ <?= t('No') ?>
+ <?php endif ?>
+ </td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/task_creation/show.php b/app/Template/task_creation/show.php
index f799919a..57e77f37 100644
--- a/app/Template/task_creation/show.php
+++ b/app/Template/task_creation/show.php
@@ -27,6 +27,7 @@
<?= $this->task->selectColumn($columns_list, $values, $errors) ?>
<?= $this->task->selectPriority($project, $values) ?>
<?= $this->task->selectScore($values, $errors) ?>
+ <?= $this->task->selectReference($values, $errors) ?>
<?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
</div>
diff --git a/app/Template/task_gantt_creation/show.php b/app/Template/task_gantt_creation/show.php
index 5e4286bd..7521d805 100644
--- a/app/Template/task_gantt_creation/show.php
+++ b/app/Template/task_gantt_creation/show.php
@@ -23,6 +23,7 @@
<?= $this->task->selectSwimlane($swimlanes_list, $values, $errors) ?>
<?= $this->task->selectPriority($project, $values) ?>
<?= $this->task->selectScore($values, $errors) ?>
+ <?= $this->task->selectReference($values, $errors) ?>
<?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
</div>
diff --git a/app/Template/task_modification/show.php b/app/Template/task_modification/show.php
index d747407e..cc38582c 100644
--- a/app/Template/task_modification/show.php
+++ b/app/Template/task_modification/show.php
@@ -21,6 +21,7 @@
<?= $this->task->selectCategory($categories_list, $values, $errors) ?>
<?= $this->task->selectPriority($project, $values) ?>
<?= $this->task->selectScore($values, $errors) ?>
+ <?= $this->task->selectReference($values, $errors) ?>
<?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
</div>
diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php
index b7a95f41..727f9109 100644
--- a/app/User/Avatar/LetterAvatarProvider.php
+++ b/app/User/Avatar/LetterAvatarProvider.php
@@ -142,7 +142,7 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface
// Make hash more sensitive for short string like 'a', 'b', 'c'
$str .= 'x';
- $max = intval(9007199254740991 / $seed2);
+ $max = intval(PHP_INT_MAX / $seed2);
for ($i = 0, $ilen = mb_strlen($str, 'UTF-8'); $i < $ilen; $i++) {
if ($hash > $max) {
diff --git a/app/User/ReverseProxyUserProvider.php b/app/User/ReverseProxyUserProvider.php
index 723b8155..34d2187d 100644
--- a/app/User/ReverseProxyUserProvider.php
+++ b/app/User/ReverseProxyUserProvider.php
@@ -22,14 +22,23 @@ class ReverseProxyUserProvider implements UserProviderInterface
protected $username = '';
/**
+ * User profile if the user already exists
+ *
+ * @access protected
+ * @var array
+ */
+ private $userProfile = array();
+
+ /**
* Constructor
*
* @access public
* @param string $username
*/
- public function __construct($username)
+ public function __construct($username, array $userProfile = array())
{
$this->username = $username;
+ $this->userProfile = $userProfile;
}
/**
@@ -84,7 +93,15 @@ class ReverseProxyUserProvider implements UserProviderInterface
*/
public function getRole()
{
- return REVERSE_PROXY_DEFAULT_ADMIN === $this->username ? Role::APP_ADMIN : Role::APP_USER;
+ if (REVERSE_PROXY_DEFAULT_ADMIN === $this->username) {
+ return Role::APP_ADMIN;
+ }
+
+ if (isset($this->userProfile['role'])) {
+ return $this->userProfile['role'];
+ }
+
+ return Role::APP_USER;
}
/**
diff --git a/app/constants.php b/app/constants.php
index 604f6acd..fc120692 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -21,7 +21,7 @@ defined('PLUGIN_INSTALLER') or define('PLUGIN_INSTALLER', true);
defined('DEBUG') or define('DEBUG', strtolower(getenv('DEBUG')) === 'true');
// Logging drivers: syslog, stdout, stderr or file
-defined('LOG_DRIVER') or define('LOG_DRIVER', getenv('LOG_DRIVER'));
+defined('LOG_DRIVER') or define('LOG_DRIVER', '');
// Logging file
defined('LOG_FILE') or define('LOG_FILE', DATA_DIR.DIRECTORY_SEPARATOR.'debug.log');
diff --git a/assets/css/app.min.css b/assets/css/app.min.css
index 31ab096a..88acd0a0 100644
--- a/assets/css/app.min.css
+++ b/assets/css/app.min.css
@@ -1 +1 @@
-a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-date,input.form-datetime{width:150px}input.form-input-large{width:400px}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}#main .confirm{max-width:700px;font-size:1.1em}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;font-size:.95em;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#666}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid silver;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:flex;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file
+a:focus,a:hover,th a{text-decoration:none}h3,label{margin-top:10px}.tooltip-arrow.bottom:after,.tooltip-arrow.top{top:-10px}.form-errors,.ui-tooltip li,ul.no-bullet li{list-style-type:none}.table-fixed td,.table-fixed th,.tooltip-arrow,header h1{overflow:hidden}#board td,td{vertical-align:top}.table-fixed td,.task-board-collapsed,div.ganttview-vtheader-series-name,header h1{text-overflow:ellipsis;white-space:nowrap}blockquote,body,li,ol,p,table,td,th,tr,ul{margin:0;padding:0;font-size:100%}form,table{margin-bottom:20px}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.page{clear:both}ul.no-bullet li{margin-left:0}.pull-right{text-align:right}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,.1);border-bottom:1px solid rgba(255,255,255,.3)}.chosen-select{min-height:27px}#ui-datepicker-div{font-size:.8em}#app-loading-icon{position:fixed;right:3px;bottom:3px}.web-notification-icon{color:#36C}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}a:hover,h1,h2,h3,th a{color:#333}.smaller{font-size:.85em}a{color:#36C;border:none}a:focus{outline:0;color:#DF5353;border:1px dotted #aaa}h1,h2,h3{font-weight:400}h2{font-size:1.3em;margin-bottom:10px}h3{font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;font-size:.95em}#calendar table{margin-bottom:0}td,th{border:1px solid #eee;padding:.5em 3px}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a:focus,th a:hover{text-decoration:underline}.page-header h2 a,a.btn,header a{text-decoration:none}.table-fixed{table-layout:fixed;white-space:nowrap}.table-stripped tr:nth-child(odd){background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70,.column-80{width:70%}.draggable-row-handle{cursor:move;color:#dedede}.btn,.draggable-item,.task-board-change-assignee,label{cursor:pointer}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,tr.draggable-item-hover{background:#FEFFF2}label{display:block}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{color:#888;border:1px solid #ccc;width:300px;max-width:95%;font-size:100%;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;appearance:none}input[type=number]:focus,input[type=date]:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,textarea:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input.form-numeric,input[type=number]{width:70px}.tag-autocomplete,textarea{width:400px}textarea{border:1px solid #ccc;max-width:99%;height:200px;font-size:100%;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}span.select2-container{margin-top:2px}::-webkit-input-placeholder{color:#ddd;padding-top:2px}::-ms-input-placeholder{color:#ddd;padding-top:2px}::-moz-placeholder{color:#ddd;padding-top:2px}.form-actions{padding-top:20px;clear:both}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-required{color:red;padding-left:5px;font-weight:700}.form-errors{color:#b94a48}ul.form-errors li{margin-left:0}.form-help{font-size:.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}input.form-date,input.form-datetime{width:150px}input.form-input-large{width:400px}input.form-input-small{width:150px}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row}.form-column{margin-right:25px}.form-login{width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-size:1.5em;font-weight:700}.popover-form{margin-bottom:0}.reset-password{margin-top:20px}.reset-password a{font-size:.8em;color:#999}.btn{font-size:1.1em;font-weight:400;-webkit-appearance:none;appearance:none;display:inline-block;color:#333;background:#f5f5f5;border:1px solid #ddd;border-radius:2px;padding:3px 10px;margin:0}.btn:hover{border:1px solid #bbb;color:#000;background:#fafafa}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:focus,.btn-red:hover{color:#fff;background:#c53727}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:focus,.btn-blue:hover{border-color:#2f5bb7;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border:1px solid #ccc;background:#f7f7f7}.buttons-header{font-size:.9em;margin-bottom:15px}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert-fade-out,.ui-tooltip-content .markdown p{margin-bottom:0}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}.tooltip-arrow.bottom,.tooltip-arrow.top:after{bottom:-10px}div.ui-tooltip{min-width:200px;max-width:600px;font-size:.85em}.tooltip-arrow{width:20px;height:10px;position:absolute}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{background:#fff;border:1px solid #aaa;box-shadow:0 0 5px #aaa;content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.textarea-dropdown,ul.dropdown-submenu-open{list-style:none;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.tooltip .fa-info-circle{color:#999;font-size:.95em}header{margin-top:10px;padding-bottom:10px;border-bottom:1px solid #dedede}header h1{margin:0;padding:0;max-width:70%;float:left}header ul{text-align:right;font-size:.9em}header li{display:inline;padding-left:30px}header a{color:#333}header a:hover{color:#666}nav .active a{color:#333;font-weight:700}.logo a{opacity:.5;color:#d40000}.logo span{color:#333}.logo a:hover{opacity:.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}header .user-links .dropdown{margin-left:15px}header h1 .tooltip{opacity:.3;font-size:.6em}.page-header{margin-bottom:20px}.page-header h2{margin:0;padding:0;font-size:1.4em;font-weight:700;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333}.page-header h2 a:focus,.page-header h2 a:hover{color:#aaa}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.menu-inline li,.page-header li{display:inline;padding-right:15px;font-size:.95em}.page-header li.active a{color:#333;text-decoration:none;font-weight:700}.page-header li.active a:focus,.page-header li.active a:hover{text-decoration:underline}.menu-inline{margin-bottom:5px}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:700;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36C;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:400}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}a.board-swimlane-toggle{font-size:.95em;text-decoration:none}a.board-swimlane-toggle:focus,a.board-swimlane-toggle:hover{color:#000;text-decoration:none;border:none}.board-task-list{overflow:auto;min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}.task-board,div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:700}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.task-board{position:relative;margin-bottom:4px;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:700}.task-board-collapsed{overflow:hidden}.task-board-saving-state{opacity:.3}.task-board-category:hover,.task-board-change-assignee:hover{opacity:.6}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-title{font-size:1.15em;margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px;border-radius:4px}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#666;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}.task-board-avatars{text-align:right;float:right}.file-thumbnail img:hover,.task-board-icons a{opacity:.5}.task-board-icons{text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1}.task-board-date{font-weight:700;color:#000}span.task-board-date-today{color:#0000D9;opacity:1}span.task-board-date-overdue{color:#D90000;opacity:1}.task-board .task-score{font-weight:700}.task-board-age{display:inline-block;font-size:.9em}.project-overview-columns,.task-summary-columns{display:-webkit-flex;-webkit-flex-direction:row}span.task-board-age-total{border:1px solid #666;padding:1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:1px solid #666;border-left:none;margin-left:-5px;padding:1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}#task-summary{margin-bottom:15px}#task-summary h2{color:#666;font-size:2.5em;margin-top:0;padding-top:0}.task-summary-buttons{margin-top:10px;font-size:.85em}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:flex;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}.task-summary-column{font-size:.9em;color:#666}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-show-title{border:2px solid #000;border-radius:8px;margin-bottom:20px}.task-show-title h2{color:#555;font-size:1.8em;margin:0;padding:8px}.comment-actions,.comment-content,.comment-title{margin-left:55px}.task-link-closed{text-decoration:line-through}.flag-milestone{color:green}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}#select2-form-color_id-results li.select2-results__option{padding:3px}.assign-me{font-size:.8em;vertical-align:bottom}.subtasks-table td,.task-links-table td{vertical-align:middle}.comment-sorting{text-align:right;font-size:.5em}.avatar-letter,.pagination,.project-overview-column,div.ganttview-hzheader-day,div.ganttview-hzheader-month{text-align:center}.comment-sorting a{color:#555;font-weight:400;text-decoration:none}.comment-sorting a:hover{color:#aaa}.comment{padding:5px;margin-bottom:15px}.comment-title,.form-column div.CodeMirror,.markdown blockquote,.markdown h1,.markdown p{margin-bottom:10px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee}.comment-username{font-weight:700;font-size:1.1em}.comment-date{color:#999;font-size:.7em;font-weight:200}.comment-actions{font-size:.8em;margin-top:8px}.subtasks-table,.task-links-table{font-size:.85em}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.markdown h1,.markdown h2,.markdown h3,.markdown h4{text-decoration:underline}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.task-links-task-count{color:#999}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;font-size:1.5em;font-weight:700}.markdown h2{font-size:1.2em;font-weight:700}.markdown h3,.markdown h4{font-size:1.1em}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.user-mention-link{font-weight:700;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.activity-title,.sidebar>ul li{border-bottom:1px dotted #efefef}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.activity-event,.listing ul,.sidebar>ul li:last-child{margin-bottom:15px}.listing ul{margin-top:15px}.activity-event{padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:400;color:#999;font-size:.8em}.activity-content{margin-left:55px}.activity-title{font-weight:700;color:#000}.activity-description{font-size:.95em;color:#555;margin-top:10px}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}.dashboard-project-stats span{font-size:.75em;margin-right:10px;color:#999}.dashboard-project-stats strong{font-size:1.2em}.dashboard-table-link{font-weight:700;color:#444;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:70%;left:15%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:90%}#main .confirm{max-width:700px;font-size:1.1em}.sidebar-container{margin-top:10px;height:100%;width:100%;display:-ms-flexbox;display:-webkit-box;display:-moz-box;display:-ms-box;display:box;-ms-flex-direction:row;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-ms-box-orient:horizontal;box-orient:horizontal}.sidebar-content{padding-left:10px;-ms-flex:1;-webkit-box-flex:1;-moz-box-flex:1;-ms-box-flex:1;box-flex:1}.sidebar{padding-right:10px;border-right:1px dotted #eee;font-size:.95em;max-width:240px;min-width:190px;width:18%;-ms-flex:0 100px;-webkit-box-flex:0;-moz-box-flex:0;-ms-box-flex:0;box-flex:0}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:700}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li.active,.sidebar-icons>ul li:hover{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}@media only screen and (max-width:1024px){body{font-size:.85em}.form-tab{max-width:404px}.form-inline-group input[type=submit],.form-inline-group label{display:block}.form-inline-group input[type=submit]{margin-top:20px}td>input[type=text]{max-width:150px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:1024px) and (orientation:landscape){header{padding-bottom:4px}div.chosen-container{font-size:.9em}input[type=number],input[type=date],input[type=email],input[type=password],input[type=text]{height:18px}.page-header .form-input-large{width:300px}}@media only screen and (max-width:640px){.hide-mobile{display:none}}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}.dropdown-submenu-open li,.textarea-dropdown li{display:block;margin:0;padding:8px 10px;font-size:.85em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child,.textarea-dropdown li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover,.textarea-dropdown .active,.textarea-dropdown li:hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a,.textarea-dropdown .active a,.textarea-dropdown li:hover a{color:#fff}.dropdown-submenu-open a,.textarea-dropdown a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.page-header .dropdown{padding-right:10px}.dropdown-menu-link-icon,.dropdown-menu-link-text{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.textarea-dropdown{margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:700;color:#b94a48}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:focus,.action-menu:hover{text-decoration:underline}.filter-box{display:inline-block;position:relative;font-size:0;margin-bottom:20px}.filter-box form,.project-header .filter-box{margin:0}.filter-box input[type=text]{margin:0;font-size:16px;height:26px;border-color:#ddd;border-top-left-radius:5px;border-bottom-left-radius:5px;vertical-align:top}.filter-box input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}.filter-box div.dropdown{background:#fafafa;display:inline-block;font-size:16px;border:1px solid #ddd;border-left:none;margin:0;padding:0 8px 0 5px;height:27px}.filter-box div.dropdown:last-child{border-top-right-radius:5px;border-bottom-right-radius:5px}.filter-box div.dropdown a{line-height:27px}div.ganttview-grid,div.ganttview-grid-row-cell,div.ganttview-hzheader-day,div.ganttview-hzheader-month,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series{float:left}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#777}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#666}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;font-size:.9em;overflow:hidden}div.ganttview-vtheader-series-name a{color:#666;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#666}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid silver;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:0}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:flex;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}.project-overview-column{margin-right:80px;padding:3px 15px;border:1px dashed #ddd;border-radius:8px}.project-overview-column strong{font-size:1.3em;color:#444}.project-overview-column span{font-size:.8em;color:#777}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:.9em;color:#555}.file-thumbnail-description{font-size:.8em;color:#aaa;margin-top:8px;margin-bottom:5px}.accordion-collapsed,.accordion-content{margin-bottom:25px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.views{display:inline-block;margin-left:10px;margin-right:10px;font-size:.9em}.views li{background:#fafafa;border-left:1px solid #ddd;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline;padding:5px 8px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.menu-inline li.active a,.views li.active a{font-weight:700;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) 0 10px repeat-x}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus,.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 div,.avatar-48 img{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 div,.avatar-20 img{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff} \ No newline at end of file
diff --git a/assets/css/src/form.css b/assets/css/src/form.css
index bb5cf9a2..2c817072 100644
--- a/assets/css/src/form.css
+++ b/assets/css/src/form.css
@@ -154,6 +154,10 @@ input.form-input-large {
width: 400px;
}
+input.form-input-small {
+ width: 150px;
+}
+
.form-columns {
display: -webkit-flex;
display: flex;
diff --git a/assets/css/vendor.min.css b/assets/css/vendor.min.css
index edd97352..9d81051d 100644
--- a/assets/css/vendor.min.css
+++ b/assets/css/vendor.min.css
@@ -459,7 +459,7 @@ This file is generated by `grunt build`, do not edit it by hand.
}
/* @end */
-.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
+.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
/*!
* FullCalendar v2.7.1 Stylesheet
diff --git a/assets/js/vendor.min.js b/assets/js/vendor.min.js
index 08f82895..4b53fb27 100644
--- a/assets/js/vendor.min.js
+++ b/assets/js/vendor.min.js
@@ -1638,9 +1638,8 @@ if(c&&c._defaults.timeOnly&&b.input.val()!==b.lastVal)try{$.datepicker._updateDa
}).call(this);
-/*! Select2 4.0.3 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k<a.length;k+=1)if(m=a[k],"."===m)a.splice(k,1),k-=1;else if(".."===m){if(1===k&&(".."===a[2]||".."===a[0]))break;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n<c.length;n+=1)if(m=o(c[n],f),k=m.f,"require"===k)u[n]=p.require(a);else if("exports"===k)u[n]=p.exports(a),s=!0;else if("module"===k)h=u[n]=p.module(a);else if(e(q,k)||e(r,k)||e(t,k))u[n]=j(k);else{if(!m.p)throw new Error(a+" missing "+k);m.p.load(m.n,g(f,!0),i(k),{}),u[n]=q[k]}l=d?d.apply(q[a],u):void 0,a&&(h&&h.exports!==b&&h.exports!==q[a]?q[a]=h.exports:l===b&&s||(q[a]=l))}else a&&(q[a]=d)},a=c=n=function(a,c,d,e,f){if("string"==typeof a)return p[a]?p[a](c):j(o(a,c).f);if(!a.splice){if(s=a,s.deps&&n(s.deps,s.callback),!c)return;c.splice?(a=c,c=d,d=null):a=b}return c=c||function(){},"function"==typeof d&&(d=e,e=f),e?m(b,a,c,d):setTimeout(function(){m(b,a,c,d)},4),n},n.config=function(a){return n(a)},a._defined=q,d=function(a,b,c){if("string"!=typeof a)throw new Error("See almond README: incorrect module build, no module name");b.splice||(c=b,b=[]),e(q,a)||e(r,a)||(r[a]=[a,b,c])},d.amd={jQuery:!0}}(),b.requirejs=a,b.require=c,b.define=d}}(),b.define("almond",function(){}),b.define("jquery",[],function(){var b=a||$;return null==b&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),b}),b.define("select2/utils",["jquery"],function(a){function b(a){var b=a.prototype,c=[];for(var d in b){var e=b[d];"function"==typeof e&&"constructor"!==d&&c.push(d)}return c}var c={};c.Extend=function(a,b){function c(){this.constructor=a}var d={}.hasOwnProperty;for(var e in b)d.call(b,e)&&(a[e]=b[e]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},c.Decorate=function(a,c){function d(){var b=Array.prototype.unshift,d=c.prototype.constructor.length,e=a.prototype.constructor;d>0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h<g.length;h++){var i=g[h];d.prototype[i]=a.prototype[i]}for(var j=(function(a){var b=function(){};a in d.prototype&&(b=d.prototype[a]);var e=c.prototype[a];return function(){var a=Array.prototype.unshift;return a.call(arguments,b),e.apply(this,arguments)}}),k=0;k<f.length;k++){var l=f[k];d.prototype[l]=j(l)}return d};var d=function(){this.listeners={}};return d.prototype.on=function(a,b){this.listeners=this.listeners||{},a in this.listeners?this.listeners[a].push(b):this.listeners[a]=[b]},d.prototype.trigger=function(a){var b=Array.prototype.slice,c=b.call(arguments,1);this.listeners=this.listeners||{},null==c&&(c=[]),0===c.length&&c.push({}),c[0]._type=a,a in this.listeners&&this.invoke(this.listeners[a],b.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},d.prototype.invoke=function(a,b){for(var c=0,d=a.length;d>c;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e<c.length;e++){var f=c[e];f=f.substring(0,1).toLowerCase()+f.substring(1),f in d||(d[f]={}),e==c.length-1&&(d[f]=a[b]),d=d[f]}delete a[b]}}return a},c.hasScroll=function(b,c){var d=a(c),e=c.style.overflowX,f=c.style.overflowY;return e!==f||"hidden"!==f&&"visible"!==f?"scroll"===e||"scroll"===f?!0:d.innerHeight()<c.scrollHeight||d.innerWidth()<c.scrollWidth:!1},c.escapeMarkup=function(a){var b={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<ul class="select2-results__options" role="tree"></ul>');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('<li role="treeitem" aria-live="assertive" class="select2-results__option"></li>'),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c<a.results.length;c++){var d=a.results[c],e=this.option(d);b.push(e)}this.$results.append(b)},c.prototype.position=function(a,b){var c=b.find(".select2-results");c.append(a)},c.prototype.sort=function(a){var b=this.options.get("sorter");return b(a)},c.prototype.highlightFirstItem=function(){var a=this.$results.find(".select2-results__option[aria-selected]"),b=a.filter("[aria-selected=true]");b.length>0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j<b.children.length;j++){var k=b.children[j],l=this.option(k);i.push(l)}var m=a("<ul></ul>",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("unselect",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("<span></span>")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('<ul class="select2-selection__rendered"></ul>'),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">&times;</span></li>');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d<a.length;d++){var e=a[d],f=this.selectionContainer(),g=this.display(e,f);f.append(g),f.prop("title",e.title||e.text),f.data("data",e),b.push(f)}var h=this.$selection.find(".select2-selection__rendered");c.appendMany(h,b)}},d}),b.define("select2/selection/placeholder",["../utils"],function(a){function b(a,b,c){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c)}return b.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},b.prototype.createPlaceholder=function(a,b){var c=this.selectionContainer();return c.html(this.display(b)),c.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),c},b.prototype.update=function(a,b){var c=1==b.length&&b[0].id!=this.placeholder.id,d=b.length>1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e<d.length;e++){var f={data:d[e]};if(this.trigger("unselect",f),f.prevented)return}this.$element.val(this.placeholder.id).trigger("change"),this.trigger("toggle",{})}}},c.prototype._handleKeyboardClear=function(a,c,d){d.isOpen()||(c.which==b.DELETE||c.which==b.BACKSPACE)&&this._handleClear(c)},c.prototype.update=function(b,c){if(b.call(this,c),!(this.$selection.find(".select2-selection__placeholder").length>0||0===c.length)){var d=a('<span class="select2-selection__clear">&times;</span>');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" aria-autocomplete="list" /></li>');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");
-if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f<a.length;f++){var g=a[f].id;-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")});else{var d=a.id;this.$element.val(d),this.$element.trigger("change")}},d.prototype.unselect=function(a){var b=this;if(this.$element.prop("multiple"))return a.selected=!1,c(a.element).is("option")?(a.element.selected=!1,void this.$element.trigger("change")):void this.current(function(d){for(var e=[],f=0;f<d.length;f++){var g=d[f].id;g!==a.id&&-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")})},d.prototype.bind=function(a,b){var c=this;this.container=a,a.on("select",function(a){c.select(a.data)}),a.on("unselect",function(a){c.unselect(a.data)})},d.prototype.destroy=function(){this.$element.find("*").each(function(){c.removeData(this,"data")})},d.prototype.query=function(a,b){var d=[],e=this,f=this.$element.children();f.each(function(){var b=c(this);if(b.is("option")||b.is("optgroup")){var f=e.item(b),g=e.matches(a,f);null!==g&&d.push(g)}}),b({results:d})},d.prototype.addOptions=function(a){b.appendMany(this.$element,a)},d.prototype.option=function(a){var b;a.children?(b=document.createElement("optgroup"),b.label=a.text):(b=document.createElement("option"),void 0!==b.textContent?b.textContent=a.text:b.innerText=a.text),a.id&&(b.value=a.id),a.disabled&&(b.disabled=!0),a.selected&&(b.selected=!0),a.title&&(b.title=a.title);var d=c(b),e=this._normalizeItem(a);return e.element=b,c.data(b,"data",e),d},d.prototype.item=function(a){var b={};if(b=c.data(a[0],"data"),null!=b)return b;if(a.is("option"))b={id:a.val(),text:a.text(),disabled:a.prop("disabled"),selected:a.prop("selected"),title:a.prop("title")};else if(a.is("optgroup")){b={text:a.prop("label"),children:[],title:a.prop("title")};for(var d=a.children("option"),e=[],f=0;f<d.length;f++){var g=c(d[f]),h=this.item(g);e.push(h)}b.children=e}return b=this._normalizeItem(b),b.element=a[0],c.data(a[0],"data",b),b},d.prototype._normalizeItem=function(a){c.isPlainObject(a)||(a={id:a,text:a}),a=c.extend({},{text:""},a);var b={selected:!1,disabled:!1};return null!=a.id&&(a.id=a.id.toString()),null!=a.text&&(a.text=a.text.toString()),null==a._resultId&&a.id&&null!=this.container&&(a._resultId=this.generateResultId(this.container,a)),c.extend({},b,a)},d.prototype.matches=function(a,b){var c=this.options.get("matcher");return c(a,b)},d}),b.define("select2/data/array",["./select","../utils","jquery"],function(a,b,c){function d(a,b){var c=b.get("data")||[];d.__super__.constructor.call(this,a,b),this.addOptions(this.convertToOptions(c))}return b.Extend(d,a),d.prototype.select=function(a){var b=this.$element.find("option").filter(function(b,c){return c.value==a.id.toString()});0===b.length&&(b=this.option(a),this.addOptions(b)),d.__super__.select.call(this,a)},d.prototype.convertToOptions=function(a){function d(a){return function(){return c(this).val()==a.id}}for(var e=this,f=this.$element.find("option"),g=f.map(function(){return e.item(c(this)).id}).get(),h=[],i=0;i<a.length;i++){var j=this._normalizeItem(a[i]);if(c.inArray(j.id,g)>=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h<e.length;h++){var i=e[h],j=this._normalizeItem(i),k=this.option(j);this.$element.append(k)}}return b.prototype.query=function(a,b,c){function d(a,f){for(var g=a.results,h=0;h<g.length;h++){var i=g[h],j=null!=i.children&&!d({results:i.children},!0),k=i.text===b.term;if(k||j)return f?!1:(a.data=g,void c(a))}if(f)return!0;var l=e.createTag(b);if(null!=l){var m=e.option(l);m.attr("data-select2-tag",!0),e.addOptions([m]),e.insertTag(g,l)}a.results=g,c(a)}var e=this;return this._removeOldTags(),null==b.term||null!=b.page?void a.call(this,b,c):void a.call(this,b,d)},b.prototype.createTag=function(b,c){var d=a.trim(c.term);return""===d?null:{id:d,text:d}},b.prototype.insertTag=function(a,b,c){b.unshift(c)},b.prototype._removeOldTags=function(b){var c=(this._lastTag,this.$element.find("option[data-select2-tag]"));c.each(function(){this.selected||a(this).remove()})},b}),b.define("select2/data/tokenizer",["jquery"],function(a){function b(a,b,c){var d=c.get("tokenizer");void 0!==d&&(this.tokenizer=d),a.call(this,b,c)}return b.prototype.bind=function(a,b,c){a.call(this,b,c),this.$search=b.dropdown.$search||b.selection.$search||c.find(".select2-search__field")},b.prototype.query=function(b,c,d){function e(b){var c=g._normalizeItem(b),d=g.$element.find("option").filter(function(){return a(this).val()===c.id});if(!d.length){var e=g.option(c);e.attr("data-select2-tag",!0),g._removeOldTags(),g.addOptions([e])}f(c)}function f(a){g.trigger("select",{data:a})}var g=this;c.term=c.term||"";var h=this.tokenizer(c,this.options,e);h.term!==c.term&&(this.$search.length&&(this.$search.val(h.term),this.$search.focus()),c.term=h.term),b.call(this,c,d)},b.prototype.tokenizer=function(b,c,d,e){for(var f=d.get("tokenSeparators")||[],g=c.term,h=0,i=this.createTag||function(a){return{id:a.term,text:a.term}};h<g.length;){var j=g[h];if(-1!==a.inArray(j,f)){var k=g.substr(0,h),l=a.extend({},c,{term:k}),m=i(l);null!=m?(e(m),g=g.substr(h+1)||"",h=0):h++}else h++}return{term:g}},b}),b.define("select2/data/minimumInputLength",[],function(){function a(a,b,c){this.minimumInputLength=c.get("minimumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",b.term.length<this.minimumInputLength?void this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumInputLength",[],function(){function a(a,b,c){this.maximumInputLength=c.get("maximumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",this.maximumInputLength>0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<span class="select2-dropdown"><span class="select2-results"></span></span>');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></span>');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('<li class="select2-results__option select2-results__option--load-more"role="treeitem" aria-disabled="true"></li>'),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a("<span></span>"),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.top<f.top-h.height,k=i.bottom>f.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d<b.length;d++){var e=b[d];e.children?c+=a(e.children):c++}return c}function b(a,b,c,d){this.minimumResultsForSearch=c.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),a.call(this,b,c,d)}return b.prototype.showSearch=function(b,c){return a(c.data.results)<this.minimumResultsForSearch?!1:b.call(this,c)},b}),b.define("select2/dropdown/selectOnClose",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("close",function(a){d._handleSelectOnClose(a)})},a.prototype._handleSelectOnClose=function(a,b){if(b&&null!=b.originalSelect2Event){var c=b.originalSelect2Event;if("select"===c._type||"unselect"===c._type)return}var d=this.getHighlightedResults();if(!(d.length<1)){var e=d.data("data");null!=e.element&&e.element.selected||null==e.element&&e.selected||this.trigger("select",{data:e})}},a}),b.define("select2/dropdown/closeOnSelect",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("select",function(a){d._selectTriggered(a)}),b.on("unselect",function(a){d._selectTriggered(a)})},a.prototype._selectTriggered=function(a,b){var c=b.originalEvent;c&&c.ctrlKey||this.trigger("close",{originalEvent:c,originalSelect2Event:b})},a}),b.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(a){var b=a.input.length-a.maximum,c="Please delete "+b+" character";return 1!=b&&(c+="s"),c},inputTooShort:function(a){var b=a.minimum-a.input.length,c="Please enter "+b+" or more characters";return c},loadingMore:function(){return"Loading more results…"},maximumSelected:function(a){var b="You can only select "+a.maximum+" item";return 1!=a.maximum&&(b+="s"),b},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),b.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C){function D(){this.reset()}D.prototype.apply=function(l){if(l=a.extend(!0,{},this.defaults,l),null==l.dataAdapter){if(null!=l.ajax?l.dataAdapter=o:null!=l.data?l.dataAdapter=n:l.dataAdapter=m,l.minimumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L<K.length;L++){var M=K[L],N={};try{N=k.loadPath(M)}catch(O){try{M=this.defaults.amdLanguageBase+M,N=k.loadPath(M)}catch(P){l.debug&&window.console&&console.warn&&console.warn('Select2: The language file for "'+M+'" could not be automatically loaded. A fallback will be used instead.');continue}}J.extend(N)}l.translations=J}else{var Q=k.loadPath(this.defaults.amdLanguageBase+"en"),R=new k(l.language);R.extend(Q),l.translations=R}return l},D.prototype.reset=function(){function b(a){function b(a){return l[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function c(d,e){if(""===a.trim(d.term))return e;if(e.children&&e.children.length>0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e<b.addedNodes.length;e++){var f=b.addedNodes[e];f.selected&&(c=!0)}else b.removedNodes&&b.removedNodes.length>0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null;
-},e.prototype.render=function(){var b=a('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=c[b].apply(c,f)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c});
+/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k<a.length;k+=1)if(m=a[k],"."===m)a.splice(k,1),k-=1;else if(".."===m){if(1===k&&(".."===a[2]||".."===a[0]))break;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n<c.length;n+=1)if(m=o(c[n],f),k=m.f,"require"===k)u[n]=p.require(a);else if("exports"===k)u[n]=p.exports(a),s=!0;else if("module"===k)h=u[n]=p.module(a);else if(e(q,k)||e(r,k)||e(t,k))u[n]=j(k);else{if(!m.p)throw new Error(a+" missing "+k);m.p.load(m.n,g(f,!0),i(k),{}),u[n]=q[k]}l=d?d.apply(q[a],u):void 0,a&&(h&&h.exports!==b&&h.exports!==q[a]?q[a]=h.exports:l===b&&s||(q[a]=l))}else a&&(q[a]=d)},a=c=n=function(a,c,d,e,f){if("string"==typeof a)return p[a]?p[a](c):j(o(a,c).f);if(!a.splice){if(s=a,s.deps&&n(s.deps,s.callback),!c)return;c.splice?(a=c,c=d,d=null):a=b}return c=c||function(){},"function"==typeof d&&(d=e,e=f),e?m(b,a,c,d):setTimeout(function(){m(b,a,c,d)},4),n},n.config=function(a){return n(a)},a._defined=q,d=function(a,b,c){if("string"!=typeof a)throw new Error("See almond README: incorrect module build, no module name");b.splice||(c=b,b=[]),e(q,a)||e(r,a)||(r[a]=[a,b,c])},d.amd={jQuery:!0}}(),b.requirejs=a,b.require=c,b.define=d}}(),b.define("almond",function(){}),b.define("jquery",[],function(){var b=a||$;return null==b&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),b}),b.define("select2/utils",["jquery"],function(a){function b(a){var b=a.prototype,c=[];for(var d in b){var e=b[d];"function"==typeof e&&"constructor"!==d&&c.push(d)}return c}var c={};c.Extend=function(a,b){function c(){this.constructor=a}var d={}.hasOwnProperty;for(var e in b)d.call(b,e)&&(a[e]=b[e]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},c.Decorate=function(a,c){function d(){var b=Array.prototype.unshift,d=c.prototype.constructor.length,e=a.prototype.constructor;d>0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h<g.length;h++){var i=g[h];d.prototype[i]=a.prototype[i]}for(var j=(function(a){var b=function(){};a in d.prototype&&(b=d.prototype[a]);var e=c.prototype[a];return function(){var a=Array.prototype.unshift;return a.call(arguments,b),e.apply(this,arguments)}}),k=0;k<f.length;k++){var l=f[k];d.prototype[l]=j(l)}return d};var d=function(){this.listeners={}};return d.prototype.on=function(a,b){this.listeners=this.listeners||{},a in this.listeners?this.listeners[a].push(b):this.listeners[a]=[b]},d.prototype.trigger=function(a){var b=Array.prototype.slice;this.listeners=this.listeners||{},a in this.listeners&&this.invoke(this.listeners[a],b.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},d.prototype.invoke=function(a,b){for(var c=0,d=a.length;d>c;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e<c.length;e++){var f=c[e];f=f.substring(0,1).toLowerCase()+f.substring(1),f in d||(d[f]={}),e==c.length-1&&(d[f]=a[b]),d=d[f]}delete a[b]}}return a},c.hasScroll=function(b,c){var d=a(c),e=c.style.overflowX,f=c.style.overflowY;return e!==f||"hidden"!==f&&"visible"!==f?"scroll"===e||"scroll"===f?!0:d.innerHeight()<c.scrollHeight||d.innerWidth()<c.scrollWidth:!1},c.escapeMarkup=function(a){var b={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<ul class="select2-results__options" role="tree"></ul>');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('<li role="treeitem" aria-live="assertive" class="select2-results__option"></li>'),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c<a.results.length;c++){var d=a.results[c],e=this.option(d);b.push(e)}this.$results.append(b)},c.prototype.position=function(a,b){var c=b.find(".select2-results");c.append(a)},c.prototype.sort=function(a){var b=this.options.get("sorter");return b(a)},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")});var f=e.filter("[aria-selected=true]");f.length>0?f.first().trigger("mouseenter"):e.first().trigger("mouseenter")})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j<b.children.length;j++){var k=b.children[j],l=this.option(k);i.push(l)}var m=a("<ul></ul>",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&d.setClasses()}),b.on("unselect",function(){b.isOpen()&&d.setClasses()}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("<span></span>")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('<ul class="select2-selection__rendered"></ul>'),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">&times;</span></li>');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d<a.length;d++){var e=a[d],f=this.selectionContainer(),g=this.display(e,f);f.append(g),f.prop("title",e.title||e.text),f.data("data",e),b.push(f)}var h=this.$selection.find(".select2-selection__rendered");c.appendMany(h,b)}},d}),b.define("select2/selection/placeholder",["../utils"],function(a){function b(a,b,c){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c)}return b.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},b.prototype.createPlaceholder=function(a,b){var c=this.selectionContainer();return c.html(this.display(b)),c.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),c},b.prototype.update=function(a,b){var c=1==b.length&&b[0].id!=this.placeholder.id,d=b.length>1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e<d.length;e++){var f={data:d[e]};if(this.trigger("unselect",f),f.prevented)return}this.$element.val(this.placeholder.id).trigger("change"),this.trigger("toggle",{})}}},c.prototype._handleKeyboardClear=function(a,c,d){d.isOpen()||(c.which==b.DELETE||c.which==b.BACKSPACE)&&this._handleClear(c)},c.prototype.update=function(b,c){if(b.call(this,c),!(this.$selection.find(".select2-selection__placeholder").length>0||0===c.length)){var d=a('<span class="select2-selection__clear">&times;</span>');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" aria-autocomplete="list" /></li>');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f<a.length;f++){var g=a[f].id;-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")});else{var d=a.id;this.$element.val(d),this.$element.trigger("change")}},d.prototype.unselect=function(a){var b=this;if(this.$element.prop("multiple"))return a.selected=!1,
+c(a.element).is("option")?(a.element.selected=!1,void this.$element.trigger("change")):void this.current(function(d){for(var e=[],f=0;f<d.length;f++){var g=d[f].id;g!==a.id&&-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")})},d.prototype.bind=function(a,b){var c=this;this.container=a,a.on("select",function(a){c.select(a.data)}),a.on("unselect",function(a){c.unselect(a.data)})},d.prototype.destroy=function(){this.$element.find("*").each(function(){c.removeData(this,"data")})},d.prototype.query=function(a,b){var d=[],e=this,f=this.$element.children();f.each(function(){var b=c(this);if(b.is("option")||b.is("optgroup")){var f=e.item(b),g=e.matches(a,f);null!==g&&d.push(g)}}),b({results:d})},d.prototype.addOptions=function(a){b.appendMany(this.$element,a)},d.prototype.option=function(a){var b;a.children?(b=document.createElement("optgroup"),b.label=a.text):(b=document.createElement("option"),void 0!==b.textContent?b.textContent=a.text:b.innerText=a.text),a.id&&(b.value=a.id),a.disabled&&(b.disabled=!0),a.selected&&(b.selected=!0),a.title&&(b.title=a.title);var d=c(b),e=this._normalizeItem(a);return e.element=b,c.data(b,"data",e),d},d.prototype.item=function(a){var b={};if(b=c.data(a[0],"data"),null!=b)return b;if(a.is("option"))b={id:a.val(),text:a.text(),disabled:a.prop("disabled"),selected:a.prop("selected"),title:a.prop("title")};else if(a.is("optgroup")){b={text:a.prop("label"),children:[],title:a.prop("title")};for(var d=a.children("option"),e=[],f=0;f<d.length;f++){var g=c(d[f]),h=this.item(g);e.push(h)}b.children=e}return b=this._normalizeItem(b),b.element=a[0],c.data(a[0],"data",b),b},d.prototype._normalizeItem=function(a){c.isPlainObject(a)||(a={id:a,text:a}),a=c.extend({},{text:""},a);var b={selected:!1,disabled:!1};return null!=a.id&&(a.id=a.id.toString()),null!=a.text&&(a.text=a.text.toString()),null==a._resultId&&a.id&&null!=this.container&&(a._resultId=this.generateResultId(this.container,a)),c.extend({},b,a)},d.prototype.matches=function(a,b){var c=this.options.get("matcher");return c(a,b)},d}),b.define("select2/data/array",["./select","../utils","jquery"],function(a,b,c){function d(a,b){var c=b.get("data")||[];d.__super__.constructor.call(this,a,b),this.addOptions(this.convertToOptions(c))}return b.Extend(d,a),d.prototype.select=function(a){var b=this.$element.find("option").filter(function(b,c){return c.value==a.id.toString()});0===b.length&&(b=this.option(a),this.addOptions(b)),d.__super__.select.call(this,a)},d.prototype.convertToOptions=function(a){function d(a){return function(){return c(this).val()==a.id}}for(var e=this,f=this.$element.find("option"),g=f.map(function(){return e.item(c(this)).id}).get(),h=[],i=0;i<a.length;i++){var j=this._normalizeItem(a[i]);if(c.inArray(j.id,g)>=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&""!==a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h<e.length;h++){var i=e[h],j=this._normalizeItem(i),k=this.option(j);this.$element.append(k)}}return b.prototype.query=function(a,b,c){function d(a,f){for(var g=a.results,h=0;h<g.length;h++){var i=g[h],j=null!=i.children&&!d({results:i.children},!0),k=i.text===b.term;if(k||j)return f?!1:(a.data=g,void c(a))}if(f)return!0;var l=e.createTag(b);if(null!=l){var m=e.option(l);m.attr("data-select2-tag",!0),e.addOptions([m]),e.insertTag(g,l)}a.results=g,c(a)}var e=this;return this._removeOldTags(),null==b.term||null!=b.page?void a.call(this,b,c):void a.call(this,b,d)},b.prototype.createTag=function(b,c){var d=a.trim(c.term);return""===d?null:{id:d,text:d}},b.prototype.insertTag=function(a,b,c){b.unshift(c)},b.prototype._removeOldTags=function(b){var c=(this._lastTag,this.$element.find("option[data-select2-tag]"));c.each(function(){this.selected||a(this).remove()})},b}),b.define("select2/data/tokenizer",["jquery"],function(a){function b(a,b,c){var d=c.get("tokenizer");void 0!==d&&(this.tokenizer=d),a.call(this,b,c)}return b.prototype.bind=function(a,b,c){a.call(this,b,c),this.$search=b.dropdown.$search||b.selection.$search||c.find(".select2-search__field")},b.prototype.query=function(a,b,c){function d(a){e.trigger("select",{data:a})}var e=this;b.term=b.term||"";var f=this.tokenizer(b,this.options,d);f.term!==b.term&&(this.$search.length&&(this.$search.val(f.term),this.$search.focus()),b.term=f.term),a.call(this,b,c)},b.prototype.tokenizer=function(b,c,d,e){for(var f=d.get("tokenSeparators")||[],g=c.term,h=0,i=this.createTag||function(a){return{id:a.term,text:a.term}};h<g.length;){var j=g[h];if(-1!==a.inArray(j,f)){var k=g.substr(0,h),l=a.extend({},c,{term:k}),m=i(l);null!=m?(e(m),g=g.substr(h+1)||"",h=0):h++}else h++}return{term:g}},b}),b.define("select2/data/minimumInputLength",[],function(){function a(a,b,c){this.minimumInputLength=c.get("minimumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",b.term.length<this.minimumInputLength?void this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumInputLength",[],function(){function a(a,b,c){this.maximumInputLength=c.get("maximumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",this.maximumInputLength>0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<span class="select2-dropdown"><span class="select2-results"></span></span>');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></span>');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('<li class="select2-results__option select2-results__option--load-more"role="treeitem" aria-disabled="true"></li>'),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a("<span></span>"),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.top<f.top-h.height,k=i.bottom>f.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d<b.length;d++){var e=b[d];e.children?c+=a(e.children):c++}return c}function b(a,b,c,d){this.minimumResultsForSearch=c.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),a.call(this,b,c,d)}return b.prototype.showSearch=function(b,c){return a(c.data.results)<this.minimumResultsForSearch?!1:b.call(this,c)},b}),b.define("select2/dropdown/selectOnClose",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("close",function(){d._handleSelectOnClose()})},a.prototype._handleSelectOnClose=function(){var a=this.getHighlightedResults();if(!(a.length<1)){var b=a.data("data");null!=b.element&&b.element.selected||null==b.element&&b.selected||this.trigger("select",{data:b})}},a}),b.define("select2/dropdown/closeOnSelect",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("select",function(a){d._selectTriggered(a)}),b.on("unselect",function(a){d._selectTriggered(a)})},a.prototype._selectTriggered=function(a,b){var c=b.originalEvent;c&&c.ctrlKey||this.trigger("close",{})},a}),b.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(a){var b=a.input.length-a.maximum,c="Please delete "+b+" character";return 1!=b&&(c+="s"),c},inputTooShort:function(a){var b=a.minimum-a.input.length,c="Please enter "+b+" or more characters";return c},loadingMore:function(){return"Loading more results…"},maximumSelected:function(a){var b="You can only select "+a.maximum+" item";return 1!=a.maximum&&(b+="s"),b},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),b.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C){function D(){this.reset()}D.prototype.apply=function(l){if(l=a.extend(!0,{},this.defaults,l),null==l.dataAdapter){if(null!=l.ajax?l.dataAdapter=o:null!=l.data?l.dataAdapter=n:l.dataAdapter=m,l.minimumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L<K.length;L++){var M=K[L],N={};try{N=k.loadPath(M)}catch(O){try{M=this.defaults.amdLanguageBase+M,N=k.loadPath(M)}catch(P){l.debug&&window.console&&console.warn&&console.warn('Select2: The language file for "'+M+'" could not be automatically loaded. A fallback will be used instead.');continue}}J.extend(N)}l.translations=J}else{var Q=k.loadPath(this.defaults.amdLanguageBase+"en"),R=new k(l.language);R.extend(Q),l.translations=R}return l},D.prototype.reset=function(){function b(a){function b(a){return l[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function c(d,e){if(""===a.trim(d.term))return e;if(e.children&&e.children.length>0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this._sync=c.bind(this._syncAttributes,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._sync);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._sync)}),this._observer.observe(this.$element[0],{attributes:!0,subtree:!1})):this.$element[0].addEventListener&&this.$element[0].addEventListener("DOMAttrModified",b._sync,!1)},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._sync),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&this.$element[0].removeEventListener("DOMAttrModified",this._sync,!1),this._sync=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d;return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2.");var e=Array.prototype.slice.call(arguments,1);d=c[b].apply(c,e)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c});
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return ce.apply(null,arguments)}function b(a){ce=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function f(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function g(a,b){for(var c in b)f(b,c)&&(a[c]=b[c]);return f(b,"toString")&&(a.toString=b.toString),f(b,"valueOf")&&(a.valueOf=b.valueOf),a}function h(a,b,c,d){return Ja(a,b,c,d,!0).utc()}function i(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function j(a){return null==a._pf&&(a._pf=i()),a._pf}function k(a){if(null==a._isValid){var b=j(a),c=de.call(b.parsedDateParts,function(a){return null!=a});a._isValid=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c),a._strict&&(a._isValid=a._isValid&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour)}return a._isValid}function l(a){var b=h(NaN);return null!=a?g(j(b),a):j(b).userInvalidated=!0,b}function m(a){return void 0===a}function n(a,b){var c,d,e;if(m(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),m(b._i)||(a._i=b._i),m(b._f)||(a._f=b._f),m(b._l)||(a._l=b._l),m(b._strict)||(a._strict=b._strict),m(b._tzm)||(a._tzm=b._tzm),m(b._isUTC)||(a._isUTC=b._isUTC),m(b._offset)||(a._offset=b._offset),m(b._pf)||(a._pf=j(b)),m(b._locale)||(a._locale=b._locale),ee.length>0)for(c in ee)d=ee[c],e=b[d],m(e)||(a[d]=e);return a}function o(b){n(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),fe===!1&&(fe=!0,a.updateOffset(this),fe=!1)}function p(a){return a instanceof o||null!=a&&null!=a._isAMomentObject}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=q(b)),c}function s(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&r(a[d])!==r(b[d]))&&g++;return g+f}function t(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function u(b,c){var d=!0;return g(function(){return null!=a.deprecationHandler&&a.deprecationHandler(null,b),d&&(t(b+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),d=!1),c.apply(this,arguments)},c)}function v(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),ge[b]||(t(c),ge[b]=!0)}function w(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function x(a){return"[object Object]"===Object.prototype.toString.call(a)}function y(a){var b,c;for(c in a)b=a[c],w(b)?this[c]=b:this["_"+c]=b;this._config=a,this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function z(a,b){var c,d=g({},a);for(c in b)f(b,c)&&(x(a[c])&&x(b[c])?(d[c]={},g(d[c],a[c]),g(d[c],b[c])):null!=b[c]?d[c]=b[c]:delete d[c]);return d}function A(a){null!=a&&this.set(a)}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a){for(var b,c,d,e,f=0;f<a.length;){for(e=B(a[f]).split("-"),b=e.length,c=B(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=D(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&s(e,c,!0)>=b-1)break;b--}f++}return null}function D(a){var b=null;if(!ke[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=ie._abbr,require("./locale/"+a),E(b)}catch(c){}return ke[a]}function E(a,b){var c;return a&&(c=m(b)?H(a):F(a,b),c&&(ie=c)),ie._abbr}function F(a,b){return null!==b?(b.abbr=a,null!=ke[a]?(v("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"),b=z(ke[a]._config,b)):null!=b.parentLocale&&(null!=ke[b.parentLocale]?b=z(ke[b.parentLocale]._config,b):v("parentLocaleUndefined","specified parentLocale is not defined yet")),ke[a]=new A(b),E(a),ke[a]):(delete ke[a],null)}function G(a,b){if(null!=b){var c;null!=ke[a]&&(b=z(ke[a]._config,b)),c=new A(b),c.parentLocale=ke[a],ke[a]=c,E(a)}else null!=ke[a]&&(null!=ke[a].parentLocale?ke[a]=ke[a].parentLocale:null!=ke[a]&&delete ke[a]);return ke[a]}function H(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return ie;if(!c(a)){if(b=D(a))return b;a=[a]}return C(a)}function I(){return he(ke)}function J(a,b){var c=a.toLowerCase();le[c]=le[c+"s"]=le[b]=a}function K(a){return"string"==typeof a?le[a]||le[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)f(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(b,c){return function(d){return null!=d?(O(this,b,d),a.updateOffset(this,c),this):N(this,b)}}function N(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function O(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function P(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=K(a),w(this[a]))return this[a](b);return this}function Q(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function R(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(pe[a]=e),b&&(pe[b[0]]=function(){return Q(e.apply(this,arguments),b[1],b[2])}),c&&(pe[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function S(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function T(a){var b,c,d=a.match(me);for(b=0,c=d.length;c>b;b++)pe[d[b]]?d[b]=pe[d[b]]:d[b]=S(d[b]);return function(b){var e,f="";for(e=0;c>e;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}function U(a,b){return a.isValid()?(b=V(b,a.localeData()),oe[b]=oe[b]||T(b),oe[b](a)):a.localeData().invalidDate()}function V(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(ne.lastIndex=0;d>=0&&ne.test(a);)a=a.replace(ne,c),ne.lastIndex=0,d-=1;return a}function W(a,b,c){He[a]=w(b)?b:function(a,d){return a&&c?c:b}}function X(a,b){return f(He,a)?He[a](b._strict,b._locale):new RegExp(Y(a))}function Y(a){return Z(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function Z(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function $(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=r(a)}),c=0;c<a.length;c++)Ie[a[c]]=d}function _(a,b){$(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function aa(a,b,c){null!=b&&f(Ie,a)&&Ie[a](b,c._a,c,a)}function ba(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function ca(a,b){return c(this._months)?this._months[a.month()]:this._months[Se.test(b)?"format":"standalone"][a.month()]}function da(a,b){return c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[Se.test(b)?"format":"standalone"][a.month()]}function ea(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;12>d;++d)f=h([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=je.call(this._shortMonthsParse,g),-1!==e?e:null):(e=je.call(this._longMonthsParse,g),-1!==e?e:null):"MMM"===b?(e=je.call(this._shortMonthsParse,g),-1!==e?e:(e=je.call(this._longMonthsParse,g),-1!==e?e:null)):(e=je.call(this._longMonthsParse,g),-1!==e?e:(e=je.call(this._shortMonthsParse,g),-1!==e?e:null))}function fa(a,b,c){var d,e,f;if(this._monthsParseExact)return ea.call(this,a,b,c);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function ga(a,b){var c;if(!a.isValid())return a;if("string"==typeof b)if(/^\d+$/.test(b))b=r(b);else if(b=a.localeData().monthsParse(b),"number"!=typeof b)return a;return c=Math.min(a.date(),ba(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ha(b){return null!=b?(ga(this,b),a.updateOffset(this,!0),this):N(this,"Month")}function ia(){return ba(this.year(),this.month())}function ja(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex}function ka(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex}function la(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=h([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=Z(d[b]),e[b]=Z(e[b]),f[b]=Z(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}function ma(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[Ke]<0||c[Ke]>11?Ke:c[Le]<1||c[Le]>ba(c[Je],c[Ke])?Le:c[Me]<0||c[Me]>24||24===c[Me]&&(0!==c[Ne]||0!==c[Oe]||0!==c[Pe])?Me:c[Ne]<0||c[Ne]>59?Ne:c[Oe]<0||c[Oe]>59?Oe:c[Pe]<0||c[Pe]>999?Pe:-1,j(a)._overflowDayOfYear&&(Je>b||b>Le)&&(b=Le),j(a)._overflowWeeks&&-1===b&&(b=Qe),j(a)._overflowWeekday&&-1===b&&(b=Re),j(a).overflow=b),a}function na(a){var b,c,d,e,f,g,h=a._i,i=Xe.exec(h)||Ye.exec(h);if(i){for(j(a).iso=!0,b=0,c=$e.length;c>b;b++)if($e[b][1].exec(i[1])){e=$e[b][0],d=$e[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=_e.length;c>b;b++)if(_e[b][1].exec(i[3])){f=(i[2]||" ")+_e[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Ze.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),Ca(a)}else a._isValid=!1}function oa(b){var c=af.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(na(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function pa(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function qa(a){var b=new Date(Date.UTC.apply(null,arguments));return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ra(a){return sa(a)?366:365}function sa(a){return a%4===0&&a%100!==0||a%400===0}function ta(){return sa(this.year())}function ua(a,b,c){var d=7+b-c,e=(7+qa(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=ra(f)+j):j>ra(a)?(f=a+1,g=j-ra(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(ra(a)-d+e)/7}function ya(a,b,c){return null!=a?a:null!=b?b:c}function za(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function Aa(a){var b,c,d,e,f=[];if(!a._d){for(d=za(a),a._w&&null==a._a[Le]&&null==a._a[Ke]&&Ba(a),a._dayOfYear&&(e=ya(a._a[Je],d[Je]),a._dayOfYear>ra(e)&&(j(a)._overflowDayOfYear=!0),c=qa(e,0,a._dayOfYear),a._a[Ke]=c.getUTCMonth(),a._a[Le]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[Me]&&0===a._a[Ne]&&0===a._a[Oe]&&0===a._a[Pe]&&(a._nextDay=!0,a._a[Me]=0),a._d=(a._useUTC?qa:pa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Me]=24)}}function Ba(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=ya(b.GG,a._a[Je],wa(Ka(),1,4).year),d=ya(b.W,1),e=ya(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=ya(b.gg,a._a[Je],wa(Ka(),f,g).year),d=ya(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>xa(c,f,g)?j(a)._overflowWeeks=!0:null!=i?j(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[Je]=h.year,a._dayOfYear=h.dayOfYear)}function Ca(b){if(b._f===a.ISO_8601)return void na(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=V(b._f,b._locale).match(me)||[],c=0;c<e.length;c++)f=e[c],d=(h.match(X(f,b))||[])[0],d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),pe[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),aa(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[Me]<=12&&b._a[Me]>0&&(j(b).bigHour=void 0),j(b).parsedDateParts=b._a.slice(0),j(b).meridiem=b._meridiem,b._a[Me]=Da(b._locale,b._a[Me],b._meridiem),Aa(b),ma(b)}function Da(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function Ea(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=n({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],Ca(b),k(b)&&(f+=j(b).charsLeftOver,f+=10*j(b).unusedTokens.length,j(b).score=f,(null==d||d>f)&&(d=f,c=b));g(a,c||b)}function Fa(a){if(!a._d){var b=L(a._i);a._a=e([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),Aa(a)}}function Ga(a){var b=new o(ma(Ha(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Ha(a){var b=a._i,e=a._f;return a._locale=a._locale||H(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),p(b)?new o(ma(b)):(c(e)?Ea(a):e?Ca(a):d(b)?a._d=b:Ia(a),k(a)||(a._d=null),a))}function Ia(b){var f=b._i;void 0===f?b._d=new Date(a.now()):d(f)?b._d=new Date(f.valueOf()):"string"==typeof f?oa(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),Aa(b)):"object"==typeof f?Fa(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Ja(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,Ga(f)}function Ka(a,b,c,d){return Ja(a,b,c,d,!1)}function La(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Ka();for(d=b[0],e=1;e<b.length;++e)(!b[e].isValid()||b[e][a](d))&&(d=b[e]);return d}function Ma(){var a=[].slice.call(arguments,0);return La("isBefore",a)}function Na(){var a=[].slice.call(arguments,0);return La("isAfter",a)}function Oa(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+1e3*h*60*60,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=H(),this._bubble()}function Pa(a){return a instanceof Oa}function Qa(a,b){R(a,0,0,function(){var a=this.utcOffset(),c="+";return 0>a&&(a=-a,c="-"),c+Q(~~(a/60),2)+b+Q(~~a%60,2)})}function Ra(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(ff)||["-",0,0],f=+(60*e[1])+r(e[2]);return"+"===e[0]?f:-f}function Sa(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(p(b)||d(b)?b.valueOf():Ka(b).valueOf())-e.valueOf(),e._d.setTime(e._d.valueOf()+f),a.updateOffset(e,!1),e):Ka(b).local()}function Ta(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ua(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=Ra(Ee,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ta(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?jb(this,db(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ta(this):null!=b?this:NaN}function Va(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Wa(a){return this.utcOffset(0,a)}function Xa(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ta(this),"m")),this}function Ya(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ra(De,this._i)),this}function Za(a){return this.isValid()?(a=a?Ka(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function $a(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function _a(){if(!m(this._isDSTShifted))return this._isDSTShifted;var a={};if(n(a,this),a=Ha(a),a._a){var b=a._isUTC?h(a._a):Ka(a._a);this._isDSTShifted=this.isValid()&&s(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function ab(){return this.isValid()?!this._isUTC:!1}function bb(){return this.isValid()?this._isUTC:!1}function cb(){return this.isValid()?this._isUTC&&0===this._offset:!1}function db(a,b){var c,d,e,g=a,h=null;return Pa(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=gf.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:r(h[Le])*c,h:r(h[Me])*c,m:r(h[Ne])*c,s:r(h[Oe])*c,ms:r(h[Pe])*c}):(h=hf.exec(a))?(c="-"===h[1]?-1:1,g={y:eb(h[2],c),M:eb(h[3],c),w:eb(h[4],c),d:eb(h[5],c),h:eb(h[6],c),m:eb(h[7],c),s:eb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=gb(Ka(g.from),Ka(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Oa(g),Pa(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function eb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function fb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function gb(a,b){var c;return a.isValid()&&b.isValid()?(b=Sa(b,a),a.isBefore(b)?c=fb(a,b):(c=fb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function hb(a){return 0>a?-1*Math.round(-1*a):Math.round(a)}function ib(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(v(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=db(c,d),jb(this,e,a),this}}function jb(b,c,d,e){var f=c._milliseconds,g=hb(c._days),h=hb(c._months);b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&O(b,"Date",N(b,"Date")+g*d),h&&ga(b,N(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function kb(a,b){var c=a||Ka(),d=Sa(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse",g=b&&(w(b[f])?b[f]():b[f]);return this.format(g||this.localeData().calendar(f,this,Ka(c)))}function lb(){return new o(this)}function mb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf()):!1}function nb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf()):!1}function ob(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function pb(a,b){var c,d=p(a)?a:Ka(a);return this.isValid()&&d.isValid()?(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf())):!1}function qb(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function rb(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function sb(a,b,c){var d,e,f,g;return this.isValid()?(d=Sa(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=tb(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:q(g)):NaN):NaN}function tb(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function ub(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function vb(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?w(Date.prototype.toISOString)?this.toDate().toISOString():U(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):U(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function wb(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=U(this,b);return this.localeData().postformat(c)}function xb(a,b){return this.isValid()&&(p(a)&&a.isValid()||Ka(a).isValid())?db({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function yb(a){return this.from(Ka(),a)}function zb(a,b){return this.isValid()&&(p(a)&&a.isValid()||Ka(a).isValid())?db({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function Ab(a){return this.to(Ka(),a)}function Bb(a){var b;return void 0===a?this._locale._abbr:(b=H(a),null!=b&&(this._locale=b),this)}function Cb(){return this._locale}function Db(a){switch(a=K(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function Eb(a){return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function Fb(){return this._d.valueOf()-6e4*(this._offset||0)}function Gb(){return Math.floor(this.valueOf()/1e3)}function Hb(){return this._offset?new Date(this.valueOf()):this._d}function Ib(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function Jb(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function Kb(){return this.isValid()?this.toISOString():null}function Lb(){return k(this)}function Mb(){return g({},j(this))}function Nb(){return j(this).overflow}function Ob(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Pb(a,b){R(0,[a,a.length],0,b)}function Qb(a){return Ub.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Rb(a){return Ub.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Sb(){return xa(this.year(),1,4)}function Tb(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ub(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Vb.call(this,a,b,c,d,e))}function Vb(a,b,c,d,e){var f=va(a,b,c,d,e),g=qa(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Wb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Xb(a){return wa(a,this._week.dow,this._week.doy).week}function Yb(){return this._week.dow}function Zb(){return this._week.doy}function $b(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function _b(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function ac(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function bc(a,b){return c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]}function cc(a){return this._weekdaysShort[a.day()]}function dc(a){return this._weekdaysMin[a.day()]}function ec(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;7>d;++d)f=h([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),-1!==e?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:null):(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._minWeekdaysParse,g),-1!==e?e:null))):(e=je.call(this._minWeekdaysParse,g),-1!==e?e:(e=je.call(this._weekdaysParse,g),-1!==e?e:(e=je.call(this._shortWeekdaysParse,g),-1!==e?e:null)))}function fc(a,b,c){var d,e,f;if(this._weekdaysParseExact)return ec.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){if(e=h([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function gc(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=ac(a,this.localeData()),this.add(a-b,"d")):b}function hc(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function ic(a){return this.isValid()?null==a?this.day()||7:this.day(this.day()%7?a:a-7):null!=a?this:NaN}function jc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex}function kc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function lc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mc(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],i=[],j=[],k=[];for(b=0;7>b;b++)c=h([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),i.push(e),j.push(f),k.push(d),k.push(e),k.push(f);for(g.sort(a),i.sort(a),j.sort(a),k.sort(a),b=0;7>b;b++)i[b]=Z(i[b]),j[b]=Z(j[b]),k[b]=Z(k[b]);this._weekdaysRegex=new RegExp("^("+k.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function nc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function oc(){return this.hours()%12||12}function pc(){return this.hours()||24}function qc(a,b){R(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function rc(a,b){return b._meridiemParse}function sc(a){return"p"===(a+"").toLowerCase().charAt(0)}function tc(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function uc(a,b){b[Pe]=r(1e3*("0."+a))}function vc(){return this._isUTC?"UTC":""}function wc(){return this._isUTC?"Coordinated Universal Time":""}function xc(a){return Ka(1e3*a)}function yc(){return Ka.apply(null,arguments).parseZone()}function zc(a,b,c){var d=this._calendar[a];return w(d)?d.call(b,c):d}function Ac(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function Bc(){return this._invalidDate}function Cc(a){return this._ordinal.replace("%d",a)}function Dc(a){return a}function Ec(a,b,c,d){var e=this._relativeTime[c];return w(e)?e(a,b,c,d):e.replace(/%d/i,a)}function Fc(a,b){var c=this._relativeTime[a>0?"future":"past"];return w(c)?c(b):c.replace(/%s/i,b)}function Gc(a,b,c,d){var e=H(),f=h().set(d,b);return e[c](f,a)}function Hc(a,b,c){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return Gc(a,b,c,"month");var d,e=[];for(d=0;12>d;d++)e[d]=Gc(a,d,c,"month");return e}function Ic(a,b,c,d){"boolean"==typeof a?("number"==typeof b&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,"number"==typeof b&&(c=b,b=void 0),b=b||"");var e=H(),f=a?e._week.dow:0;if(null!=c)return Gc(b,(c+f)%7,d,"day");var g,h=[];for(g=0;7>g;g++)h[g]=Gc(b,(g+f)%7,d,"day");return h}function Jc(a,b){return Hc(a,b,"months")}function Kc(a,b){return Hc(a,b,"monthsShort")}function Lc(a,b,c){return Ic(a,b,c,"weekdays")}function Mc(a,b,c){return Ic(a,b,c,"weekdaysShort")}function Nc(a,b,c){return Ic(a,b,c,"weekdaysMin")}function Oc(){var a=this._data;return this._milliseconds=Jf(this._milliseconds),this._days=Jf(this._days),this._months=Jf(this._months),a.milliseconds=Jf(a.milliseconds),a.seconds=Jf(a.seconds),a.minutes=Jf(a.minutes),a.hours=Jf(a.hours),a.months=Jf(a.months),a.years=Jf(a.years),this}function Pc(a,b,c,d){var e=db(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function Qc(a,b){return Pc(this,a,b,1)}function Rc(a,b){return Pc(this,a,b,-1)}function Sc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Tc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Sc(Vc(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=q(f/1e3),i.seconds=a%60,b=q(a/60),i.minutes=b%60,c=q(b/60),i.hours=c%24,g+=q(c/24),e=q(Uc(g)),h+=e,g-=Sc(Vc(e)),d=q(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function Uc(a){return 4800*a/146097}function Vc(a){return 146097*a/4800}function Wc(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+Uc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(Vc(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function Xc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*r(this._months/12)}function Yc(a){return function(){return this.as(a)}}function Zc(a){
return a=K(a),this[a+"s"]()}function $c(a){return function(){return this._data[a]}}function _c(){return q(this.days()/7)}function ad(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function bd(a,b,c){var d=db(a).abs(),e=Zf(d.as("s")),f=Zf(d.as("m")),g=Zf(d.as("h")),h=Zf(d.as("d")),i=Zf(d.as("M")),j=Zf(d.as("y")),k=e<$f.s&&["s",e]||1>=f&&["m"]||f<$f.m&&["mm",f]||1>=g&&["h"]||g<$f.h&&["hh",g]||1>=h&&["d"]||h<$f.d&&["dd",h]||1>=i&&["M"]||i<$f.M&&["MM",i]||1>=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,ad.apply(null,k)}function cd(a,b){return void 0===$f[a]?!1:void 0===b?$f[a]:($f[a]=b,!0)}function dd(a){var b=this.localeData(),c=bd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function ed(){var a,b,c,d=_f(this._milliseconds)/1e3,e=_f(this._days),f=_f(this._months);a=q(d/60),b=q(a/60),d%=60,a%=60,c=q(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}
//! moment.js locale configuration
diff --git a/bower.json b/bower.json
index 786757a8..f299d87e 100644
--- a/bower.json
+++ b/bower.json
@@ -20,6 +20,6 @@
"font-awesome": "fontawesome#^4.6.3",
"d3": "~3.5.0",
"isMobile": "0.4.0",
- "select2": "^4.0.3"
+ "select2": "master"
}
}
diff --git a/doc/api-external-task-link-procedures.markdown b/doc/api-external-task-link-procedures.markdown
new file mode 100644
index 00000000..85f67b60
--- /dev/null
+++ b/doc/api-external-task-link-procedures.markdown
@@ -0,0 +1,221 @@
+External Task Link API Procedures
+=================================
+
+## getExternalTaskLinkTypes
+
+- Purpose: **Get all registered external link providers**
+- Parameters: **none**
+- Result on success: **dict**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"getExternalTaskLinkTypes","id":477370568}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": {
+ "auto": "Auto",
+ "attachment": "Attachment",
+ "file": "Local File",
+ "weblink": "Web Link"
+ },
+ "id": 477370568
+}
+```
+
+## getExternalTaskLinkProviderDependencies
+
+- Purpose: **Get available dependencies for a given provider**
+- Parameters:
+ - **providerName** (string, required)
+- Result on success: **dict**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"getExternalTaskLinkProviderDependencies","id":124790226,"params":["weblink"]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": {
+ "related": "Related"
+ },
+ "id": 124790226
+}
+```
+
+## createExternalTaskLink
+
+- Purpose: **Create a new external link**
+- Parameters:
+ - **task_id** (integer, required)
+ - **url** (string, required)
+ - **dependency** (string, required)
+ - **type** (string, optional)
+ - **title** (string, optional)
+- Result on success: **link_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"createExternalTaskLink","id":924217495,"params":[9,"http:\/\/localhost\/document.pdf","related","attachment"]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": 1,
+ "id": 924217495
+}
+```
+
+## updateExternalTaskLink
+
+- Purpose: **Update external task link**
+- Parameters:
+ - **task_id** (integer, required)
+ - **link_id** (integer, required)
+ - **title** (string, required)
+ - **url** (string, required)
+ - **dependency** (string, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc":"2.0",
+ "method":"updateExternalTaskLink",
+ "id":1123562620,
+ "params": {
+ "task_id":9,
+ "link_id":1,
+ "title":"New title"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": true,
+ "id": 1123562620
+}
+```
+
+## getExternalTaskLinkById
+
+- Purpose: **Get an external task link**
+- Parameters:
+ - **task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **dict**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"getExternalTaskLinkById","id":2107066744,"params":[9,1]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": {
+ "id": "1",
+ "link_type": "attachment",
+ "dependency": "related",
+ "title": "document.pdf",
+ "url": "http:\/\/localhost\/document.pdf",
+ "date_creation": "1466965256",
+ "date_modification": "1466965256",
+ "task_id": "9",
+ "creator_id": "0"
+ },
+ "id": 2107066744
+}
+```
+
+## getAllExternalTaskLinks
+
+- Purpose: **Get all external links attached to a task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **list of external links**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"getAllExternalTaskLinks","id":2069307223,"params":[9]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": [
+ {
+ "id": "1",
+ "link_type": "attachment",
+ "dependency": "related",
+ "title": "New title",
+ "url": "http:\/\/localhost\/document.pdf",
+ "date_creation": "1466965256",
+ "date_modification": "1466965256",
+ "task_id": "9",
+ "creator_id": "0",
+ "creator_name": null,
+ "creator_username": null,
+ "dependency_label": "Related",
+ "type": "Attachment"
+ }
+ ],
+ "id": 2069307223
+}
+```
+
+## removeExternalTaskLink
+
+- Purpose: **Remove an external link**
+- Parameters:
+ - **task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"removeExternalTaskLink","id":552055660,"params":[9,1]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": true,
+ "id": 552055660
+}
+```
diff --git a/doc/api-internal-task-link-procedures.markdown b/doc/api-internal-task-link-procedures.markdown
new file mode 100644
index 00000000..eca0d886
--- /dev/null
+++ b/doc/api-internal-task-link-procedures.markdown
@@ -0,0 +1,187 @@
+Internal Task Link API Procedures
+=================================
+
+## createTaskLink
+
+- Purpose: **Create a link between two tasks**
+- Parameters:
+ - **task_id** (integer, required)
+ - **opposite_task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **task_link_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createTaskLink",
+ "id": 509742912,
+ "params": [
+ 2,
+ 3,
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 509742912,
+ "result": 1
+}
+```
+
+## updateTaskLink
+
+- Purpose: **Update task link**
+- Parameters:
+ - **task_link_id** (integer, required)
+ - **task_id** (integer, required)
+ - **opposite_task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateTaskLink",
+ "id": 669037109,
+ "params": [
+ 1,
+ 2,
+ 4,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 669037109,
+ "result": true
+}
+```
+
+## getTaskLinkById
+
+- Purpose: **Get a task link**
+- Parameters:
+ - **task_link_id** (integer, required)
+- Result on success: **task link properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getTaskLinkById",
+ "id": 809885202,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 809885202,
+ "result": {
+ "id": "1",
+ "link_id": "1",
+ "task_id": "2",
+ "opposite_task_id": "3"
+ }
+}
+```
+
+## getAllTaskLinks
+
+- Purpose: **Get all links related to a task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **list of task link**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllTaskLinks",
+ "id": 810848359,
+ "params": [
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 810848359,
+ "result": [
+ {
+ "id": "1",
+ "task_id": "3",
+ "label": "relates to",
+ "title": "B",
+ "is_active": "1",
+ "project_id": "1",
+ "task_time_spent": "0",
+ "task_time_estimated": "0",
+ "task_assignee_id": "0",
+ "task_assignee_username": null,
+ "task_assignee_name": null,
+ "column_title": "Backlog"
+ }
+ ]
+}
+```
+
+## removeTaskLink
+
+- Purpose: **Remove a link between two tasks**
+- Parameters:
+ - **task_link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeTaskLink",
+ "id": 473028226,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 473028226,
+ "result": true
+}
+```
diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown
index 0f922a7c..ab1056f0 100644
--- a/doc/api-json-rpc.markdown
+++ b/doc/api-json-rpc.markdown
@@ -58,8 +58,12 @@ Usage
- [Automatic Actions](api-action-procedures.markdown)
- [Tasks](api-task-procedures.markdown)
- [Subtasks](api-subtask-procedures.markdown)
-- [Files](api-file-procedures.markdown)
+- [Subtask Time Tracking](api-subtask-time-tracking-procedures.markdown)
+- [Task Files](api-task-file-procedures.markdown)
+- [Project Files](api-project-file-procedures.markdown)
- [Links](api-link-procedures.markdown)
+- [Internal Task Links](api-internal-task-link-procedures.markdown)
+- [External Task Links](api-external-task-link-procedures.markdown)
- [Comments](api-comment-procedures.markdown)
- [Users](api-user-procedures.markdown)
- [Groups](api-group-procedures.markdown)
diff --git a/doc/api-link-procedures.markdown b/doc/api-link-procedures.markdown
index 6113316f..44e78a2a 100644
--- a/doc/api-link-procedures.markdown
+++ b/doc/api-link-procedures.markdown
@@ -283,188 +283,3 @@ Response example:
"result": true
}
```
-
-## createTaskLink
-
-- Purpose: **Create a link between two tasks**
-- Parameters:
- - **task_id** (integer, required)
- - **opposite_task_id** (integer, required)
- - **link_id** (integer, required)
-- Result on success: **task_link_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createTaskLink",
- "id": 509742912,
- "params": [
- 2,
- 3,
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 509742912,
- "result": 1
-}
-```
-
-## updateTaskLink
-
-- Purpose: **Update task link**
-- Parameters:
- - **task_link_id** (integer, required)
- - **task_id** (integer, required)
- - **opposite_task_id** (integer, required)
- - **link_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateTaskLink",
- "id": 669037109,
- "params": [
- 1,
- 2,
- 4,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 669037109,
- "result": true
-}
-```
-
-## getTaskLinkById
-
-- Purpose: **Get a task link**
-- Parameters:
- - **task_link_id** (integer, required)
-- Result on success: **task link properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getTaskLinkById",
- "id": 809885202,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 809885202,
- "result": {
- "id": "1",
- "link_id": "1",
- "task_id": "2",
- "opposite_task_id": "3"
- }
-}
-```
-
-## getAllTaskLinks
-
-- Purpose: **Get all links related to a task**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **list of task link**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllTaskLinks",
- "id": 810848359,
- "params": [
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 810848359,
- "result": [
- {
- "id": "1",
- "task_id": "3",
- "label": "relates to",
- "title": "B",
- "is_active": "1",
- "project_id": "1",
- "task_time_spent": "0",
- "task_time_estimated": "0",
- "task_assignee_id": "0",
- "task_assignee_username": null,
- "task_assignee_name": null,
- "column_title": "Backlog"
- }
- ]
-}
-```
-
-## removeTaskLink
-
-- Purpose: **Remove a link between two tasks**
-- Parameters:
- - **task_link_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeTaskLink",
- "id": 473028226,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 473028226,
- "result": true
-}
-```
diff --git a/doc/api-project-file-procedures.markdown b/doc/api-project-file-procedures.markdown
new file mode 100644
index 00000000..fdc5da1a
--- /dev/null
+++ b/doc/api-project-file-procedures.markdown
@@ -0,0 +1,221 @@
+Project File API Procedures
+===========================
+
+## createProjectFile
+
+- Purpose: **Create and upload a new project attachment**
+- Parameters:
+ - **project_id** (integer, required)
+ - **filename** (integer, required)
+ - **blob** File content encoded in base64 (string, required)
+- Result on success: **file_id**
+- Result on failure: **false**
+- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createProjectFile",
+ "id": 94500810,
+ "params": [
+ 1,
+ "My file",
+ "cGxhaW4gdGV4dCBmaWxl"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 94500810,
+ "result": 1
+}
+```
+
+## getAllProjectFiles
+
+- Purpose: **Get all files attached to a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **list of files**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllProjectFiles",
+ "id": 1880662820,
+ "params": {
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1880662820,
+ "result": [
+ {
+ "id": "1",
+ "name": "My file",
+ "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
+ "is_image": "0",
+ "project_id": "1",
+ "date": "1432509941",
+ "user_id": "0",
+ "size": "15",
+ "username": null,
+ "user_name": null
+ }
+ ]
+}
+```
+
+## getProjectFile
+
+- Purpose: **Get file information**
+- Parameters:
+ - **project_id** (integer, required)
+ - **file_id** (integer, required)
+- Result on success: **file properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectFile",
+ "id": 318676852,
+ "params": [
+ "42",
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 318676852,
+ "result": {
+ "id": "1",
+ "name": "My file",
+ "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
+ "is_image": "0",
+ "project_id": "1",
+ "date": "1432509941",
+ "user_id": "0",
+ "size": "15"
+ }
+}
+```
+
+## downloadProjectFile
+
+- Purpose: **Download project file contents (encoded in base64)**
+- Parameters:
+ - **project_id** (integer, required)
+ - **file_id** (integer, required)
+- Result on success: **base64 encoded string**
+- Result on failure: **empty string**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "downloadProjectFile",
+ "id": 235943344,
+ "params": [
+ "1",
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 235943344,
+ "result": "cGxhaW4gdGV4dCBmaWxl"
+}
+```
+
+## removeProjectFile
+
+- Purpose: **Remove a file associated to a project**
+- Parameters:
+ - **project_id** (integer, required)
+ - **file_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeProjectFile",
+ "id": 447036524,
+ "params": [
+ "1",
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 447036524,
+ "result": true
+}
+```
+
+## removeAllProjectFiles
+
+- Purpose: **Remove all files associated to a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeAllProjectFiles",
+ "id": 593312993,
+ "params": {
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 593312993,
+ "result": true
+}
+```
diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown
index d375852c..09000e68 100644
--- a/doc/api-project-procedures.markdown
+++ b/doc/api-project-procedures.markdown
@@ -133,6 +133,55 @@ Response example:
}
```
+## getProjectByIdentifier
+
+- Purpose: **Get project information**
+- Parameters:
+ - **identifier** (string, required)
+- Result on success: **project properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectByIdentifier",
+ "id": 1620253806,
+ "params": {
+ "identifier": "TEST"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1620253806,
+ "result": {
+ "id": "1",
+ "name": "Test",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1436119135",
+ "is_public": "0",
+ "is_private": "0",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": "test",
+ "identifier": "TEST",
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
+ }
+ }
+}
+```
+
## getAllProjects
- Purpose: **Get all available projects**
diff --git a/doc/api-subtask-time-tracking-procedures.markdown b/doc/api-subtask-time-tracking-procedures.markdown
new file mode 100644
index 00000000..67447623
--- /dev/null
+++ b/doc/api-subtask-time-tracking-procedures.markdown
@@ -0,0 +1,102 @@
+Subtask Time Tracking API procedures
+====================================
+
+## hasSubtaskTimer
+
+- Purpose: **Check if a timer is started for the given subtask and user**
+- Parameters:
+ - **subtask_id** (integer, required)
+ - **user_id** (integer, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"hasSubtaskTimer","id":1786995697,"params":[2,4]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": true,
+ "id": 1786995697
+}
+```
+
+## setSubtaskStartTime
+
+- Purpose: **Start subtask timer for a user**
+- Parameters:
+ - **subtask_id** (integer, required)
+ - **user_id** (integer, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"setSubtaskStartTime","id":1168991769,"params":[2,4]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": true,
+ "id": 1168991769
+}
+```
+
+## setSubtaskEndTime
+
+- Purpose: **Stop subtask timer for a user**
+- Parameters:
+ - **subtask_id** (integer, required)
+ - **user_id** (integer, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"setSubtaskEndTime","id":1026607603,"params":[2,4]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": true,
+ "id": 1026607603
+}
+```
+
+## getSubtaskTimeSpent
+
+- Purpose: **Get time spent on a subtask for a user**
+- Parameters:
+ - **subtask_id** (integer, required)
+ - **user_id** (integer, optional)
+- Result on success: **number of hours**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{"jsonrpc":"2.0","method":"getSubtaskTimeSpent","id":738527378,"params":[2,4]}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "result": 1.5,
+ "id": 738527378
+}
+```
diff --git a/doc/api-file-procedures.markdown b/doc/api-task-file-procedures.markdown
index 930be733..51840bea 100644
--- a/doc/api-file-procedures.markdown
+++ b/doc/api-task-file-procedures.markdown
@@ -1,5 +1,5 @@
-API File Procedures
-===================
+Task File API Procedures
+========================
## createTaskFile
diff --git a/doc/creating-tasks.markdown b/doc/creating-tasks.markdown
index 2ebdbb9c..99dce713 100644
--- a/doc/creating-tasks.markdown
+++ b/doc/creating-tasks.markdown
@@ -3,25 +3,28 @@ Creating Tasks
From the board, click on the plus sign next to the column name:
-![Task creation from the board](https://kanboard.net/screenshots/documentation/task-creation-board.png)
+![Task creation from the board](screenshots/task-creation-board.png)
Then the task creation form appears:
-![Task creation form](https://kanboard.net/screenshots/documentation/task-creation-form.png)
-
-The only mandatory field is the title.
+![Task creation form](screenshots/task-creation-form.png)
Field description:
- **Title**: The title of your task, which will be displayed on the board.
-- **Description**: Allow you to add more information about the task, the content can be written in [Markdown](https://kanboard.net/documentation/syntax-guide).
+- **Description**: Description that use the [Markdown](syntax-guide.markdown) format.
+- **Tags**: The list of tags associated to tasks.
- **Create another task**: Check this box if you want to create a similar task (some fields will be pre-filled).
+- **Color**: Choose the color of the card.
- **Assignee**: The person that will work on the task.
-- **Category**: Only one category can be assigned to a task.
+- **Category**: Only one category can be assigned to a task (visible only if the projects have categories).
- **Column**: The column where the task will be created, your task will be positioned at the bottom.
-- **Color**: Choose the color of the card.
+- **Priority**: Task priority, the range can be defined in the project settings, default values are P0 to P3.
- **Complexity**: Used in agile project management (Scrum), the complexity or story points is a number that tells the team how hard the story is. Often, people use the Fibonacci series.
-- **Original Estimate**: Estimation in hours to complete the tasks.
+- **Reference**: External ID for the task, for example it can be ticket number that come from another system
+- **Original Estimate**: Estimation in hours to complete the task.
+- **Time Spent**: Time spent working on the task.
+- **Start Date**: This is a date time field.
- **Due Date**: Overdue tasks will have a red due date and upcoming due dates will be black on the board. Several date format are accepted in addition to the date picker.
With the preview link, you can see the task description converted from the Markdown syntax.
diff --git a/doc/docker.markdown b/doc/docker.markdown
index 3f13e954..e55130e5 100644
--- a/doc/docker.markdown
+++ b/doc/docker.markdown
@@ -3,17 +3,17 @@ How to run Kanboard with Docker?
Kanboard can run easily with [Docker](https://www.docker.com).
-The image size is approximately **50MB** and contains:
+The image size is approximately **70MB** and contains:
- [Alpine Linux](http://alpinelinux.org/)
- The [process manager S6](http://skarnet.org/software/s6/)
- Nginx
-- PHP-FPM
+- PHP 7
The Kanboard cronjob is also running everyday at midnight.
URL rewriting is enabled in the included config file.
-When the container is running, the memory utilization is around **20MB**.
+When the container is running, the memory utilization is around **30MB**.
Use the stable version
----------------------
@@ -64,8 +64,8 @@ Volumes
You can attach 2 volumes to your container:
-- Data folder: `/var/www/kanboard/data`
-- Plugins folder: `/var/www/kanboard/plugins`
+- Data folder: `/var/www/app/data`
+- Plugins folder: `/var/www/app/plugins`
Use the flag `-v` to mount a volume on the host machine like described in [official Docker documentation](https://docs.docker.com/engine/userguide/containers/dockervolumes/).
@@ -84,8 +84,8 @@ The list of environment variables is available on [this page](env.markdown).
Config files
------------
-- The container already include a custom config file located at `/var/www/kanboard/config.php`.
-- You can store your own config file on the data volume: `/var/www/kanboard/data/config.php`.
+- The container already include a custom config file located at `/var/www/app/config.php`.
+- You can store your own config file on the data volume: `/var/www/app/data/config.php`.
References
----------
diff --git a/doc/env.markdown b/doc/env.markdown
index 28f14b18..902066d7 100644
--- a/doc/env.markdown
+++ b/doc/env.markdown
@@ -7,4 +7,3 @@ Environment variables maybe useful when Kanboard is deployed as container (Docke
|---------------|---------------------------------------------------------------------------------------------------------------------------------|
| DATABASE_URL | `[database type]://[username]:[password]@[host]:[port]/[database name]`, example: `postgres://foo:foo@myserver:5432/kanboard` |
| DEBUG | Enable/Disable debug mode: "true" or "false" |
-| LOG_DRIVER | Logging driver: stdout, stderr, file or syslog |
diff --git a/doc/fr_FR/creating-tasks.markdown b/doc/fr_FR/creating-tasks.markdown
index 9b7fa274..c3cfed01 100644
--- a/doc/fr_FR/creating-tasks.markdown
+++ b/doc/fr_FR/creating-tasks.markdown
@@ -3,25 +3,33 @@ Créer des tâches
Depuis le tableau, cliquez sur le signe plus + à côté du nom de la colonne :
-![Création de tâche à partir du tableau](https://kanboard.net/screenshots/documentation/task-creation-board.png)
+![Création de tâche à partir du tableau](screenshots/task-creation-board.png)
Le formulaire de création de tâche apparaît :
-![Formulaire de création de tâche](https://kanboard.net/screenshots/documentation/task-creation-form.png)
+![Formulaire de création de tâche](screenshots/task-creation-form.png)
Le seul champ obligatoire est le titre.
Description des champs :
- **Titre** : le titre de votre tâche, tel qu'il sera affiché sur le tableau.
-- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](https://kanboard.net/documentation/syntax-guide).
+- **Description** : vous permet d'ajouter davantage d'informations sur la tâche. Le contenu peut être écrit en [Markdown](syntax-guide.markdown).
+- **Libellés**: Liste de libellés associés à la tâche.
- **Créer une autre tâche** : cochez cette case si vous souhaitez créer une tâche similaire (les champs seront pré-remplis).
- **Assigné** : la personne qui va travailler sur la tâche.
- **Catégorie** : une seule catégorie peut être assignée à une tâche.
- **Colonne** : la colonne dans laquelle la tâche sera créée. La tâche sera positionnée en bas de cette colonne.
- **Couleur** : Choisissez la couleur de la carte.
- **Complexité** : utilisée dans la gestion de projet agile (Scrum), la complexité des points d'étape est un nombre qui montre à l'équipe le degré de difficulté de l'avancement du projet. Les utilisateurs se servent souvent des suites de Fibonacci.
+- **Référence** : Identifiant externe, par exemple cela peut-être un numéro de ticket qui vient d'un système externe.
- **Estimation originale** : estimation du nombre d'heures nécessaire pour terminer les tâches.
- **Date d'échéance** : les tâches dont la date d'échéance est dépassée auront une date d'échéance en rouge et les dates suivantes seront en noir dans le tableau. Plusieurs formats de date sont acceptés, outre le sélecteur de date.
Avec le lien d'aperçu (« Prévisualiser »), vous pouvez voir la description de la tâche convertie depuis la syntaxe Markdown.
+
+Vous créer une tâche de plusieurs manières :
+
+- Avec l'icône avec le signe plus sur le board
+- Avec le raccourci clavier "n"
+- Depuis le menu déroulant en haut à gauche
diff --git a/doc/fr_FR/index.markdown b/doc/fr_FR/index.markdown
index f74c3fce..a73c5c23 100644
--- a/doc/fr_FR/index.markdown
+++ b/doc/fr_FR/index.markdown
@@ -22,6 +22,7 @@ Utiliser Kanboard
- [Types de projets](project-types.markdown)
- [Créer des projets](creating-projects.markdown)
- [Modifier des projets](editing-projects.markdown)
+- [Supprimer des projets](removing-projects.markdown)
- [Partager des tableaux et des tâches](sharing-projects.markdown)
- [Actions automatiques](automatic-actions.markdown)
- [Permissions des projets](project-permissions.markdown)
diff --git a/doc/fr_FR/removing-projects.markdown b/doc/fr_FR/removing-projects.markdown
new file mode 100644
index 00000000..1b64191e
--- /dev/null
+++ b/doc/fr_FR/removing-projects.markdown
@@ -0,0 +1,10 @@
+Supprimer des projets
+=====================
+
+Pour supprimer un projet, vous devez être gestionnaire du projet ou administrateur.
+
+Aller dans les **Préférences du projet**, depuis le menu à gauche, en bas, choisissez **Supprimer**.
+
+![Supprimer un Projet](screenshots/project-remove.png)
+
+Supprimer un projet, supprime également toutes les tâches qui appartiennent à ce projet.
diff --git a/doc/fr_FR/screenshots/task-creation-board.png b/doc/fr_FR/screenshots/task-creation-board.png
new file mode 100644
index 00000000..18f13b3f
--- /dev/null
+++ b/doc/fr_FR/screenshots/task-creation-board.png
Binary files differ
diff --git a/doc/fr_FR/screenshots/task-creation-form.png b/doc/fr_FR/screenshots/task-creation-form.png
new file mode 100644
index 00000000..5e4b455e
--- /dev/null
+++ b/doc/fr_FR/screenshots/task-creation-form.png
Binary files differ
diff --git a/doc/index.markdown b/doc/index.markdown
index c1e9a506..bc3e8a32 100644
--- a/doc/index.markdown
+++ b/doc/index.markdown
@@ -22,6 +22,7 @@ Using Kanboard
- [Project Types](project-types.markdown)
- [Creating projects](creating-projects.markdown)
- [Editing projects](editing-projects.markdown)
+- [Removing projects](removing-projects.markdown)
- [Sharing boards and tasks](sharing-projects.markdown)
- [Automatic actions](automatic-actions.markdown)
- [Project permissions](project-permissions.markdown)
@@ -46,6 +47,7 @@ Using Kanboard
- [Subtasks](subtasks.markdown)
- [Analytics for tasks](analytics-tasks.markdown)
- [User mentions](user-mentions.markdown)
+- [Tags](tags.markdown)
### Working with users and groups
diff --git a/doc/nice-urls.markdown b/doc/nice-urls.markdown
index 9fbb3510..bfea719d 100644
--- a/doc/nice-urls.markdown
+++ b/doc/nice-urls.markdown
@@ -88,18 +88,25 @@ define('ENABLE_URL_REWRITE', true);
Adapt the example above according to your own configuration.
IIS configuration example
----------------------------
+-------------------------
-Create a web.config in you installation folder:
+1. Download and install the Rewrite module for IIS: [Download link](http://www.iis.net/learn/extensions/url-rewrite-module/using-the-url-rewrite-module)
+2. Create a web.config in you installation folder:
```xml
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0"?>
<configuration>
<system.webServer>
+ <defaultDocument>
+ <files>
+ <clear />
+ <add value="index.php" />
+ </files>
+ </defaultDocument>
<rewrite>
<rules>
- <rule name="Imported Rule 1" stopProcessing="true">
- <match url="^" ignoreCase="false" />
+ <rule name="Kanboard URL Rewrite" stopProcessing="true">
+ <match url="^(.*)$" ignoreCase="false" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
</conditions>
@@ -109,7 +116,6 @@ Create a web.config in you installation folder:
</rewrite>
</system.webServer>
</configuration>
-
```
In your Kanboard `config.php`:
diff --git a/doc/removing-projects.markdown b/doc/removing-projects.markdown
new file mode 100644
index 00000000..f9e622cb
--- /dev/null
+++ b/doc/removing-projects.markdown
@@ -0,0 +1,10 @@
+Removing Projects
+=================
+
+To remove a project, you must be manager of the project or administrator.
+
+Go to the **"Project settings"**, and from the menu on the left, at the bottom, choose **"Remove"**.
+
+![Removing Projects](screenshots/project-remove.png)
+
+Removing a project remove all tasks that belongs to this project.
diff --git a/doc/screenshots/project-remove.png b/doc/screenshots/project-remove.png
new file mode 100644
index 00000000..d1c33cc1
--- /dev/null
+++ b/doc/screenshots/project-remove.png
Binary files differ
diff --git a/doc/screenshots/tags-board.png b/doc/screenshots/tags-board.png
new file mode 100644
index 00000000..1a7f710d
--- /dev/null
+++ b/doc/screenshots/tags-board.png
Binary files differ
diff --git a/doc/screenshots/tags-global.png b/doc/screenshots/tags-global.png
new file mode 100644
index 00000000..f5059ae8
--- /dev/null
+++ b/doc/screenshots/tags-global.png
Binary files differ
diff --git a/doc/screenshots/tags-projects.png b/doc/screenshots/tags-projects.png
new file mode 100644
index 00000000..2e08ce90
--- /dev/null
+++ b/doc/screenshots/tags-projects.png
Binary files differ
diff --git a/doc/screenshots/tags-search.png b/doc/screenshots/tags-search.png
new file mode 100644
index 00000000..eab1ad80
--- /dev/null
+++ b/doc/screenshots/tags-search.png
Binary files differ
diff --git a/doc/screenshots/tags-task.png b/doc/screenshots/tags-task.png
new file mode 100644
index 00000000..c9c8b2f7
--- /dev/null
+++ b/doc/screenshots/tags-task.png
Binary files differ
diff --git a/doc/screenshots/task-creation-board.png b/doc/screenshots/task-creation-board.png
new file mode 100644
index 00000000..456dff0b
--- /dev/null
+++ b/doc/screenshots/task-creation-board.png
Binary files differ
diff --git a/doc/screenshots/task-creation-form.png b/doc/screenshots/task-creation-form.png
new file mode 100644
index 00000000..d1e5d24e
--- /dev/null
+++ b/doc/screenshots/task-creation-form.png
Binary files differ
diff --git a/doc/tags.markdown b/doc/tags.markdown
new file mode 100644
index 00000000..26b3169d
--- /dev/null
+++ b/doc/tags.markdown
@@ -0,0 +1,28 @@
+Tags
+====
+
+With Kanboard, you can associate one or many tags to a task.
+You can define tags globally for all projects or only for a specific project.
+
+![Tags on the board](screenshots/tags-board.png)
+
+From the task form, you can enter the desired tags:
+
+![Tags form](screenshots/tags-task.png)
+
+The auto-completion form will show up to suggest available tags.
+
+You can also create tags directly from the task form.
+By default, when you create tags from a task form they are associated to the current project:
+
+![Project Tags](screenshots/tags-projects.png)
+
+All tags can be managed in the project settings.
+
+To define tags globally for all projects, go to the application settings:
+
+![Global Tags](screenshots/tags-global.png)
+
+To search tasks based on tags, just use the attribute "tag":
+
+![Search Tags](screenshots/tags-search.png)
diff --git a/doc/windows-iis-installation.markdown b/doc/windows-iis-installation.markdown
index bd4607de..26ce178f 100644
--- a/doc/windows-iis-installation.markdown
+++ b/doc/windows-iis-installation.markdown
@@ -12,7 +12,10 @@ PHP installation
- [Microsoft IIS 7.0 and later](http://php.net/manual/en/install.windows.iis7.php)
- [PHP for Windows is available here](http://windows.php.net/download/)
-Edit the `php.ini`, uncomment these PHP modules:
+
+### PHP.ini
+
+You need at least, these extensions in your `php.ini`:
```ini
extension=php_gd2.dll
@@ -22,7 +25,9 @@ extension=php_openssl.dll
extension=php_pdo_sqlite.dll
```
-Set the time zone:
+The complete list of required PHP extensions is available on the [requirements page](requirements.markdown)
+
+Do not forget to set the time zone:
```ini
date.timezone = America/Montreal
@@ -30,27 +35,21 @@ date.timezone = America/Montreal
The list of supported time zones can be found in the [PHP documentation](http://php.net/manual/en/timezones.america.php).
-Check if PHP runs correctly:
-
-Go the IIS document root `C:\inetpub\wwwroot` and create a file `phpinfo.php`:
-
-```php
-<?php
-
-phpinfo();
-
-?>
-```
-
-Open a browser at `http://localhost/phpinfo.php` and you should see the current PHP settings.
-If you got an error 500, something is not correctly done in your installation.
-
Notes:
- If you use PHP < 5.4, you have to enable the short tags in your php.ini
- Don't forget to enable the required php extensions mentioned above
- If you got an error about "the library MSVCP110.dll is missing", you probably need to download the Visual C++ Redistributable for Visual Studio from the Microsoft website.
+IIS Modules
+-----------
+
+The Kanboard archive contains a `web.config` file to enable [URL rewriting](nice-urls.markdown).
+This configuration require the [Rewrite module for IIS](http://www.iis.net/learn/extensions/url-rewrite-module/using-the-url-rewrite-module).
+
+If you don't have the rewrite module, you will get an internal server error (500) from IIS.
+If you don't want to have Kanboard with nice URLs, you can remove the file `web.config`.
+
Kanboard installation
---------------------
@@ -59,12 +58,7 @@ Kanboard installation
- Make sure the directory `data` is writable by the IIS user
- Open your web browser to use Kanboard http://localhost/kanboard/
- The default credentials are **admin/admin**
-
-Tested configurations
----------------------
-
-- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16
-- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29
+- [URL rewrite configuration](nice-urls.markdown)
Notes
-----
diff --git a/docker-compose.yml b/docker-compose.yml
index aa0a6710..9b618cf6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,8 +5,8 @@ services:
ports:
- "80:80"
volumes:
- - kanboard_data:/var/www/kanboard/data
- - kanboard_plugins:/var/www/kanboard/plugins
+ - kanboard_data:/var/www/app/data
+ - kanboard_plugins:/var/www/app/plugins
volumes:
kanboard_data:
driver: local
diff --git a/docker/crontab/cronjob.alpine b/docker/crontab/cronjob.alpine
new file mode 100644
index 00000000..d051ff28
--- /dev/null
+++ b/docker/crontab/cronjob.alpine
@@ -0,0 +1 @@
+1 0 * * * cd /var/www/app && ./kanboard cronjob
diff --git a/.docker/kanboard/config.php b/docker/kanboard/config.php
index fa1c5971..58daff48 100644
--- a/.docker/kanboard/config.php
+++ b/docker/kanboard/config.php
@@ -1,3 +1,4 @@
<?php
define('ENABLE_URL_REWRITE', true);
+define('LOG_DRIVER', 'stderr');
diff --git a/docker/php/env.conf b/docker/php/env.conf
new file mode 100644
index 00000000..bcf2e37c
--- /dev/null
+++ b/docker/php/env.conf
@@ -0,0 +1,2 @@
+env[DATABASE_URL] = $DATABASE_URL
+env[DEBUG] = $DEBUG
diff --git a/.docker/services.d/cron/run b/docker/services.d/cron/run
index da378099..da378099 100755
--- a/.docker/services.d/cron/run
+++ b/docker/services.d/cron/run
diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh
index a88c7ed8..5a37ae4e 100755
--- a/tests/docker/entrypoint.sh
+++ b/tests/docker/entrypoint.sh
@@ -23,7 +23,7 @@ case "$1" in
/var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.sqlite.xml
;;
"integration-test-postgres")
- wait_schema_creation 5
+ wait_schema_creation 10
/var/www/html/vendor/phpunit/phpunit/phpunit -c /var/www/html/tests/integration.postgres.xml
;;
"integration-test-mysql")
diff --git a/tests/integration/BaseProcedureTest.php b/tests/integration/BaseProcedureTest.php
index e3382e82..beb13ff7 100644
--- a/tests/integration/BaseProcedureTest.php
+++ b/tests/integration/BaseProcedureTest.php
@@ -17,6 +17,7 @@ abstract class BaseProcedureTest extends PHPUnit_Framework_TestCase
protected $projectId = 0;
protected $taskTitle = 'My task';
protected $taskId = 0;
+ protected $subtaskId = 0;
protected $groupName1 = 'My Group A';
protected $groupName2 = 'My Group B';
@@ -119,4 +120,14 @@ abstract class BaseProcedureTest extends PHPUnit_Framework_TestCase
$this->taskId = $this->app->createTask(array('title' => $this->taskTitle, 'project_id' => $this->projectId));
$this->assertNotFalse($this->taskId);
}
+
+ public function assertCreateSubtask()
+ {
+ $this->subtaskId = $this->app->createSubtask(array(
+ 'task_id' => $this->taskId,
+ 'title' => 'subtask #1',
+ ));
+
+ $this->assertNotFalse($this->subtaskId);
+ }
}
diff --git a/tests/integration/ProjectFileProcedureTest.php b/tests/integration/ProjectFileProcedureTest.php
new file mode 100644
index 00000000..8ac70d87
--- /dev/null
+++ b/tests/integration/ProjectFileProcedureTest.php
@@ -0,0 +1,66 @@
+<?php
+
+require_once __DIR__.'/BaseProcedureTest.php';
+
+class ProjectFileProcedureTest extends BaseProcedureTest
+{
+ protected $projectName = 'My project to test project files';
+ protected $fileId;
+
+ public function testAll()
+ {
+ $this->assertCreateTeamProject();
+ $this->assertCreateProjectFile();
+ $this->assertGetProjectFile();
+ $this->assertDownloadProjectFile();
+ $this->assertGetAllFiles();
+ $this->assertRemoveProjectFile();
+ $this->assertRemoveAllProjectFiles();
+ }
+
+ public function assertCreateProjectFile()
+ {
+ $this->fileId = $this->app->createProjectFile($this->projectId, 'My file.txt', base64_encode('plain text file'));
+ $this->assertNotFalse($this->fileId);
+ }
+
+ public function assertGetProjectFile()
+ {
+ $file = $this->app->getProjectFile($this->projectId, $this->fileId);
+ $this->assertNotEmpty($file);
+ $this->assertEquals('My file.txt', $file['name']);
+ }
+
+ public function assertDownloadProjectFile()
+ {
+ $content = $this->app->downloadProjectFile($this->projectId, $this->fileId);
+ $this->assertNotEmpty($content);
+ $this->assertEquals('plain text file', base64_decode($content));
+ }
+
+ public function assertGetAllFiles()
+ {
+ $files = $this->app->getAllProjectFiles($this->projectId);
+ $this->assertCount(1, $files);
+ $this->assertEquals('My file.txt', $files[0]['name']);
+ }
+
+ public function assertRemoveProjectFile()
+ {
+ $this->assertTrue($this->app->removeProjectFile($this->projectId, $this->fileId));
+
+ $files = $this->app->getAllProjectFiles($this->projectId);
+ $this->assertEmpty($files);
+ }
+
+ public function assertRemoveAllProjectFiles()
+ {
+ $this->assertCreateProjectFile();
+ $this->assertCreateProjectFile();
+
+ $this->assertTrue($this->app->removeAllProjectFiles($this->projectId));
+
+ $files = $this->app->getAllProjectFiles($this->projectId);
+ $this->assertEmpty($files);
+ }
+}
diff --git a/tests/integration/SubtaskProcedureTest.php b/tests/integration/SubtaskProcedureTest.php
index 7ab4ef0b..b9868e6f 100644
--- a/tests/integration/SubtaskProcedureTest.php
+++ b/tests/integration/SubtaskProcedureTest.php
@@ -5,7 +5,6 @@ require_once __DIR__.'/BaseProcedureTest.php';
class SubtaskProcedureTest extends BaseProcedureTest
{
protected $projectName = 'My project to test subtasks';
- private $subtaskId = 0;
public function testAll()
{
@@ -18,16 +17,6 @@ class SubtaskProcedureTest extends BaseProcedureTest
$this->assertRemoveSubtask();
}
- public function assertCreateSubtask()
- {
- $this->subtaskId = $this->app->createSubtask(array(
- 'task_id' => $this->taskId,
- 'title' => 'subtask #1',
- ));
-
- $this->assertNotFalse($this->subtaskId);
- }
-
public function assertGetSubtask()
{
$subtask = $this->app->getSubtask($this->subtaskId);
diff --git a/tests/integration/SubtaskTimeTrackingProcedureTest.php b/tests/integration/SubtaskTimeTrackingProcedureTest.php
new file mode 100644
index 00000000..6c45c983
--- /dev/null
+++ b/tests/integration/SubtaskTimeTrackingProcedureTest.php
@@ -0,0 +1,46 @@
+<?php
+
+require_once __DIR__.'/BaseProcedureTest.php';
+
+class SubtaskTimeTrackingProcedureTest extends BaseProcedureTest
+{
+ protected $projectName = 'My project to test subtask time tracking';
+
+ public function testAll()
+ {
+ $this->assertCreateTeamProject();
+ $this->assertCreateTask();
+ $this->assertCreateSubtask();
+ $this->assertHasNoTimer();
+ $this->assertStartTimer();
+ $this->assertHasTimer();
+ $this->assertStopTimer();
+ $this->assertHasNoTimer();
+ $this->assertGetSubtaskTimeSpent();
+ }
+
+ public function assertHasNoTimer()
+ {
+ $this->assertFalse($this->app->hasSubtaskTimer($this->subtaskId, $this->userUserId));
+ }
+
+ public function assertHasTimer()
+ {
+ $this->assertTrue($this->app->hasSubtaskTimer($this->subtaskId, $this->userUserId));
+ }
+
+ public function assertStartTimer()
+ {
+ $this->assertTrue($this->app->setSubtaskStartTime($this->subtaskId, $this->userUserId));
+ }
+
+ public function assertStopTimer()
+ {
+ $this->assertTrue($this->app->setSubtaskEndTime($this->subtaskId, $this->userUserId));
+ }
+
+ public function assertGetSubtaskTimeSpent()
+ {
+ $this->assertEquals(0, $this->app->getSubtaskTimeSpent($this->subtaskId, $this->userUserId));
+ }
+}
diff --git a/tests/integration/TaskExternalLinkProcedureTest.php b/tests/integration/TaskExternalLinkProcedureTest.php
new file mode 100644
index 00000000..47ff53ad
--- /dev/null
+++ b/tests/integration/TaskExternalLinkProcedureTest.php
@@ -0,0 +1,98 @@
+<?php
+
+require_once __DIR__.'/BaseProcedureTest.php';
+
+class TaskExternalLinkProcedureTest extends BaseProcedureTest
+{
+ protected $projectName = 'My project to test external links';
+ private $linkId = 0;
+
+ public function testAll()
+ {
+ $this->assertCreateTeamProject();
+ $this->assertCreateTask();
+ $this->assertGetExternalTaskLinkTypes();
+ $this->assertGetExternalTaskLinkProviderDependencies();
+ $this->assertGetExternalTaskLinkProviderDependenciesWithProviderNotFound();
+ $this->assertCreateExternalTaskLink();
+ $this->assertUpdateExternalTaskLink();
+ $this->assertGetAllExternalTaskLinks();
+ $this->assertRemoveExternalTaskLink();
+ }
+
+ public function assertGetExternalTaskLinkTypes()
+ {
+ $expected = array(
+ 'auto' => 'Auto',
+ 'attachment' => 'Attachment',
+ 'file' => 'Local File',
+ 'weblink' => 'Web Link',
+ );
+
+ $types = $this->app->getExternalTaskLinkTypes();
+ $this->assertEquals($expected, $types);
+ }
+
+ public function assertGetExternalTaskLinkProviderDependencies()
+ {
+ $expected = array(
+ 'related' => 'Related',
+ );
+
+ $dependencies = $this->app->getExternalTaskLinkProviderDependencies('weblink');
+
+ $this->assertEquals($expected, $dependencies);
+ }
+
+ public function assertGetExternalTaskLinkProviderDependenciesWithProviderNotFound()
+ {
+ $this->assertFalse($this->app->getExternalTaskLinkProviderDependencies('foobar'));
+ }
+
+ public function assertCreateExternalTaskLink()
+ {
+ $url = 'http://localhost/document.pdf';
+ $this->linkId = $this->app->createExternalTaskLink($this->taskId, $url, 'related', 'attachment');
+ $this->assertNotFalse($this->linkId);
+
+ $link = $this->app->getExternalTaskLinkById($this->taskId, $this->linkId);
+ $this->assertEquals($this->linkId, $link['id']);
+ $this->assertEquals($this->taskId, $link['task_id']);
+ $this->assertEquals('document.pdf', $link['title']);
+ $this->assertEquals($url, $link['url']);
+ $this->assertEquals('related', $link['dependency']);
+ $this->assertEquals(0, $link['creator_id']);
+ }
+
+ public function assertUpdateExternalTaskLink()
+ {
+ $this->assertTrue($this->app->updateExternalTaskLink(array(
+ 'task_id' => $this->taskId,
+ 'link_id' => $this->linkId,
+ 'title' => 'New title',
+ )));
+
+ $link = $this->app->getExternalTaskLinkById($this->taskId, $this->linkId);
+ $this->assertEquals($this->linkId, $link['id']);
+ $this->assertEquals($this->taskId, $link['task_id']);
+ $this->assertEquals('New title', $link['title']);
+ $this->assertEquals('related', $link['dependency']);
+ $this->assertEquals(0, $link['creator_id']);
+ }
+
+ public function assertGetAllExternalTaskLinks()
+ {
+ $links = $this->app->getAllExternalTaskLinks($this->taskId);
+ $this->assertCount(1, $links);
+ $this->assertEquals($this->linkId, $links[0]['id']);
+ $this->assertEquals($this->taskId, $links[0]['task_id']);
+ $this->assertEquals('New title', $links[0]['title']);
+ $this->assertEquals('related', $links[0]['dependency']);
+ $this->assertEquals(0, $links[0]['creator_id']);
+ }
+
+ public function assertRemoveExternalTaskLink()
+ {
+ $this->assertTrue($this->app->removeExternalTaskLink($this->taskId, $this->linkId));
+ }
+}
diff --git a/tests/integration/TaskFileProcedureTest.php b/tests/integration/TaskFileProcedureTest.php
index 61155555..60909ecd 100644
--- a/tests/integration/TaskFileProcedureTest.php
+++ b/tests/integration/TaskFileProcedureTest.php
@@ -21,7 +21,7 @@ class TaskFileProcedureTest extends BaseProcedureTest
public function assertCreateTaskFile()
{
- $this->fileId = $this->app->createTaskFile(1, $this->taskId, 'My file', base64_encode('plain text file'));
+ $this->fileId = $this->app->createTaskFile($this->projectId, $this->taskId, 'My file', base64_encode('plain text file'));
$this->assertNotFalse($this->fileId);
}
diff --git a/tests/units/Auth/ReverseProxyAuthTest.php b/tests/units/Auth/ReverseProxyAuthTest.php
new file mode 100644
index 00000000..cdbc247d
--- /dev/null
+++ b/tests/units/Auth/ReverseProxyAuthTest.php
@@ -0,0 +1,111 @@
+<?php
+
+use Kanboard\Auth\ReverseProxyAuth;
+use Kanboard\Core\Security\Role;
+use Kanboard\Model\UserModel;
+
+require_once __DIR__.'/../Base.php';
+
+class ReverseProxyAuthTest extends Base
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->container['request'] = $this
+ ->getMockBuilder('\Kanboard\Core\Http\Request')
+ ->setConstructorArgs(array($this->container))
+ ->setMethods(array('getRemoteUser'))
+ ->getMock();
+ }
+
+ public function testGetName()
+ {
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertEquals('ReverseProxy', $provider->getName());
+ }
+
+ public function testAuthenticateSuccess()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue('admin'));
+
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertTrue($provider->authenticate());
+ }
+
+ public function testAuthenticateFailure()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue(''));
+
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertFalse($provider->authenticate());
+ }
+
+ public function testValidSession()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue('admin'));
+
+ $this->container['sessionStorage']->user = array(
+ 'username' => 'admin'
+ );
+
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertTrue($provider->isValidSession());
+ }
+
+ public function testInvalidSession()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue('foobar'));
+
+ $this->container['sessionStorage']->user = array(
+ 'username' => 'admin'
+ );
+
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertFalse($provider->isValidSession());
+ }
+
+ public function testRoleForNewUser()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue('someone'));
+
+ $provider = new ReverseProxyAuth($this->container);
+ $this->assertTrue($provider->authenticate());
+
+ $user = $provider->getUser();
+ $this->assertEquals(Role::APP_USER, $user->getRole());
+ }
+
+ public function testRoleIsPreservedForExistingUser()
+ {
+ $this->container['request']
+ ->expects($this->once())
+ ->method('getRemoteUser')
+ ->will($this->returnValue('someone'));
+
+ $provider = new ReverseProxyAuth($this->container);
+ $userModel = new UserModel($this->container);
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'someone', 'role' => Role::APP_MANAGER)));
+
+ $this->assertTrue($provider->authenticate());
+
+ $user = $provider->getUser();
+ $this->assertEquals(Role::APP_MANAGER, $user->getRole());
+ }
+}
diff --git a/tests/units/Core/Filter/LexerTest.php b/tests/units/Core/Filter/LexerTest.php
index b777531d..d57a7953 100644
--- a/tests/units/Core/Filter/LexerTest.php
+++ b/tests/units/Core/Filter/LexerTest.php
@@ -214,4 +214,48 @@ class LexerTest extends Base
$this->assertSame($expected, $lexer->tokenize('tag:"tag 1" tag:tag2'));
}
+
+ public function testTokenizeWithDash()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(test:)/", 'T_TEST');
+
+ $expected = array(
+ 'T_TEST' => array('PO-123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('test:PO-123'));
+
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'myDefaultToken' => array('PO-123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('PO-123'));
+ }
+
+ public function testTokenizeWithUnderscore()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(test:)/", 'T_TEST');
+
+ $expected = array(
+ 'T_TEST' => array('PO_123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('test:PO_123'));
+
+ $lexer = new Lexer();
+ $lexer->addToken("/^(test:)/", 'T_TEST');
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'T_TEST' => array('ABC-123'),
+ 'myDefaultToken' => array('PO_123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('test:ABC-123 PO_123'));
+ }
}
diff --git a/tests/units/Core/Ldap/LdapUserTest.php b/tests/units/Core/Ldap/LdapUserTest.php
index 505b8a03..143a8c0d 100644
--- a/tests/units/Core/Ldap/LdapUserTest.php
+++ b/tests/units/Core/Ldap/LdapUserTest.php
@@ -845,4 +845,64 @@ class LdapUserTest extends Base
$this->assertTrue($this->user->hasGroupUserFilter());
}
+
+ public function testHasGroupsConfigured()
+ {
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupAdminDn')
+ ->will($this->returnValue('something'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupManagerDn')
+ ->will($this->returnValue('something'));
+
+ $this->assertTrue($this->user->hasGroupsConfigured());
+ }
+
+ public function testHasGroupAdminDnConfigured()
+ {
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupAdminDn')
+ ->will($this->returnValue('something'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupManagerDn')
+ ->will($this->returnValue(''));
+
+ $this->assertTrue($this->user->hasGroupsConfigured());
+ }
+
+ public function testHasGroupManagerDnConfigured()
+ {
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupAdminDn')
+ ->will($this->returnValue(''));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupManagerDn')
+ ->will($this->returnValue('something'));
+
+ $this->assertTrue($this->user->hasGroupsConfigured());
+ }
+
+ public function testHasGroupsNotConfigured()
+ {
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupAdminDn')
+ ->will($this->returnValue(''));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupManagerDn')
+ ->will($this->returnValue(''));
+
+ $this->assertFalse($this->user->hasGroupsConfigured());
+ }
}
diff --git a/tests/units/Helper/UserHelperTest.php b/tests/units/Helper/UserHelperTest.php
index 10bbc58e..d5bd1789 100644
--- a/tests/units/Helper/UserHelperTest.php
+++ b/tests/units/Helper/UserHelperTest.php
@@ -31,6 +31,12 @@ class UserHelperTest extends Base
$this->assertEquals('Project Viewer', $helper->getRoleName(Role::PROJECT_VIEWER));
}
+ public function testHasAccessWithoutSession()
+ {
+ $helper = new UserHelper($this->container);
+ $this->assertFalse($helper->hasAccess('UserCreationController', 'create'));
+ }
+
public function testHasAccessForAdmins()
{
$helper = new UserHelper($this->container);
@@ -73,6 +79,15 @@ class UserHelperTest extends Base
$this->assertTrue($helper->hasAccess('ProjectCreationController', 'createPrivate'));
}
+ public function testHasProjectAccessWithoutSession()
+ {
+ $helper = new UserHelper($this->container);
+ $project = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+ $this->assertFalse($helper->hasProjectAccess('ProjectEditController', 'edit', 1));
+ }
+
public function testHasProjectAccessForAdmins()
{
$helper = new UserHelper($this->container);
diff --git a/tests/units/Model/NotificationModelTest.php b/tests/units/Model/NotificationModelTest.php
new file mode 100644
index 00000000..889f3349
--- /dev/null
+++ b/tests/units/Model/NotificationModelTest.php
@@ -0,0 +1,109 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\NotificationModel;
+use Kanboard\Subscriber\NotificationSubscriber;
+
+class NotificationModelTest extends Base
+{
+ public function testGetTitle()
+ {
+ $notificationModel = new NotificationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $subtaskModel = new SubtaskModel($this->container);
+ $commentModel = new CommentModel($this->container);
+ $taskFileModel = new TaskFileModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+ $this->assertEquals(1, $subtaskModel->create(array('title' => 'test', 'task_id' => 1)));
+ $this->assertEquals(1, $commentModel->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1)));
+ $this->assertEquals(1, $taskFileModel->create(1, 'test', 'blah', 123));
+
+ $task = $taskFinderModel->getDetails(1);
+ $subtask = $subtaskModel->getById(1, true);
+ $comment = $commentModel->getById(1);
+ $file = $commentModel->getById(1);
+
+ $this->assertNotEmpty($task);
+ $this->assertNotEmpty($subtask);
+ $this->assertNotEmpty($comment);
+ $this->assertNotEmpty($file);
+
+ foreach (NotificationSubscriber::getSubscribedEvents() as $event_name => $values) {
+ $title = $notificationModel->getTitleWithoutAuthor($event_name, array(
+ 'task' => $task,
+ 'comment' => $comment,
+ 'subtask' => $subtask,
+ 'file' => $file,
+ 'changes' => array()
+ ));
+
+ $this->assertNotEmpty($title);
+
+ $title = $notificationModel->getTitleWithAuthor('foobar', $event_name, array(
+ 'task' => $task,
+ 'comment' => $comment,
+ 'subtask' => $subtask,
+ 'file' => $file,
+ 'changes' => array()
+ ));
+
+ $this->assertNotEmpty($title);
+ }
+
+ $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor(TaskModel::EVENT_OVERDUE, array('tasks' => array(array('id' => 1)))));
+ $this->assertNotEmpty($notificationModel->getTitleWithoutAuthor('unkown', array()));
+ }
+
+ public function testGetTaskIdFromEvent()
+ {
+ $notificationModel = new NotificationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $subtaskModel = new SubtaskModel($this->container);
+ $commentModel = new CommentModel($this->container);
+ $taskFileModel = new TaskFileModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+ $this->assertEquals(1, $subtaskModel->create(array('title' => 'test', 'task_id' => 1)));
+ $this->assertEquals(1, $commentModel->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1)));
+ $this->assertEquals(1, $taskFileModel->create(1, 'test', 'blah', 123));
+
+ $task = $taskFinderModel->getDetails(1);
+ $subtask = $subtaskModel->getById(1, true);
+ $comment = $commentModel->getById(1);
+ $file = $commentModel->getById(1);
+
+ $this->assertNotEmpty($task);
+ $this->assertNotEmpty($subtask);
+ $this->assertNotEmpty($comment);
+ $this->assertNotEmpty($file);
+
+ foreach (NotificationSubscriber::getSubscribedEvents() as $event_name => $values) {
+ $task_id = $notificationModel->getTaskIdFromEvent($event_name, array(
+ 'task' => $task,
+ 'comment' => $comment,
+ 'subtask' => $subtask,
+ 'file' => $file,
+ 'changes' => array()
+ ));
+
+ $this->assertEquals($task_id, $task['id']);
+ }
+
+ $this->assertEquals(1, $notificationModel->getTaskIdFromEvent(TaskModel::EVENT_OVERDUE, array('tasks' => array(array('id' => 1)))));
+ }
+}
diff --git a/tests/units/Model/NotificationTest.php b/tests/units/Model/NotificationTest.php
deleted file mode 100644
index 96ee5f4e..00000000
--- a/tests/units/Model/NotificationTest.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\SubtaskModel;
-use Kanboard\Model\CommentModel;
-use Kanboard\Model\TaskFileModel;
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\ProjectModel;
-use Kanboard\Model\NotificationModel;
-use Kanboard\Subscriber\NotificationSubscriber;
-
-class NotificationTest extends Base
-{
- public function testGetTitle()
- {
- $wn = new NotificationModel($this->container);
- $p = new ProjectModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $s = new SubtaskModel($this->container);
- $c = new CommentModel($this->container);
- $f = new TaskFileModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
- $this->assertEquals(1, $s->create(array('title' => 'test', 'task_id' => 1)));
- $this->assertEquals(1, $c->create(array('comment' => 'test', 'task_id' => 1, 'user_id' => 1)));
- $this->assertEquals(1, $f->create(1, 'test', 'blah', 123));
-
- $task = $tf->getDetails(1);
- $subtask = $s->getById(1, true);
- $comment = $c->getById(1);
- $file = $c->getById(1);
-
- $this->assertNotEmpty($task);
- $this->assertNotEmpty($subtask);
- $this->assertNotEmpty($comment);
- $this->assertNotEmpty($file);
-
- foreach (NotificationSubscriber::getSubscribedEvents() as $event_name => $values) {
- $title = $wn->getTitleWithoutAuthor($event_name, array(
- 'task' => $task,
- 'comment' => $comment,
- 'subtask' => $subtask,
- 'file' => $file,
- 'changes' => array()
- ));
-
- $this->assertNotEmpty($title);
-
- $title = $wn->getTitleWithAuthor('foobar', $event_name, array(
- 'task' => $task,
- 'comment' => $comment,
- 'subtask' => $subtask,
- 'file' => $file,
- 'changes' => array()
- ));
-
- $this->assertNotEmpty($title);
- }
-
- $this->assertNotEmpty($wn->getTitleWithoutAuthor(TaskModel::EVENT_OVERDUE, array('tasks' => array(array('id' => 1)))));
- $this->assertNotEmpty($wn->getTitleWithoutAuthor('unkown', array()));
- }
-}
diff --git a/tests/units/Model/ProjectDuplicationModelTest.php b/tests/units/Model/ProjectDuplicationModelTest.php
new file mode 100644
index 00000000..54261728
--- /dev/null
+++ b/tests/units/Model/ProjectDuplicationModelTest.php
@@ -0,0 +1,548 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\ActionModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\ProjectDuplicationModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\GroupModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Core\Security\Role;
+
+class ProjectDuplicationModelTest extends Base
+{
+ public function testGetSelections()
+ {
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $this->assertCount(7, $projectDuplicationModel->getOptionalSelection());
+ $this->assertCount(8, $projectDuplicationModel->getPossibleSelection());
+ }
+
+ public function testGetClonedProjectName()
+ {
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals('test (Clone)', $projectDuplicationModel->getClonedProjectName('test'));
+
+ $this->assertEquals(50, strlen($projectDuplicationModel->getClonedProjectName(str_repeat('a', 50))));
+ $this->assertEquals(str_repeat('a', 42).' (Clone)', $projectDuplicationModel->getClonedProjectName(str_repeat('a', 50)));
+
+ $this->assertEquals(50, strlen($projectDuplicationModel->getClonedProjectName(str_repeat('a', 60))));
+ $this->assertEquals(str_repeat('a', 42).' (Clone)', $projectDuplicationModel->getClonedProjectName(str_repeat('a', 60)));
+ }
+
+ public function testClonePublicProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Public')));
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $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 testClonePrivateProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Private', 'is_private' => 1), 1, true));
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $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, $projectUserRoleModel->getUserRole(2, 1));
+ }
+
+ public function testCloneSharedProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Shared')));
+ $this->assertTrue($projectModel->update(array('id' => 1, 'is_public' => 1, 'token' => 'test')));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals('test', $project['token']);
+ $this->assertEquals(1, $project['is_public']);
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $this->assertEquals('Shared (Clone)', $project['name']);
+ $this->assertEquals('', $project['token']);
+ $this->assertEquals(0, $project['is_public']);
+ }
+
+ public function testCloneInactiveProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Inactive')));
+ $this->assertTrue($projectModel->update(array('id' => 1, 'is_active' => 0)));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(0, $project['is_active']);
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $this->assertEquals('Inactive (Clone)', $project['name']);
+ $this->assertEquals(1, $project['is_active']);
+ }
+
+ public function testCloneProjectWithOwner()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Owner')));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(0, $project['owner_id']);
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel'), 1));
+
+ $project = $projectModel->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 testCloneProjectWithDifferentPriorities()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array(
+ 'name' => 'My project',
+ 'priority_default' => 2,
+ 'priority_start' => -2,
+ 'priority_end' => 8,
+ )));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $this->assertEquals('My project (Clone)', $project['name']);
+ $this->assertEquals(2, $project['priority_default']);
+ $this->assertEquals(-2, $project['priority_start']);
+ $this->assertEquals(8, $project['priority_end']);
+ }
+
+ public function testCloneProjectWithDifferentName()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Owner')));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(0, $project['owner_id']);
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel'), 1, 'Foobar'));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $this->assertEquals('Foobar', $project['name']);
+ $this->assertEquals(1, $project['owner_id']);
+ }
+
+ public function testCloneProjectAndForceItToBePrivate()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Owner')));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(0, $project['owner_id']);
+ $this->assertEquals(0, $project['is_private']);
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel'), 1, 'Foobar', true));
+
+ $project = $projectModel->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()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1)));
+ $this->assertEquals(2, $categoryModel->create(array('name' => 'C2', 'project_id' => 1)));
+ $this->assertEquals(3, $categoryModel->create(array('name' => 'C3', 'project_id' => 1)));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $project = $projectModel->getById(2);
+ $this->assertNotEmpty($project);
+ $this->assertEquals('P1 (Clone)', $project['name']);
+
+ $categories = $categoryModel->getAll(2);
+ $this->assertCount(3, $categories);
+ $this->assertEquals('C1', $categories[0]['name']);
+ $this->assertEquals('C2', $categories[1]['name']);
+ $this->assertEquals('C3', $categories[2]['name']);
+ }
+
+ public function testCloneProjectWithUsers()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+ $userModel = new UserModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $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->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 3, Role::PROJECT_MEMBER));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 4, Role::PROJECT_VIEWER));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $this->assertCount(3, $projectUserRoleModel->getUsers(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 testCloneProjectWithUsersAndOverrideOwner()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+ $userModel = new UserModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1'), 2));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(2, $project['owner_id']);
+
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel'), 1));
+
+ $this->assertCount(2, $projectUserRoleModel->getUsers(2));
+ $this->assertEquals(Role::PROJECT_MANAGER, $projectUserRoleModel->getUserRole(2, 2));
+ $this->assertEquals(Role::PROJECT_MANAGER, $projectUserRoleModel->getUserRole(2, 1));
+
+ $project = $projectModel->getById(2);
+ $this->assertEquals(1, $project['owner_id']);
+ }
+
+ public function testCloneTeamProjectToPrivatProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+ $userModel = new UserModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user2')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1'), 2));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals(2, $project['owner_id']);
+ $this->assertEquals(0, $project['is_private']);
+
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel'), 3, 'My private project', true));
+
+ $this->assertCount(1, $projectUserRoleModel->getUsers(2));
+ $this->assertEquals(Role::PROJECT_MANAGER, $projectUserRoleModel->getUserRole(2, 3));
+
+ $project = $projectModel->getById(2);
+ $this->assertEquals(3, $project['owner_id']);
+ $this->assertEquals(1, $project['is_private']);
+ }
+
+ public function testCloneProjectWithGroups()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $userModel = new UserModel($this->container);
+ $groupModel = new GroupModel($this->container);
+ $groupMemberModel = new GroupMemberModel($this->container);
+ $projectGroupRoleModel = new ProjectGroupRoleModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+
+ $this->assertEquals(1, $projectModel->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, $projectDuplicationModel->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()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $actionModel = new ActionModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $actionModel->create(array(
+ 'project_id' => 1,
+ 'event_name' => TaskModel::EVENT_MOVE_COLUMN,
+ 'action_name' => 'TaskAssignCurrentUser',
+ 'params' => array('column_id' => 2),
+ )));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $actions = $actionModel->getAllByProject(2);
+
+ $this->assertNotEmpty($actions);
+ $this->assertEquals('TaskAssignCurrentUser', $actions[0]['action_name']);
+ $this->assertNotEmpty($actions[0]['params']);
+ $this->assertEquals(6, $actions[0]['params']['column_id']);
+ }
+
+ public function testCloneProjectWithActionTaskAssignColorCategory()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $actionModel = new ActionModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $categoryModel->create(array('name' => 'C1', 'project_id' => 1)));
+ $this->assertEquals(2, $categoryModel->create(array('name' => 'C2', 'project_id' => 1)));
+ $this->assertEquals(3, $categoryModel->create(array('name' => 'C3', 'project_id' => 1)));
+
+ $this->assertEquals(1, $actionModel->create(array(
+ 'project_id' => 1,
+ 'event_name' => TaskModel::EVENT_CREATE_UPDATE,
+ 'action_name' => 'TaskAssignColorCategory',
+ 'params' => array('color_id' => 'blue', 'category_id' => 2),
+ )));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1));
+
+ $actions = $actionModel->getAllByProject(2);
+
+ $this->assertNotEmpty($actions);
+ $this->assertEquals('TaskAssignColorCategory', $actions[0]['action_name']);
+ $this->assertNotEmpty($actions[0]['params']);
+ $this->assertEquals('blue', $actions[0]['params']['color_id']);
+ $this->assertEquals(5, $actions[0]['params']['category_id']);
+ }
+
+ public function testCloneProjectWithSwimlanes()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1', 'default_swimlane' => 'New Default')));
+
+ // create initial swimlanes
+ $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S1')));
+ $this->assertEquals(2, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S2')));
+ $this->assertEquals(3, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S3')));
+
+ // create initial tasks
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T0', 'project_id' => 1, 'swimlane_id' => 0)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'swimlane_id' => 1)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1, 'swimlane_id' => 2)));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'T3', 'project_id' => 1, 'swimlane_id' => 3)));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('categoryModel', 'swimlaneModel')));
+
+ $swimlanes = $swimlaneModel->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']);
+
+ $swimlane = $swimlaneModel->getDefault(2);
+ $this->assertEquals('New Default', $swimlane['default_swimlane']);
+
+ // Check if tasks are NOT been duplicated
+ $this->assertCount(0, $taskFinderModel->getAll(2));
+ }
+
+ public function testCloneProjectWithTasks()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ // create initial tasks
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3)));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('categoryModel', 'actionModel', 'projectTaskDuplicationModel')));
+
+ // Check if Tasks have been duplicated
+ $tasks = $taskFinderModel->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 testCloneProjectWithSwimlanesAndTasks()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1', 'default_swimlane' => 'New Default')));
+
+ // create initial swimlanes
+ $this->assertEquals(1, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S1')));
+ $this->assertEquals(2, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S2')));
+ $this->assertEquals(3, $swimlaneModel->create(array('project_id' => 1, 'name' => 'S3')));
+
+ // create initial tasks
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1)));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('projectPermissionModel', 'swimlaneModel', 'projectTaskDuplicationModel')));
+
+ // Check if Swimlanes have been duplicated
+ $swimlanes = $swimlaneModel->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']);
+
+ $swimlane = $swimlaneModel->getDefault(2);
+ $this->assertEquals('New Default', $swimlane['default_swimlane']);
+
+ // Check if Tasks have been duplicated
+ $tasks = $taskFinderModel->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 testCloneProjectWithTags()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectDuplicationModel = new ProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $tagModel = new TagModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'T1', 'project_id' => 1, 'column_id' => 1, 'tags' => array('A'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'T2', 'project_id' => 1, 'column_id' => 2, 'tags' => array('A', 'B'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'T3', 'project_id' => 1, 'column_id' => 3, 'tags' => array('C'))));
+
+ $this->assertEquals(2, $projectDuplicationModel->duplicate(1, array('categoryModel', 'actionModel', 'tagDuplicationModel', 'projectTaskDuplicationModel')));
+
+ $tasks = $taskFinderModel->getAll(2);
+ $this->assertCount(3, $tasks);
+ $this->assertEquals('T1', $tasks[0]['title']);
+ $this->assertEquals('T2', $tasks[1]['title']);
+ $this->assertEquals('T3', $tasks[2]['title']);
+
+ $tags = $tagModel->getAllByProject(2);
+ $this->assertCount(3, $tags);
+ $this->assertEquals(4, $tags[0]['id']);
+ $this->assertEquals('A', $tags[0]['name']);
+ $this->assertEquals(5, $tags[1]['id']);
+ $this->assertEquals('B', $tags[1]['name']);
+ $this->assertEquals(6, $tags[2]['id']);
+ $this->assertEquals('C', $tags[2]['name']);
+
+ $tags = $taskTagModel->getList(4);
+ $this->assertEquals('A', $tags[4]);
+
+ $tags = $taskTagModel->getList(5);
+ $this->assertEquals('A', $tags[4]);
+ $this->assertEquals('B', $tags[5]);
+
+ $tags = $taskTagModel->getList(6);
+ $this->assertEquals('C', $tags[6]);
+ }
+}
diff --git a/tests/units/Model/ProjectDuplicationTest.php b/tests/units/Model/ProjectDuplicationTest.php
deleted file mode 100644
index 312c7168..00000000
--- a/tests/units/Model/ProjectDuplicationTest.php
+++ /dev/null
@@ -1,482 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\ActionModel;
-use Kanboard\Model\ProjectModel;
-use Kanboard\Model\CategoryModel;
-use Kanboard\Model\ProjectUserRoleModel;
-use Kanboard\Model\ProjectGroupRoleModel;
-use Kanboard\Model\ProjectDuplicationModel;
-use Kanboard\Model\UserModel;
-use Kanboard\Model\GroupModel;
-use Kanboard\Model\GroupMemberModel;
-use Kanboard\Model\SwimlaneModel;
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Core\Security\Role;
-
-class ProjectDuplicationTest extends Base
-{
- public function testGetSelections()
- {
- $projectDuplicationModel = new ProjectDuplicationModel($this->container);
- $this->assertCount(6, $projectDuplicationModel->getOptionalSelection());
- $this->assertCount(7, $projectDuplicationModel->getPossibleSelection());
- }
-
- public function testGetClonedProjectName()
- {
- $pd = new ProjectDuplicationModel($this->container);
-
- $this->assertEquals('test (Clone)', $pd->getClonedProjectName('test'));
-
- $this->assertEquals(50, strlen($pd->getClonedProjectName(str_repeat('a', 50))));
- $this->assertEquals(str_repeat('a', 42).' (Clone)', $pd->getClonedProjectName(str_repeat('a', 50)));
-
- $this->assertEquals(50, strlen($pd->getClonedProjectName(str_repeat('a', 60))));
- $this->assertEquals(str_repeat('a', 42).' (Clone)', $pd->getClonedProjectName(str_repeat('a', 60)));
- }
-
- public function testClonePublicProject()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Public')));
- $this->assertEquals(2, $pd->duplicate(1));
-
- $project = $p->getById(2);
- $this->assertNotEmpty($project);
- $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 testClonePrivateProject()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
-
- $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('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 testCloneSharedProject()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $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('Shared (Clone)', $project['name']);
- $this->assertEquals('', $project['token']);
- $this->assertEquals(0, $project['is_public']);
- }
-
- public function testCloneInactiveProject()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($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 ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $projectUserRoleModel = new ProjectUserRoleModel($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('projectPermissionModel'), 1));
-
- $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 ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($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('projectPermissionModel'), 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 ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($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('projectPermissionModel'), 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()
- {
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'P1')));
-
- $this->assertEquals(1, $c->create(array('name' => 'C1', 'project_id' => 1)));
- $this->assertEquals(2, $c->create(array('name' => 'C2', 'project_id' => 1)));
- $this->assertEquals(3, $c->create(array('name' => 'C3', 'project_id' => 1)));
-
- $this->assertEquals(2, $pd->duplicate(1));
-
- $project = $p->getById(2);
- $this->assertNotEmpty($project);
- $this->assertEquals('P1 (Clone)', $project['name']);
-
- $categories = $c->getAll(2);
- $this->assertCount(3, $categories);
- $this->assertEquals('C1', $categories[0]['name']);
- $this->assertEquals('C2', $categories[1]['name']);
- $this->assertEquals('C3', $categories[2]['name']);
- }
-
- public function testCloneProjectWithUsers()
- {
- $p = new ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
- $u = new UserModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $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_MANAGER));
- $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER));
- $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 ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
- $u = new UserModel($this->container);
- $pd = new ProjectDuplicationModel($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('projectPermissionModel'), 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->assertEquals(1, $project['owner_id']);
- }
-
- public function testCloneTeamProjectToPrivatProject()
- {
- $p = new ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
- $u = new UserModel($this->container);
- $pd = new ProjectDuplicationModel($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('projectPermissionModel'), 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 ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $userModel = new UserModel($this->container);
- $groupModel = new GroupModel($this->container);
- $groupMemberModel = new GroupMemberModel($this->container);
- $projectGroupRoleModel = new ProjectGroupRoleModel($this->container);
- $projectUserRoleModel = new ProjectUserRoleModel($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()
- {
- $p = new ProjectModel($this->container);
- $a = new ActionModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'P1')));
-
- $this->assertEquals(1, $a->create(array(
- 'project_id' => 1,
- 'event_name' => TaskModel::EVENT_MOVE_COLUMN,
- 'action_name' => 'TaskAssignCurrentUser',
- 'params' => array('column_id' => 2),
- )));
-
- $this->assertEquals(2, $pd->duplicate(1));
-
- $actions = $a->getAllByProject(2);
-
- $this->assertNotEmpty($actions);
- $this->assertEquals('TaskAssignCurrentUser', $actions[0]['action_name']);
- $this->assertNotEmpty($actions[0]['params']);
- $this->assertEquals(6, $actions[0]['params']['column_id']);
- }
-
- public function testCloneProjectWithActionTaskAssignColorCategory()
- {
- $p = new ProjectModel($this->container);
- $a = new ActionModel($this->container);
- $c = new CategoryModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'P1')));
-
- $this->assertEquals(1, $c->create(array('name' => 'C1', 'project_id' => 1)));
- $this->assertEquals(2, $c->create(array('name' => 'C2', 'project_id' => 1)));
- $this->assertEquals(3, $c->create(array('name' => 'C3', 'project_id' => 1)));
-
- $this->assertEquals(1, $a->create(array(
- 'project_id' => 1,
- 'event_name' => TaskModel::EVENT_CREATE_UPDATE,
- 'action_name' => 'TaskAssignColorCategory',
- 'params' => array('color_id' => 'blue', 'category_id' => 2),
- )));
-
- $this->assertEquals(2, $pd->duplicate(1));
-
- $actions = $a->getAllByProject(2);
-
- $this->assertNotEmpty($actions);
- $this->assertEquals('TaskAssignColorCategory', $actions[0]['action_name']);
- $this->assertNotEmpty($actions[0]['params']);
- $this->assertEquals('blue', $actions[0]['params']['color_id']);
- $this->assertEquals(5, $actions[0]['params']['category_id']);
- }
-
- public function testCloneProjectWithSwimlanes()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $s = new SwimlaneModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $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')));
-
- // 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)));
-
- $this->assertEquals(2, $pd->duplicate(1, array('categoryModel', 'swimlaneModel')));
-
- $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']);
-
- $swimlane = $s->getDefault(2);
- $this->assertEquals('New Default', $swimlane['default_swimlane']);
-
- // Check if tasks are NOT been duplicated
- $this->assertCount(0, $tf->getAll(2));
- }
-
- public function testCloneProjectWithTasks()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'P1')));
-
- // 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)));
-
- $this->assertEquals(2, $pd->duplicate(1, array('categoryModel', 'actionModel', 'taskModel')));
-
- // 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 testCloneProjectWithSwimlanesAndTasks()
- {
- $p = new ProjectModel($this->container);
- $pd = new ProjectDuplicationModel($this->container);
- $s = new SwimlaneModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $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')));
-
- // 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->assertEquals(2, $pd->duplicate(1, array('projectPermissionModel', 'swimlaneModel', 'taskModel')));
-
- // 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']);
-
- $swimlane = $s->getDefault(2);
- $this->assertEquals('New Default', $swimlane['default_swimlane']);
-
- // 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']);
- }
-}
diff --git a/tests/units/Model/ProjectTest.php b/tests/units/Model/ProjectModelTest.php
index 472d7351..81e0dd57 100644
--- a/tests/units/Model/ProjectTest.php
+++ b/tests/units/Model/ProjectModelTest.php
@@ -11,16 +11,16 @@ use Kanboard\Model\TaskCreationModel;
use Kanboard\Model\ConfigModel;
use Kanboard\Model\CategoryModel;
-class ProjectTest extends Base
+class ProjectModelTest extends Base
{
public function testCreationForAllLanguages()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
foreach ($this->container['languageModel']->getLanguages() as $locale => $language) {
Translator::unload();
Translator::load($locale);
- $this->assertNotFalse($p->create(array('name' => 'UnitTest '.$locale)), 'Unable to create project with '.$locale.':'.$language);
+ $this->assertNotFalse($projectModel->create(array('name' => 'UnitTest '.$locale)), 'Unable to create project with '.$locale.':'.$language);
}
Translator::unload();
@@ -28,11 +28,11 @@ class ProjectTest extends Base
public function testCreation()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals(1, $project['is_active']);
$this->assertEquals(0, $project['is_public']);
@@ -43,19 +43,19 @@ class ProjectTest extends Base
public function testCreationWithDuplicateName()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'UnitTest')));
}
public function testCreationWithStartAndDate()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest', 'start_date' => '2015-01-01', 'end_date' => '2015-12-31')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest', 'start_date' => '2015-01-01', 'end_date' => '2015-12-31')));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals('2015-01-01', $project['start_date']);
$this->assertEquals('2015-12-31', $project['end_date']);
@@ -63,19 +63,19 @@ class ProjectTest extends Base
public function testCreationWithDefaultCategories()
{
- $p = new ProjectModel($this->container);
- $c = new ConfigModel($this->container);
- $cat = new CategoryModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $configModel = new ConfigModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
// Multiple categories correctly formatted
- $this->assertTrue($c->save(array('project_categories' => 'Test1, Test2')));
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
+ $this->assertTrue($configModel->save(array('project_categories' => 'Test1, Test2')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest1')));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
- $categories = $cat->getAll(1);
+ $categories = $categoryModel->getAll(1);
$this->assertNotEmpty($categories);
$this->assertEquals(2, count($categories));
$this->assertEquals('Test1', $categories[0]['name']);
@@ -83,85 +83,85 @@ class ProjectTest extends Base
// Single category
- $this->assertTrue($c->save(array('project_categories' => 'Test1')));
+ $this->assertTrue($configModel->save(array('project_categories' => 'Test1')));
$this->container['memoryCache']->flush();
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest2')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'UnitTest2')));
- $project = $p->getById(2);
+ $project = $projectModel->getById(2);
$this->assertNotEmpty($project);
- $categories = $cat->getAll(2);
+ $categories = $categoryModel->getAll(2);
$this->assertNotEmpty($categories);
$this->assertEquals(1, count($categories));
$this->assertEquals('Test1', $categories[0]['name']);
// Multiple categories badly formatted
- $this->assertTrue($c->save(array('project_categories' => 'ABC, , DEF 3, ')));
+ $this->assertTrue($configModel->save(array('project_categories' => 'ABC, , DEF 3, ')));
$this->container['memoryCache']->flush();
- $this->assertEquals(3, $p->create(array('name' => 'UnitTest3')));
+ $this->assertEquals(3, $projectModel->create(array('name' => 'UnitTest3')));
- $project = $p->getById(3);
+ $project = $projectModel->getById(3);
$this->assertNotEmpty($project);
- $categories = $cat->getAll(3);
+ $categories = $categoryModel->getAll(3);
$this->assertNotEmpty($categories);
$this->assertEquals(2, count($categories));
$this->assertEquals('ABC', $categories[0]['name']);
$this->assertEquals('DEF 3', $categories[1]['name']);
// No default categories
- $this->assertTrue($c->save(array('project_categories' => ' ')));
+ $this->assertTrue($configModel->save(array('project_categories' => ' ')));
$this->container['memoryCache']->flush();
- $this->assertEquals(4, $p->create(array('name' => 'UnitTest4')));
+ $this->assertEquals(4, $projectModel->create(array('name' => 'UnitTest4')));
- $project = $p->getById(4);
+ $project = $projectModel->getById(4);
$this->assertNotEmpty($project);
- $categories = $cat->getAll(4);
+ $categories = $categoryModel->getAll(4);
$this->assertEmpty($categories);
}
public function testUpdateLastModifiedDate()
{
- $p = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
$now = time();
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals($now, $project['last_modified'], 'Wrong Timestamp', 1);
sleep(1);
- $this->assertTrue($p->updateModificationDate(1));
+ $this->assertTrue($projectModel->updateModificationDate(1));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertGreaterThan($now, $project['last_modified']);
}
public function testGetAllIds()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
- $this->assertEmpty($p->getAllByIds(array()));
- $this->assertNotEmpty($p->getAllByIds(array(1, 2)));
- $this->assertCount(1, $p->getAllByIds(array(1)));
+ $this->assertEmpty($projectModel->getAllByIds(array()));
+ $this->assertNotEmpty($projectModel->getAllByIds(array(1, 2)));
+ $this->assertCount(1, $projectModel->getAllByIds(array(1)));
}
public function testIsLastModified()
{
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
$now = time();
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals($now, $project['last_modified']);
@@ -170,113 +170,113 @@ class ProjectTest extends Base
$listener = new ProjectModificationDateSubscriber($this->container);
$this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($listener, 'execute'));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
$called = $this->container['dispatcher']->getCalledListeners();
$this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.Kanboard\Subscriber\ProjectModificationDateSubscriber::execute', $called);
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
- $this->assertTrue($p->isModifiedSince(1, $now));
+ $this->assertTrue($projectModel->isModifiedSince(1, $now));
}
public function testRemove()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($p->remove(1));
- $this->assertFalse($p->remove(1234));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertTrue($projectModel->remove(1));
+ $this->assertFalse($projectModel->remove(1234));
}
public function testEnable()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($p->disable(1));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertTrue($projectModel->disable(1));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals(0, $project['is_active']);
- $this->assertFalse($p->disable(1111));
+ $this->assertFalse($projectModel->disable(1111));
}
public function testDisable()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($p->disable(1));
- $this->assertTrue($p->enable(1));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertTrue($projectModel->disable(1));
+ $this->assertTrue($projectModel->enable(1));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals(1, $project['is_active']);
- $this->assertFalse($p->enable(1234567));
+ $this->assertFalse($projectModel->enable(1234567));
}
public function testEnablePublicAccess()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($p->enablePublicAccess(1));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertTrue($projectModel->enablePublicAccess(1));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals(1, $project['is_public']);
$this->assertNotEmpty($project['token']);
- $this->assertFalse($p->enablePublicAccess(123));
+ $this->assertFalse($projectModel->enablePublicAccess(123));
}
public function testDisablePublicAccess()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($p->enablePublicAccess(1));
- $this->assertTrue($p->disablePublicAccess(1));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertTrue($projectModel->enablePublicAccess(1));
+ $this->assertTrue($projectModel->disablePublicAccess(1));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals(0, $project['is_public']);
$this->assertEmpty($project['token']);
- $this->assertFalse($p->disablePublicAccess(123));
+ $this->assertFalse($projectModel->disablePublicAccess(123));
}
public function testIdentifier()
{
- $p = new ProjectModel($this->container);
+ $projectModel = new ProjectModel($this->container);
// Creation
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest1', 'identifier' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest2')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest1', 'identifier' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'UnitTest2')));
- $project = $p->getById(1);
+ $project = $projectModel->getById(1);
$this->assertNotEmpty($project);
$this->assertEquals('TEST1', $project['identifier']);
- $project = $p->getById(2);
+ $project = $projectModel->getById(2);
$this->assertNotEmpty($project);
$this->assertEquals('', $project['identifier']);
// Update
- $this->assertTrue($p->update(array('id' => '2', 'identifier' => 'test2')));
+ $this->assertTrue($projectModel->update(array('id' => '2', 'identifier' => 'test2')));
- $project = $p->getById(2);
+ $project = $projectModel->getById(2);
$this->assertNotEmpty($project);
$this->assertEquals('TEST2', $project['identifier']);
- $project = $p->getByIdentifier('test1');
+ $project = $projectModel->getByIdentifier('test1');
$this->assertNotEmpty($project);
$this->assertEquals('TEST1', $project['identifier']);
- $project = $p->getByIdentifier('');
+ $project = $projectModel->getByIdentifier('');
$this->assertFalse($project);
}
@@ -303,34 +303,4 @@ class ProjectTest extends Base
$this->assertEquals('', $project['owner_username']);
$this->assertEquals(0, $project['owner_id']);
}
-
- public function testPriority()
- {
- $projectModel = new ProjectModel($this->container);
- $this->assertEquals(1, $projectModel->create(array('name' => 'My project 2')));
-
- $project = $projectModel->getById(1);
- $this->assertNotEmpty($project);
- $this->assertEquals(0, $project['priority_default']);
- $this->assertEquals(0, $project['priority_start']);
- $this->assertEquals(3, $project['priority_end']);
-
- $this->assertEquals(
- array(0 => 0, 1 => 1, 2 => 2, 3 => 3),
- $projectModel->getPriorities($project)
- );
-
- $this->assertTrue($projectModel->update(array('id' => 1, 'priority_start' => 2, 'priority_end' => 5, 'priority_default' => 4)));
-
- $project = $projectModel->getById(1);
- $this->assertNotEmpty($project);
- $this->assertEquals(4, $project['priority_default']);
- $this->assertEquals(2, $project['priority_start']);
- $this->assertEquals(5, $project['priority_end']);
-
- $this->assertEquals(
- array(2 => 2, 3 => 3, 4 => 4, 5 => 5),
- $projectModel->getPriorities($project)
- );
- }
}
diff --git a/tests/units/Model/ProjectTaskPriorityModelTest.php b/tests/units/Model/ProjectTaskPriorityModelTest.php
new file mode 100644
index 00000000..61661e5e
--- /dev/null
+++ b/tests/units/Model/ProjectTaskPriorityModelTest.php
@@ -0,0 +1,84 @@
+<?php
+
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectTaskPriorityModelTest extends Base
+{
+ public function testPriority()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectTaskPriorityModel = new ProjectTaskPriorityModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'My project 2')));
+ $this->assertEquals(0, $projectTaskPriorityModel->getDefaultPriority(1));
+
+ $project = $projectModel->getById(1);
+ $this->assertNotEmpty($project);
+ $this->assertEquals(0, $project['priority_default']);
+ $this->assertEquals(0, $project['priority_start']);
+ $this->assertEquals(3, $project['priority_end']);
+
+ $this->assertEquals(
+ array(0 => 0, 1 => 1, 2 => 2, 3 => 3),
+ $projectTaskPriorityModel->getPriorities($project)
+ );
+
+ $this->assertTrue($projectModel->update(array('id' => 1, 'priority_start' => 2, 'priority_end' => 5, 'priority_default' => 4)));
+
+ $project = $projectModel->getById(1);
+ $this->assertNotEmpty($project);
+ $this->assertEquals(4, $project['priority_default']);
+ $this->assertEquals(2, $project['priority_start']);
+ $this->assertEquals(5, $project['priority_end']);
+
+ $this->assertEquals(
+ array(2 => 2, 3 => 3, 4 => 4, 5 => 5),
+ $projectTaskPriorityModel->getPriorities($project)
+ );
+
+ $this->assertEquals(4, $projectTaskPriorityModel->getDefaultPriority(1));
+ }
+
+ public function testGetPrioritySettings()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectTaskPriorityModel = new ProjectTaskPriorityModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'My project 2')));
+
+ $expected = array(
+ 'priority_default' => 0,
+ 'priority_start' => 0,
+ 'priority_end' => 3,
+ );
+
+ $this->assertEquals($expected, $projectTaskPriorityModel->getPrioritySettings(1));
+ $this->assertNull($projectTaskPriorityModel->getPrioritySettings(2));
+ }
+
+ public function testGetPriorityForProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $projectTaskPriorityModel = new ProjectTaskPriorityModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array(
+ 'name' => 'My project 1',
+ 'priority_default' => 2,
+ 'priority_start' => -2,
+ 'priority_end' => 8,
+ )));
+
+ $this->assertEquals(2, $projectTaskPriorityModel->getPriorityForProject(1, 42));
+ $this->assertEquals(0, $projectTaskPriorityModel->getPriorityForProject(1, 0));
+ $this->assertEquals(1, $projectTaskPriorityModel->getPriorityForProject(1, 1));
+ $this->assertEquals(-2, $projectTaskPriorityModel->getPriorityForProject(1, -2));
+ $this->assertEquals(-1, $projectTaskPriorityModel->getPriorityForProject(1, -1));
+ $this->assertEquals(8, $projectTaskPriorityModel->getPriorityForProject(1, 8));
+ $this->assertEquals(5, $projectTaskPriorityModel->getPriorityForProject(1, 5));
+ $this->assertEquals(2, $projectTaskPriorityModel->getPriorityForProject(1, 9));
+ $this->assertEquals(2, $projectTaskPriorityModel->getPriorityForProject(1, -3));
+ }
+}
diff --git a/tests/units/Model/SwimlaneTest.php b/tests/units/Model/SwimlaneTest.php
index cf0be169..4541e07f 100644
--- a/tests/units/Model/SwimlaneTest.php
+++ b/tests/units/Model/SwimlaneTest.php
@@ -40,7 +40,17 @@ class SwimlaneTest extends Base
$this->assertEquals(2, $swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #2')));
$swimlane = $swimlaneModel->getFirstActiveSwimlane(1);
+ $this->assertEquals(0, $swimlane['id']);
+ $this->assertEquals('Default swimlane', $swimlane['name']);
+
+ $this->assertTrue($swimlaneModel->disableDefault(1));
+
+ $swimlane = $swimlaneModel->getFirstActiveSwimlane(1);
$this->assertEquals(2, $swimlane['id']);
+ $this->assertEquals('Swimlane #2', $swimlane['name']);
+
+ $this->assertTrue($swimlaneModel->disable(1, 2));
+ $this->assertNull($swimlaneModel->getFirstActiveSwimlane(1));
}
public function testGetList()
diff --git a/tests/units/Model/TagDuplicationModelTest.php b/tests/units/Model/TagDuplicationModelTest.php
new file mode 100644
index 00000000..0c95c0fd
--- /dev/null
+++ b/tests/units/Model/TagDuplicationModelTest.php
@@ -0,0 +1,31 @@
+<?php
+
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\TagModel;
+
+require_once __DIR__.'/../Base.php';
+
+class TagDuplicationModelTest extends Base
+{
+ public function testProjectDuplication()
+ {
+ $tagModel = new TagModel($this->container);
+ $tagDuplicationModel = new TagDuplicationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+
+ $this->assertEquals(1, $tagModel->create(0, 'Tag 1'));
+ $this->assertEquals(2, $tagModel->create(1, 'Tag 2'));
+ $this->assertEquals(3, $tagModel->create(1, 'Tag 3'));
+
+ $this->assertTrue($tagDuplicationModel->duplicate(1, 2));
+
+ $tags = $tagModel->getAllByProject(2);
+ $this->assertCount(2, $tags);
+ $this->assertEquals('Tag 2', $tags[0]['name']);
+ $this->assertEquals('Tag 3', $tags[1]['name']);
+ }
+}
diff --git a/tests/units/Model/TaskCreationModelTest.php b/tests/units/Model/TaskCreationModelTest.php
new file mode 100644
index 00000000..f97c61dc
--- /dev/null
+++ b/tests/units/Model/TaskCreationModelTest.php
@@ -0,0 +1,403 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+
+class TaskCreationModelTest extends Base
+{
+ public function onCreate($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
+
+ $event_data = $event->getAll();
+ $this->assertNotEmpty($event_data);
+ $this->assertEquals(1, $event_data['task_id']);
+ $this->assertEquals('test', $event_data['title']);
+ }
+
+ public function testNoTitle()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1)));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('Untitled', $task['title']);
+ $this->assertEquals(1, $task['project_id']);
+ }
+
+ public function testMinimum()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $finderModel = new TaskFinderModel($this->container);
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, array($this, 'onCreate'));
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test')));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.TaskCreationModelTest::onCreate', $called);
+
+ $task = $finderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertNotFalse($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('yellow', $task['color_id']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(1, $task['column_id']);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(0, $task['creator_id']);
+
+ $this->assertEquals('test', $task['title']);
+ $this->assertEquals('', $task['description']);
+ $this->assertEquals('', $task['reference']);
+
+ $this->assertEquals(time(), $task['date_creation'], 'Wrong timestamp', 1);
+ $this->assertEquals(time(), $task['date_modification'], 'Wrong timestamp', 1);
+ $this->assertEquals(0, $task['date_due']);
+ $this->assertEquals(0, $task['date_completed']);
+ $this->assertEquals(0, $task['date_started']);
+
+ $this->assertEquals(0, $task['time_estimated']);
+ $this->assertEquals(0, $task['time_spent']);
+
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(1, $task['is_active']);
+ $this->assertEquals(0, $task['score']);
+ }
+
+ public function testColorId()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'color_id' => 'blue')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('blue', $task['color_id']);
+ }
+
+ public function testOwnerId()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(1, $task['owner_id']);
+ }
+
+ public function testCategoryId()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'category_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(1, $task['category_id']);
+ }
+
+ public function testCreatorId()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'creator_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(1, $task['creator_id']);
+ }
+
+ public function testThatCreatorIsDefined()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->container['sessionStorage']->user = array('id' => 1);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(1, $task['creator_id']);
+ }
+
+ public function testColumnId()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(2, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ }
+
+ public function testPosition()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(2, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(2, $task['id']);
+ $this->assertEquals(2, $task['column_id']);
+ $this->assertEquals(2, $task['position']);
+ }
+
+ public function testDescription()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'description' => 'test')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('test', $task['description']);
+ }
+
+ public function testReference()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'reference' => 'test')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('test', $task['reference']);
+ }
+
+ public function testDateDue()
+ {
+ $date = '2014-11-23';
+ $timestamp = strtotime('+2days');
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_due' => $date)));
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_due' => $timestamp)));
+ $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_due' => '')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals($date, date('Y-m-d', $task['date_due']));
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['id']);
+ $this->assertEquals(date('Y-m-d 00:00', $timestamp), date('Y-m-d 00:00', $task['date_due']));
+
+ $task = $taskFinderModel->getById(3);
+ $this->assertEquals(3, $task['id']);
+ $this->assertEquals(0, $task['date_due']);
+ }
+
+ public function testDateStarted()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+
+ // Set only a date
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
+
+ // Set a datetime
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24 16:25')));
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
+
+ // Set a datetime
+ $this->assertEquals(3, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '11/24/2014 18:25')));
+
+ $task = $taskFinderModel->getById(3);
+ $this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
+
+ // Set a timestamp
+ $this->assertEquals(4, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_started' => time())));
+
+ $task = $taskFinderModel->getById(4);
+ $this->assertEquals(time(), $task['date_started'], '', 1);
+
+ // Set empty string
+ $this->assertEquals(5, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '')));
+ $task = $taskFinderModel->getById(5);
+ $this->assertEquals(0, $task['date_started']);
+ }
+
+ public function testTime()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.5, 'time_spent' => 2.3)));
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => '', 'time_spent' => '')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals(1.5, $task['time_estimated']);
+ $this->assertEquals(2.3, $task['time_spent']);
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['id']);
+ $this->assertEquals(0, $task['time_estimated']);
+ $this->assertEquals(0, $task['time_spent']);
+ }
+
+ public function testStripColumn()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'another_task' => '1')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ }
+
+ public function testScore()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'score' => '3')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertNotFalse($task);
+ $this->assertEquals(3, $task['score']);
+ }
+
+ public function testDefaultColor()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $configModel = new ConfigModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals('yellow', $task['color_id']);
+
+ $this->assertTrue($configModel->save(array('default_color' => 'orange')));
+ $this->container['memoryCache']->flush();
+
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test2')));
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals('orange', $task['color_id']);
+ }
+
+ public function testDueDateYear2038TimestampBug()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_due' => strtotime('2050-01-10 12:30'))));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals('2050-01-10 00:00', date('Y-m-d H:i', $task['date_due']));
+ }
+}
diff --git a/tests/units/Model/TaskCreationTest.php b/tests/units/Model/TaskCreationTest.php
deleted file mode 100644
index 7f2f6f5f..00000000
--- a/tests/units/Model/TaskCreationTest.php
+++ /dev/null
@@ -1,397 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\ConfigModel;
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Model\ProjectModel;
-
-class TaskCreationTest extends Base
-{
- public function onCreate($event)
- {
- $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
-
- $event_data = $event->getAll();
- $this->assertNotEmpty($event_data);
- $this->assertEquals(1, $event_data['task_id']);
- $this->assertEquals('test', $event_data['title']);
- }
-
- public function testNoTitle()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1)));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['id']);
- $this->assertEquals('Untitled', $task['title']);
- $this->assertEquals(1, $task['project_id']);
- }
-
- public function testMinimum()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, array($this, 'onCreate'));
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.TaskCreationTest::onCreate', $called);
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertNotFalse($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals('yellow', $task['color_id']);
- $this->assertEquals(1, $task['project_id']);
- $this->assertEquals(1, $task['column_id']);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(0, $task['creator_id']);
-
- $this->assertEquals('test', $task['title']);
- $this->assertEquals('', $task['description']);
- $this->assertEquals('', $task['reference']);
-
- $this->assertEquals(time(), $task['date_creation'], 'Wrong timestamp', 1);
- $this->assertEquals(time(), $task['date_modification'], 'Wrong timestamp', 1);
- $this->assertEquals(0, $task['date_due']);
- $this->assertEquals(0, $task['date_completed']);
- $this->assertEquals(0, $task['date_started']);
-
- $this->assertEquals(0, $task['time_estimated']);
- $this->assertEquals(0, $task['time_spent']);
-
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(1, $task['is_active']);
- $this->assertEquals(0, $task['score']);
- }
-
- public function testColorId()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'color_id' => 'blue')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals('blue', $task['color_id']);
- }
-
- public function testOwnerId()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(1, $task['owner_id']);
- }
-
- public function testCategoryId()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'category_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(1, $task['category_id']);
- }
-
- public function testCreatorId()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'creator_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(1, $task['creator_id']);
- }
-
- public function testThatCreatorIsDefined()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->container['sessionStorage']->user = array('id' => 1);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(1, $task['creator_id']);
- }
-
- public function testColumnId()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(2, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- }
-
- public function testPosition()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(2, $task['column_id']);
- $this->assertEquals(1, $task['position']);
-
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test', 'column_id' => 2)));
-
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(2, $task['id']);
- $this->assertEquals(2, $task['column_id']);
- $this->assertEquals(2, $task['position']);
- }
-
- public function testDescription()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'description' => 'test')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals('test', $task['description']);
- }
-
- public function testReference()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'reference' => 'test')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals('test', $task['reference']);
- }
-
- public function testDateDue()
- {
- $date = '2014-11-23';
- $timestamp = strtotime('+2days');
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_due' => $date)));
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_due' => $timestamp)));
- $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_due' => '')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['id']);
- $this->assertEquals($date, date('Y-m-d', $task['date_due']));
-
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(2, $task['id']);
- $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']);
- $this->assertEquals(0, $task['date_due']);
- }
-
- public function testDateStarted()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
-
- // Set only a date
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24')));
-
- $task = $tf->getById(1);
- $this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
-
- // Set a datetime
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24 16:25')));
-
- $task = $tf->getById(2);
- $this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
-
- // Set a datetime
- $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '11/24/2014 18:25')));
-
- $task = $tf->getById(3);
- $this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
-
- // Set a timestamp
- $this->assertEquals(4, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => time())));
-
- $task = $tf->getById(4);
- $this->assertEquals(time(), $task['date_started'], '', 1);
-
- // Set empty string
- $this->assertEquals(5, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '')));
- $task = $tf->getById(5);
- $this->assertEquals(0, $task['date_started']);
- }
-
- public function testTime()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.5, 'time_spent' => 2.3)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
-
- $this->assertEquals(1, $task['id']);
- $this->assertEquals(1.5, $task['time_estimated']);
- $this->assertEquals(2.3, $task['time_spent']);
- }
-
- public function testStripColumn()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'another_task' => '1')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- }
-
- public function testScore()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'score' => '3')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertNotFalse($task);
- $this->assertEquals(3, $task['score']);
- }
-
- public function testDefaultColor()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $c = new ConfigModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test1')));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals('yellow', $task['color_id']);
-
- $this->assertTrue($c->save(array('default_color' => 'orange')));
- $this->container['memoryCache']->flush();
-
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test2')));
-
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals('orange', $task['color_id']);
- }
-
- public function testDueDateYear2038TimestampBug()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_due' => strtotime('2050-01-10 12:30'))));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals('2050-01-10 00:00', date('Y-m-d H:i', $task['date_due']));
- }
-}
diff --git a/tests/units/Model/TaskDuplicationModelTest.php b/tests/units/Model/TaskDuplicationModelTest.php
new file mode 100644
index 00000000..1c42b6a1
--- /dev/null
+++ b/tests/units/Model/TaskDuplicationModelTest.php
@@ -0,0 +1,142 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\TaskTagModel;
+
+class TaskDuplicationModelTest extends Base
+{
+ public function testThatDuplicateDefineCreator()
+ {
+ $taskDuplicationModel = new TaskDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(0, $task['creator_id']);
+
+ $this->container['sessionStorage']->user = array('id' => 1);
+
+ // We duplicate our task
+ $this->assertEquals(2, $taskDuplicationModel->duplicate(1));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['creator_id']);
+ }
+
+ public function testDuplicateSameProject()
+ {
+ $taskDuplicationModel = new TaskDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+
+ // We create a task and a project
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+
+ // Some categories
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 1)));
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #2', 'project_id' => 1)));
+ $this->assertTrue($categoryModel->exists(1));
+ $this->assertTrue($categoryModel->exists(2));
+
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'column_id' => 3,
+ 'owner_id' => 1,
+ 'category_id' => 2,
+ 'time_spent' => 4.4
+ )));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(3, $task['column_id']);
+ $this->assertEquals(2, $task['category_id']);
+ $this->assertEquals(4.4, $task['time_spent']);
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
+
+ // We duplicate our task
+ $this->assertEquals(2, $taskDuplicationModel->duplicate(1));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(TaskModel::STATUS_OPEN, $task['is_active']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(1, $task['owner_id']);
+ $this->assertEquals(2, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(3, $task['column_id']);
+ $this->assertEquals(2, $task['position']);
+ $this->assertEquals('test', $task['title']);
+ $this->assertEquals(0, $task['time_spent']);
+ }
+
+ public function testDuplicateSameProjectWithTags()
+ {
+ $taskDuplicationModel = new TaskDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'tags' => array('T1', 'T2')
+ )));
+
+ $this->assertEquals(2, $taskDuplicationModel->duplicate(1));
+
+ $tags = $taskTagModel->getList(2);
+ $this->assertCount(2, $tags);
+ $this->assertArrayHasKey(1, $tags);
+ $this->assertArrayHasKey(2, $tags);
+ }
+
+ public function testDuplicateSameProjectWithPriority()
+ {
+ $taskDuplicationModel = new TaskDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'priority' => 2
+ )));
+
+ $this->assertEquals(2, $taskDuplicationModel->duplicate(1));
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['priority']);
+ }
+}
diff --git a/tests/units/Model/TaskDuplicationTest.php b/tests/units/Model/TaskDuplicationTest.php
deleted file mode 100644
index 79b75e54..00000000
--- a/tests/units/Model/TaskDuplicationTest.php
+++ /dev/null
@@ -1,700 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Core\DateParser;
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\TaskDuplicationModel;
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Model\ProjectModel;
-use Kanboard\Model\ProjectUserRoleModel;
-use Kanboard\Model\CategoryModel;
-use Kanboard\Model\UserModel;
-use Kanboard\Model\SwimlaneModel;
-use Kanboard\Core\Security\Role;
-
-class TaskDuplicationTest extends Base
-{
- public function testThatDuplicateDefineCreator()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(1, $task['project_id']);
- $this->assertEquals(0, $task['creator_id']);
-
- $this->container['sessionStorage']->user = array('id' => 1);
-
- // We duplicate our task
- $this->assertEquals(2, $td->duplicate(1));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['creator_id']);
- }
-
- public function testDuplicateSameProject()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
-
- // We create a task and a project
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
-
- // 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));
- $this->assertTrue($c->exists(2));
-
- $this->assertEquals(
- 1,
- $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1, 'category_id' => 2, 'time_spent' => 4.4)
- ));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(1, $task['project_id']);
- $this->assertEquals(3, $task['column_id']);
- $this->assertEquals(2, $task['category_id']);
- $this->assertEquals(4.4, $task['time_spent']);
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
-
- // We duplicate our task
- $this->assertEquals(2, $td->duplicate(1));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(TaskModel::STATUS_OPEN, $task['is_active']);
- $this->assertEquals(1, $task['project_id']);
- $this->assertEquals(1, $task['owner_id']);
- $this->assertEquals(2, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(3, $task['column_id']);
- $this->assertEquals(2, $task['position']);
- $this->assertEquals('test', $task['title']);
- $this->assertEquals(0, $task['time_spent']);
- }
-
- public function testDuplicateAnotherProject()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 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)));
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testDuplicateAnotherProjectWithCategory()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $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));
- $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)));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(2, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testDuplicateAnotherProjectWithPredefinedCategory()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $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));
- $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)));
-
- // We duplicate our task to the 2nd project with no category
- $this->assertEquals(2, $td->duplicateToProject(1, 2, null, null, 0));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['category_id']);
-
- // We duplicate our task to the 2nd project with a different category
- $this->assertEquals(3, $td->duplicateToProject(1, 2, null, null, 3));
-
- // Check the values of the duplicated task
- $task = $tf->getById(3);
- $this->assertNotEmpty($task);
- $this->assertEquals(3, $task['category_id']);
- }
-
- public function testDuplicateAnotherProjectWithSwimlane()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $s = new SwimlaneModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($s->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(2, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testDuplicateAnotherProjectWithoutSwimlane()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $s = new SwimlaneModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($s->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testDuplicateAnotherProjectWithPredefinedSwimlane()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $s = new SwimlaneModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($s->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2, 3));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(3, $task['swimlane_id']);
- }
-
- public function testDuplicateAnotherProjectWithPredefinedColumn()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2)));
-
- // We duplicate our task to the 2nd project with a different column
- $this->assertEquals(2, $td->duplicateToProject(1, 2, null, 7));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(7, $task['column_id']);
- }
-
- public function testDuplicateAnotherProjectWithUser()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
-
- // We create a new user for our project
- $user = new UserModel($this->container);
- $this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
- $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(3, $td->duplicateToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(3);
- $this->assertNotEmpty($task);
- $this->assertEquals(2, $task['position']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(2, $task['owner_id']);
- $this->assertEquals(2, $task['project_id']);
-
- // We duplicate a task with a not allowed user
- $this->assertEquals(4, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 3)));
- $this->assertEquals(5, $td->duplicateToProject(4, 2));
-
- $task = $tf->getById(5);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals(5, $task['column_id']);
- }
-
- public function testDuplicateAnotherProjectWithPredefinedUser()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $pr = new ProjectUserRoleModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
- $this->assertTrue($pr->addUser(2, 1, Role::PROJECT_MEMBER));
-
- // We duplicate our task to the 2nd project
- $this->assertEquals(2, $td->duplicateToProject(1, 2, null, null, null, 1));
-
- // Check the values of the duplicated task
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['owner_id']);
- }
-
- public function onMoveProject($event)
- {
- $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
-
- $event_data = $event->getAll();
- $this->assertNotEmpty($event_data);
- $this->assertEquals(1, $event_data['task_id']);
- $this->assertEquals('test', $event_data['title']);
- }
-
- public function testMoveAnotherProject()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $user = new UserModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'owner_id' => 1, 'category_id' => 10, 'position' => 333)));
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_PROJECT, array($this, 'onMoveProject'));
-
- // We duplicate our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_MOVE_PROJECT.'.TaskDuplicationTest::onMoveProject', $called);
-
- // Check the values of the moved task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals(5, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testMoveAnotherProjectWithCategory()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $c = new CategoryModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $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));
- $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)));
-
- // We move our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- // Check the values of the duplicated task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(2, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testMoveAnotherProjectWithUser()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
- $user = new UserModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a new user for our project
- $this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
- $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
-
- // We move our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- // Check the values of the moved task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['owner_id']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals(6, $task['column_id']);
- }
-
- public function testMoveAnotherProjectWithForbiddenUser()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $pp = new ProjectUserRoleModel($this->container);
- $user = new UserModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- // We create a new user for our project
- $this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
- $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 3)));
-
- // We move our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- // Check the values of the moved task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals(6, $task['column_id']);
- }
-
- public function testMoveAnotherProjectWithSwimlane()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $s = new SwimlaneModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($s->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
-
- // We move our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- // Check the values of the moved task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(2, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testMoveAnotherProjectWithoutSwimlane()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $s = new SwimlaneModel($this->container);
-
- // We create 2 projects
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertNotFalse($s->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
- $this->assertNotFalse($s->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
-
- // We create a task
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
-
- // We move our task to the 2nd project
- $this->assertTrue($td->moveToProject(1, 2));
-
- // Check the values of the moved task
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(0, $task['owner_id']);
- $this->assertEquals(0, $task['category_id']);
- $this->assertEquals(0, $task['swimlane_id']);
- $this->assertEquals(6, $task['column_id']);
- $this->assertEquals(1, $task['position']);
- $this->assertEquals(2, $task['project_id']);
- $this->assertEquals('test', $task['title']);
- }
-
- public function testCalculateRecurringTaskDueDate()
- {
- $td = new TaskDuplicationModel($this->container);
-
- $values = array('date_due' => 0);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(0, $values['date_due']);
-
- $values = array('date_due' => 0, 'recurrence_factor' => 0, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(0, $values['date_due']);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => 1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(time() + 86400, $values['date_due'], '', 1);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => -2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(time() - 2 * 86400, $values['date_due'], '', 1);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => 1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(1431291376 + 86400, $values['date_due'], '', 1);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => -1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(1431291376 - 86400, $values['date_due'], '', 1);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => 2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_MONTHS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(1436561776, $values['date_due'], '', 1);
-
- $values = array('date_due' => 1431291376, 'recurrence_factor' => 2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_YEARS);
- $td->calculateRecurringTaskDueDate($values);
- $this->assertEquals(1494449776, $values['date_due'], '', 1);
- }
-
- public function testDuplicateRecurringTask()
- {
- $td = new TaskDuplicationModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
- $dp = new DateParser($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
-
- $this->assertEquals(1, $tc->create(array(
- 'title' => 'test',
- 'project_id' => 1,
- 'date_due' => 1436561776,
- 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING,
- 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_CLOSE,
- 'recurrence_factor' => 2,
- 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS,
- 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE,
- )));
-
- $this->assertEquals(2, $td->duplicateRecurringTask(1));
-
- $task = $tf->getById(1);
- $this->assertNotEmpty($task);
- $this->assertEquals(TaskModel::RECURRING_STATUS_PROCESSED, $task['recurrence_status']);
- $this->assertEquals(2, $task['recurrence_child']);
- $this->assertEquals(1436486400, $task['date_due'], '', 2);
-
- $task = $tf->getById(2);
- $this->assertNotEmpty($task);
- $this->assertEquals(TaskModel::RECURRING_STATUS_PENDING, $task['recurrence_status']);
- $this->assertEquals(TaskModel::RECURRING_TRIGGER_CLOSE, $task['recurrence_trigger']);
- $this->assertEquals(TaskModel::RECURRING_TIMEFRAME_DAYS, $task['recurrence_timeframe']);
- $this->assertEquals(TaskModel::RECURRING_BASEDATE_TRIGGERDATE, $task['recurrence_basedate']);
- $this->assertEquals(1, $task['recurrence_parent']);
- $this->assertEquals(2, $task['recurrence_factor']);
- $this->assertEquals($dp->removeTimeFromTimestamp(strtotime('+2 days')), $task['date_due'], '', 2);
- }
-}
diff --git a/tests/units/Model/TaskFinderModelTest.php b/tests/units/Model/TaskFinderModelTest.php
new file mode 100644
index 00000000..72da3b6d
--- /dev/null
+++ b/tests/units/Model/TaskFinderModelTest.php
@@ -0,0 +1,142 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+
+class TaskFinderModelTest extends Base
+{
+ public function testGetTasksForDashboard()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $columnModel = new ColumnModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1)));
+
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 1));
+
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 0));
+
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+ }
+
+ public function testGetOverdueTasks()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1)));
+
+ $tasks = $taskFinderModel->getOverdueTasks();
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+
+ public function testGetOverdueTasksByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+
+ $tasks = $taskFinderModel->getOverdueTasksByProject(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+
+ public function testGetOverdueTasksByUser()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+
+ $tasks = $taskFinderModel->getOverdueTasksByUser(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(2, $tasks);
+
+ $this->assertEquals(1, $tasks[0]['id']);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ $this->assertEquals(1, $tasks[0]['owner_id']);
+ $this->assertEquals(1, $tasks[0]['project_id']);
+ $this->assertEquals('Project #1', $tasks[0]['project_name']);
+ $this->assertEquals('admin', $tasks[0]['assignee_username']);
+ $this->assertEquals('', $tasks[0]['assignee_name']);
+
+ $this->assertEquals('Task #2', $tasks[1]['title']);
+ }
+
+ public function testCountByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 2)));
+
+ $this->assertEquals(1, $taskFinderModel->countByProjectId(1));
+ $this->assertEquals(2, $taskFinderModel->countByProjectId(2));
+ }
+
+ public function testGetProjectToken()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+
+ $this->assertTrue($projectModel->enablePublicAccess(1));
+
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+
+ $project = $projectModel->getById(1);
+ $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1));
+ $this->assertEmpty($taskFinderModel->getProjectToken(2));
+ }
+}
diff --git a/tests/units/Model/TaskFinderTest.php b/tests/units/Model/TaskFinderTest.php
deleted file mode 100644
index 46792baf..00000000
--- a/tests/units/Model/TaskFinderTest.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Model\ProjectModel;
-
-class TaskFinderTest extends Base
-{
- public function testGetOverdueTasks()
- {
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
- $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
- $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0)));
- $this->assertEquals(4, $tc->create(array('title' => 'Task #3', 'project_id' => 1)));
-
- $tasks = $tf->getOverdueTasks();
- $this->assertNotEmpty($tasks);
- $this->assertTrue(is_array($tasks));
- $this->assertCount(1, $tasks);
- $this->assertEquals('Task #1', $tasks[0]['title']);
- }
-
- public function testGetOverdueTasksByProject()
- {
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(2, $p->create(array('name' => 'Project #2')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
- $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day'))));
- $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
- $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
- $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1)));
-
- $tasks = $tf->getOverdueTasksByProject(1);
- $this->assertNotEmpty($tasks);
- $this->assertTrue(is_array($tasks));
- $this->assertCount(1, $tasks);
- $this->assertEquals('Task #1', $tasks[0]['title']);
- }
-
- public function testGetOverdueTasksByUser()
- {
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(2, $p->create(array('name' => 'Project #2')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
- $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
- $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
- $this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
- $this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1)));
-
- $tasks = $tf->getOverdueTasksByUser(1);
- $this->assertNotEmpty($tasks);
- $this->assertTrue(is_array($tasks));
- $this->assertCount(2, $tasks);
-
- $this->assertEquals(1, $tasks[0]['id']);
- $this->assertEquals('Task #1', $tasks[0]['title']);
- $this->assertEquals(1, $tasks[0]['owner_id']);
- $this->assertEquals(1, $tasks[0]['project_id']);
- $this->assertEquals('Project #1', $tasks[0]['project_name']);
- $this->assertEquals('admin', $tasks[0]['assignee_username']);
- $this->assertEquals('', $tasks[0]['assignee_name']);
-
- $this->assertEquals('Task #2', $tasks[1]['title']);
- }
-
- public function testCountByProject()
- {
- $tc = new TaskCreationModel($this->container);
- $tf = new TaskFinderModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'Project #1')));
- $this->assertEquals(2, $p->create(array('name' => 'Project #2')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
- $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 2)));
- $this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 2)));
-
- $this->assertEquals(1, $tf->countByProjectId(1));
- $this->assertEquals(2, $tf->countByProjectId(2));
- }
-
- public function testGetProjectToken()
- {
- $taskCreationModel = new TaskCreationModel($this->container);
- $taskFinderModel = new TaskFinderModel($this->container);
- $projectModel = new ProjectModel($this->container);
-
- $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
- $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
-
- $this->assertTrue($projectModel->enablePublicAccess(1));
-
- $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
- $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
-
- $project = $projectModel->getById(1);
- $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1));
- $this->assertEmpty($taskFinderModel->getProjectToken(2));
- }
-}
diff --git a/tests/units/Model/TaskModelTest.php b/tests/units/Model/TaskModelTest.php
new file mode 100644
index 00000000..e42b5ae6
--- /dev/null
+++ b/tests/units/Model/TaskModelTest.php
@@ -0,0 +1,30 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\ProjectModel;
+
+class TaskModelTest extends Base
+{
+ public function testRemove()
+ {
+ $taskModel = new TaskModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+
+ $this->assertTrue($taskModel->remove(1));
+ $this->assertFalse($taskModel->remove(1234));
+ }
+
+ public function testGetTaskIdFromText()
+ {
+ $taskModel = new TaskModel($this->container);
+ $this->assertEquals(123, $taskModel->getTaskIdFromText('My task #123'));
+ $this->assertEquals(0, $taskModel->getTaskIdFromText('My task 123'));
+ }
+}
diff --git a/tests/units/Model/TaskModificationModelTest.php b/tests/units/Model/TaskModificationModelTest.php
new file mode 100644
index 00000000..c81f968b
--- /dev/null
+++ b/tests/units/Model/TaskModificationModelTest.php
@@ -0,0 +1,322 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskModificationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskTagModel;
+
+class TaskModificationModelTest extends Base
+{
+ public function onCreateUpdate($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
+
+ $event_data = $event->getAll();
+ $this->assertNotEmpty($event_data);
+ $this->assertEquals(1, $event_data['task_id']);
+ $this->assertEquals('Task #1', $event_data['title']);
+ }
+
+ public function onUpdate($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
+
+ $event_data = $event->getAll();
+ $this->assertNotEmpty($event_data);
+ $this->assertEquals(1, $event_data['task_id']);
+ $this->assertEquals('Task #1', $event_data['title']);
+ }
+
+ public function onAssigneeChange($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
+
+ $event_data = $event->getAll();
+ $this->assertNotEmpty($event_data);
+ $this->assertEquals(1, $event_data['task_id']);
+ $this->assertEquals(1, $event_data['owner_id']);
+ }
+
+ public function testThatNoEventAreFiredWhenNoChanges()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($this, 'onCreateUpdate'));
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, array($this, 'onUpdate'));
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'title' => 'test')));
+
+ $this->assertEmpty($this->container['dispatcher']->getCalledListeners());
+ }
+
+ public function testChangeTitle()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($this, 'onCreateUpdate'));
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, array($this, 'onUpdate'));
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'title' => 'Task #1')));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.TaskModificationModelTest::onCreateUpdate', $called);
+ $this->assertArrayHasKey(TaskModel::EVENT_UPDATE.'.TaskModificationModelTest::onUpdate', $called);
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('Task #1', $task['title']);
+ }
+
+ public function testChangeAssignee()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['owner_id']);
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_ASSIGNEE_CHANGE, array($this, 'onAssigneeChange'));
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'owner_id' => 1)));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_ASSIGNEE_CHANGE.'.TaskModificationModelTest::onAssigneeChange', $called);
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(1, $task['owner_id']);
+ }
+
+ public function testChangeDescription()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('', $task['description']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'description' => 'test')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('test', $task['description']);
+ }
+
+ public function testChangeCategory()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['category_id']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'category_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(1, $task['category_id']);
+ }
+
+ public function testChangeColor()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('yellow', $task['color_id']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'color_id' => 'blue')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('blue', $task['color_id']);
+ }
+
+ public function testChangeScore()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['score']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'score' => 13)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(13, $task['score']);
+ }
+
+ public function testChangeDueDate()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['date_due']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_due' => '2014-11-24')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('2014-11-24', date('Y-m-d', $task['date_due']));
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_due' => time())));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(date('Y-m-d'), date('Y-m-d', $task['date_due']));
+ }
+
+ public function testChangeStartedDate()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['date_started']);
+
+ // Set only a date
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_started' => '2014-11-24')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
+
+ // Set a datetime
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_started' => '2014-11-24 16:25')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
+
+ // Set a datetime
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_started' => '11/24/2014 18:25')));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
+
+ // Set a timestamp
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'date_started' => time())));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(time(), $task['date_started'], '', 1);
+ }
+
+ public function testChangeTimeEstimated()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['time_estimated']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'time_estimated' => 13.3)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(13.3, $task['time_estimated']);
+ }
+
+ public function testChangeTimeSpent()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(0, $task['time_spent']);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'time_spent' => 13.3)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertEquals(13.3, $task['time_spent']);
+ }
+
+ public function testChangeTags()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'tags' => array('tag1', 'tag2'))));
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'tags' => array('tag2'))));
+
+ $tags = $taskTagModel->getList(1);
+ $this->assertEquals(array(2 => 'tag2'), $tags);
+ }
+
+ public function testRemoveAllTags()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskModificationModel = new TaskModificationModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test1', 'project_id' => 1, 'tags' => array('tag1', 'tag2'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'test2', 'project_id' => 1, 'tags' => array('tag1', 'tag2'))));
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1)));
+ $tags = $taskTagModel->getList(1);
+ $this->assertEquals(array(1 => 'tag1', 2 => 'tag2'), $tags);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 1, 'tags' => array())));
+ $tags = $taskTagModel->getList(1);
+ $this->assertEquals(array(), $tags);
+
+ $this->assertTrue($taskModificationModel->update(array('id' => 2, 'tags' => array(''))));
+ $tags = $taskTagModel->getList(2);
+ $this->assertEquals(array(), $tags);
+ }
+}
diff --git a/tests/units/Model/TaskModificationTest.php b/tests/units/Model/TaskModificationTest.php
deleted file mode 100644
index 5cbc44e6..00000000
--- a/tests/units/Model/TaskModificationTest.php
+++ /dev/null
@@ -1,313 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\TaskModificationModel;
-use Kanboard\Model\TaskFinderModel;
-use Kanboard\Model\ProjectModel;
-use Kanboard\Model\TaskTagModel;
-
-class TaskModificationTest extends Base
-{
- public function onCreateUpdate($event)
- {
- $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
-
- $event_data = $event->getAll();
- $this->assertNotEmpty($event_data);
- $this->assertEquals(1, $event_data['task_id']);
- $this->assertEquals('Task #1', $event_data['title']);
- }
-
- public function onUpdate($event)
- {
- $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
-
- $event_data = $event->getAll();
- $this->assertNotEmpty($event_data);
- $this->assertEquals(1, $event_data['task_id']);
- $this->assertEquals('Task #1', $event_data['title']);
- }
-
- public function onAssigneeChange($event)
- {
- $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
-
- $event_data = $event->getAll();
- $this->assertNotEmpty($event_data);
- $this->assertEquals(1, $event_data['task_id']);
- $this->assertEquals(1, $event_data['owner_id']);
- }
-
- public function testThatNoEventAreFiredWhenNoChanges()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($this, 'onCreateUpdate'));
- $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, array($this, 'onUpdate'));
-
- $this->assertTrue($tm->update(array('id' => 1, 'title' => 'test')));
-
- $this->assertEmpty($this->container['dispatcher']->getCalledListeners());
- }
-
- public function testChangeTitle()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, array($this, 'onCreateUpdate'));
- $this->container['dispatcher']->addListener(TaskModel::EVENT_UPDATE, array($this, 'onUpdate'));
-
- $this->assertTrue($tm->update(array('id' => 1, 'title' => 'Task #1')));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.TaskModificationTest::onCreateUpdate', $called);
- $this->assertArrayHasKey(TaskModel::EVENT_UPDATE.'.TaskModificationTest::onUpdate', $called);
-
- $task = $tf->getById(1);
- $this->assertEquals('Task #1', $task['title']);
- }
-
- public function testChangeAssignee()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['owner_id']);
-
- $this->container['dispatcher']->addListener(TaskModel::EVENT_ASSIGNEE_CHANGE, array($this, 'onAssigneeChange'));
-
- $this->assertTrue($tm->update(array('id' => 1, 'owner_id' => 1)));
-
- $called = $this->container['dispatcher']->getCalledListeners();
- $this->assertArrayHasKey(TaskModel::EVENT_ASSIGNEE_CHANGE.'.TaskModificationTest::onAssigneeChange', $called);
-
- $task = $tf->getById(1);
- $this->assertEquals(1, $task['owner_id']);
- }
-
- public function testChangeDescription()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals('', $task['description']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'description' => 'test')));
-
- $task = $tf->getById(1);
- $this->assertEquals('test', $task['description']);
- }
-
- public function testChangeCategory()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['category_id']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'category_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(1, $task['category_id']);
- }
-
- public function testChangeColor()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals('yellow', $task['color_id']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'color_id' => 'blue')));
-
- $task = $tf->getById(1);
- $this->assertEquals('blue', $task['color_id']);
- }
-
- public function testChangeScore()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['score']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'score' => 13)));
-
- $task = $tf->getById(1);
- $this->assertEquals(13, $task['score']);
- }
-
- public function testChangeDueDate()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['date_due']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'date_due' => '2014-11-24')));
-
- $task = $tf->getById(1);
- $this->assertEquals('2014-11-24', date('Y-m-d', $task['date_due']));
-
- $this->assertTrue($tm->update(array('id' => 1, 'date_due' => time())));
-
- $task = $tf->getById(1);
- $this->assertEquals(date('Y-m-d'), date('Y-m-d', $task['date_due']));
- }
-
- public function testChangeStartedDate()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['date_started']);
-
- // Set only a date
- $this->assertTrue($tm->update(array('id' => 1, 'date_started' => '2014-11-24')));
-
- $task = $tf->getById(1);
- $this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
-
- // Set a datetime
- $this->assertTrue($tm->update(array('id' => 1, 'date_started' => '2014-11-24 16:25')));
-
- $task = $tf->getById(1);
- $this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
-
- // Set a datetime
- $this->assertTrue($tm->update(array('id' => 1, 'date_started' => '11/24/2014 18:25')));
-
- $task = $tf->getById(1);
- $this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
-
- // Set a timestamp
- $this->assertTrue($tm->update(array('id' => 1, 'date_started' => time())));
-
- $task = $tf->getById(1);
- $this->assertEquals(time(), $task['date_started'], '', 1);
- }
-
- public function testChangeTimeEstimated()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['time_estimated']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'time_estimated' => 13.3)));
-
- $task = $tf->getById(1);
- $this->assertEquals(13.3, $task['time_estimated']);
- }
-
- public function testChangeTimeSpent()
- {
- $p = new ProjectModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $tm = new TaskModificationModel($this->container);
- $tf = new TaskFinderModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
-
- $task = $tf->getById(1);
- $this->assertEquals(0, $task['time_spent']);
-
- $this->assertTrue($tm->update(array('id' => 1, 'time_spent' => 13.3)));
-
- $task = $tf->getById(1);
- $this->assertEquals(13.3, $task['time_spent']);
- }
-
- public function testChangeTags()
- {
- $projectModel = new ProjectModel($this->container);
- $taskCreationModel = new TaskCreationModel($this->container);
- $taskModificationModel = new TaskModificationModel($this->container);
- $taskTagModel = new TaskTagModel($this->container);
-
- $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
- $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'tags' => array('tag1', 'tag2'))));
- $this->assertTrue($taskModificationModel->update(array('id' => 1, 'tags' => array('tag2'))));
-
- $tags = $taskTagModel->getList(1);
- $this->assertEquals(array(2 => 'tag2'), $tags);
- }
-
- public function testRemoveAllTags()
- {
- $projectModel = new ProjectModel($this->container);
- $taskCreationModel = new TaskCreationModel($this->container);
- $taskModificationModel = new TaskModificationModel($this->container);
- $taskTagModel = new TaskTagModel($this->container);
-
- $this->assertEquals(1, $projectModel->create(array('name' => 'test')));
- $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'tags' => array('tag1', 'tag2'))));
- $this->assertTrue($taskModificationModel->update(array('id' => 1)));
-
- $tags = $taskTagModel->getList(1);
- $this->assertEquals(array(), $tags);
- }
-}
diff --git a/tests/units/Model/TaskProjectDuplicationModelTest.php b/tests/units/Model/TaskProjectDuplicationModelTest.php
new file mode 100644
index 00000000..9ff33bc1
--- /dev/null
+++ b/tests/units/Model/TaskProjectDuplicationModelTest.php
@@ -0,0 +1,377 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Core\Security\Role;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\UserModel;
+
+class TaskProjectDuplicationModelTest extends Base
+{
+ public function testDuplicateAnotherProject()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 1)));
+ $this->assertTrue($categoryModel->exists(1));
+
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'column_id' => 2,
+ 'owner_id' => 1,
+ 'category_id' => 1,
+ 'priority' => 3,
+ )));
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE_UPDATE, function () {});
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_CREATE, function () {});
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE_UPDATE.'.closure', $called);
+ $this->assertArrayHasKey(TaskModel::EVENT_CREATE.'.closure', $called);
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals(3, $task['priority']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testDuplicateAnotherProjectWithCategory()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 1)));
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 2)));
+ $this->assertTrue($categoryModel->exists(1));
+ $this->assertTrue($categoryModel->exists(2));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1)));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(2, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testDuplicateAnotherProjectWithPredefinedCategory()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 1)));
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 2)));
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #2', 'project_id' => 2)));
+ $this->assertTrue($categoryModel->exists(1));
+ $this->assertTrue($categoryModel->exists(2));
+ $this->assertTrue($categoryModel->exists(3));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1)));
+
+ // We duplicate our task to the 2nd project with no category
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2, null, null, 0));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['category_id']);
+
+ // We duplicate our task to the 2nd project with a different category
+ $this->assertEquals(3, $taskProjectDuplicationModel->duplicateToProject(1, 2, null, null, 3));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(3);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(3, $task['category_id']);
+ }
+
+ public function testDuplicateAnotherProjectWithSwimlane()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(2, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testDuplicateAnotherProjectWithoutSwimlane()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testDuplicateAnotherProjectWithPredefinedSwimlane()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2, 3));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(3, $task['swimlane_id']);
+ }
+
+ public function testDuplicateAnotherProjectWithPredefinedColumn()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2)));
+
+ // We duplicate our task to the 2nd project with a different column
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2, null, 7));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(7, $task['column_id']);
+ }
+
+ public function testDuplicateAnotherProjectWithUser()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+
+ // We create a new user for our project
+ $user = new UserModel($this->container);
+ $this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MEMBER));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(3, $taskProjectDuplicationModel->duplicateToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(3);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['position']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(2, $task['owner_id']);
+ $this->assertEquals(2, $task['project_id']);
+
+ // We duplicate a task with a not allowed user
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 3)));
+ $this->assertEquals(5, $taskProjectDuplicationModel->duplicateToProject(4, 2));
+
+ $task = $taskFinderModel->getById(5);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals(5, $task['column_id']);
+ }
+
+ public function testDuplicateAnotherProjectWithPredefinedUser()
+ {
+ $taskProjectDuplicationModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
+ $this->assertTrue($projectUserRoleModel->addUser(2, 1, Role::PROJECT_MEMBER));
+
+ // We duplicate our task to the 2nd project
+ $this->assertEquals(2, $taskProjectDuplicationModel->duplicateToProject(1, 2, null, null, null, 1));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['owner_id']);
+ }
+
+ public function testDuplicateAnotherProjectWithDifferentTags()
+ {
+ $taskProjectMoveModel = new TaskProjectDuplicationModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $tagModel = new TagModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create our tags for each projects
+ $this->assertEquals(1, $tagModel->create(1, 'T1'));
+ $this->assertEquals(2, $tagModel->create(1, 'T2'));
+ $this->assertEquals(3, $tagModel->create(2, 'T2'));
+ $this->assertEquals(4, $tagModel->create(2, 'T3'));
+ $this->assertEquals(5, $tagModel->create(0, 'T4'));
+ $this->assertEquals(6, $tagModel->create(0, 'T5'));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'tags' => array('T1', 'T2', 'T5'))));
+
+ // We move our task to the 2nd project
+ $this->assertEquals(2, $taskProjectMoveModel->duplicateToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['id']);
+ $this->assertEquals(2, $task['project_id']);
+
+ // Check tags
+ $tags = $taskTagModel->getList(2);
+ $this->assertCount(2, $tags);
+ $this->assertArrayHasKey(3, $tags);
+ $this->assertArrayHasKey(6, $tags);
+ }
+}
diff --git a/tests/units/Model/TaskProjectMoveModelTest.php b/tests/units/Model/TaskProjectMoveModelTest.php
new file mode 100644
index 00000000..c4282638
--- /dev/null
+++ b/tests/units/Model/TaskProjectMoveModelTest.php
@@ -0,0 +1,277 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Core\Security\Role;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskProjectMoveModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\UserModel;
+
+class TaskProjectMoveModelTest extends Base
+{
+ public function onMoveProject($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\TaskEvent', $event);
+
+ $event_data = $event->getAll();
+ $this->assertNotEmpty($event_data);
+ $this->assertEquals(1, $event_data['task_id']);
+ $this->assertEquals('test', $event_data['title']);
+ }
+
+ public function testMoveAnotherProject()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'owner_id' => 1,
+ 'category_id' => 10,
+ 'position' => 333,
+ 'priority' => 1,
+ )));
+
+ $this->container['dispatcher']->addListener(TaskModel::EVENT_MOVE_PROJECT, array($this, 'onMoveProject'));
+
+ // We duplicate our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(TaskModel::EVENT_MOVE_PROJECT.'.TaskProjectMoveModelTest::onMoveProject', $called);
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals(5, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(1, $task['priority']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testMoveAnotherProjectWithCategory()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new CategoryModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 1)));
+ $this->assertNotFalse($categoryModel->create(array('name' => 'Category #1', 'project_id' => 2)));
+ $this->assertTrue($categoryModel->exists(1));
+ $this->assertTrue($categoryModel->exists(2));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1)));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the duplicated task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(2, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testMoveAnotherProjectWithUser()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+ $userModel = new UserModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a new user for our project
+ $this->assertNotFalse($userModel->create(array('username' => 'unittest#1', 'password' => 'unittest')));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MEMBER));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['owner_id']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals(6, $task['column_id']);
+ }
+
+ public function testMoveAnotherProjectWithForbiddenUser()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $projectUserRoleModel = new ProjectUserRoleModel($this->container);
+ $userModel = new UserModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create a new user for our project
+ $this->assertNotFalse($userModel->create(array('username' => 'unittest#1', 'password' => 'unittest')));
+ $this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($projectUserRoleModel->addUser(2, 2, Role::PROJECT_MEMBER));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 3)));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals(6, $task['column_id']);
+ }
+
+ public function testMoveAnotherProjectWithSwimlane()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #1')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(2, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testMoveAnotherProjectWithoutSwimlane()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $swimlaneModel = new SwimlaneModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 1, 'name' => 'Swimlane #1')));
+ $this->assertNotFalse($swimlaneModel->create(array('project_id' => 2, 'name' => 'Swimlane #2')));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(0, $task['owner_id']);
+ $this->assertEquals(0, $task['category_id']);
+ $this->assertEquals(0, $task['swimlane_id']);
+ $this->assertEquals(6, $task['column_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(2, $task['project_id']);
+ $this->assertEquals('test', $task['title']);
+ }
+
+ public function testMoveAnotherProjectWithDifferentTags()
+ {
+ $taskProjectMoveModel = new TaskProjectMoveModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $tagModel = new TagModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ // We create 2 projects
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'test2')));
+
+ // We create our tags for each projects
+ $this->assertEquals(1, $tagModel->create(1, 'T1'));
+ $this->assertEquals(2, $tagModel->create(1, 'T2'));
+ $this->assertEquals(3, $tagModel->create(2, 'T3'));
+ $this->assertEquals(4, $tagModel->create(2, 'T4'));
+ $this->assertEquals(5, $tagModel->create(0, 'T5'));
+ $this->assertEquals(6, $tagModel->create(0, 'T6'));
+
+ // We create a task
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'test', 'project_id' => 1, 'tags' => array('T1', 'T5', 'T6'))));
+
+ // We move our task to the 2nd project
+ $this->assertTrue($taskProjectMoveModel->moveToProject(1, 2));
+
+ // Check the values of the moved task
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['project_id']);
+
+ // Check tags
+ $tags = $taskTagModel->getList(1);
+ $this->assertCount(2, $tags);
+ $this->assertArrayHasKey(5, $tags);
+ $this->assertArrayHasKey(6, $tags);
+ }
+}
diff --git a/tests/units/Model/TaskRecurrenceModelTest.php b/tests/units/Model/TaskRecurrenceModelTest.php
new file mode 100644
index 00000000..85d77bc2
--- /dev/null
+++ b/tests/units/Model/TaskRecurrenceModelTest.php
@@ -0,0 +1,126 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Core\DateParser;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskRecurrenceModel;
+use Kanboard\Model\TaskTagModel;
+
+class TaskRecurrenceModelTest extends Base
+{
+ public function testRecurrenceSettings()
+ {
+ $taskRecurrenceModel = new TaskRecurrenceModel($this->container);
+
+ $statuses = $taskRecurrenceModel->getRecurrenceStatusList();
+ $this->assertCount(2, $statuses);
+ $this->assertArrayHasKey(TaskModel::RECURRING_STATUS_NONE, $statuses);
+ $this->assertArrayHasKey(TaskModel::RECURRING_STATUS_PENDING, $statuses);
+ $this->assertArrayNotHasKey(TaskModel::RECURRING_STATUS_PROCESSED, $statuses);
+
+ $triggers = $taskRecurrenceModel->getRecurrenceTriggerList();
+ $this->assertCount(3, $triggers);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, $triggers);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_LAST_COLUMN, $triggers);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_CLOSE, $triggers);
+
+ $dates = $taskRecurrenceModel->getRecurrenceBasedateList();
+ $this->assertCount(2, $dates);
+ $this->assertArrayHasKey(TaskModel::RECURRING_BASEDATE_DUEDATE, $dates);
+ $this->assertArrayHasKey(TaskModel::RECURRING_BASEDATE_TRIGGERDATE, $dates);
+
+ $timeframes = $taskRecurrenceModel->getRecurrenceTimeframeList();
+ $this->assertCount(3, $timeframes);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_DAYS, $timeframes);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_MONTHS, $timeframes);
+ $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_YEARS, $timeframes);
+ }
+
+ public function testCalculateRecurringTaskDueDate()
+ {
+ $taskRecurrenceModel = new TaskRecurrenceModel($this->container);
+
+ $values = array('date_due' => 0);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(0, $values['date_due']);
+
+ $values = array('date_due' => 0, 'recurrence_factor' => 0, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(0, $values['date_due']);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => 1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(time() + 86400, $values['date_due'], '', 1);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => -2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(time() - 2 * 86400, $values['date_due'], '', 1);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => 1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(1431291376 + 86400, $values['date_due'], '', 1);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => -1, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(1431291376 - 86400, $values['date_due'], '', 1);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => 2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_MONTHS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(1436561776, $values['date_due'], '', 1);
+
+ $values = array('date_due' => 1431291376, 'recurrence_factor' => 2, 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_DUEDATE, 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_YEARS);
+ $taskRecurrenceModel->calculateRecurringTaskDueDate($values);
+ $this->assertEquals(1494449776, $values['date_due'], '', 1);
+ }
+
+ public function testDuplicateRecurringTask()
+ {
+ $taskRecurrenceModel = new TaskRecurrenceModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $dateParser = new DateParser($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'title' => 'test',
+ 'project_id' => 1,
+ 'date_due' => 1436561776,
+ 'recurrence_status' => TaskModel::RECURRING_STATUS_PENDING,
+ 'recurrence_trigger' => TaskModel::RECURRING_TRIGGER_CLOSE,
+ 'recurrence_factor' => 2,
+ 'recurrence_timeframe' => TaskModel::RECURRING_TIMEFRAME_DAYS,
+ 'recurrence_basedate' => TaskModel::RECURRING_BASEDATE_TRIGGERDATE,
+ 'tags' => array('T1', 'T2'),
+ )));
+
+ $this->assertEquals(2, $taskRecurrenceModel->duplicateRecurringTask(1));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(TaskModel::RECURRING_STATUS_PROCESSED, $task['recurrence_status']);
+ $this->assertEquals(2, $task['recurrence_child']);
+ $this->assertEquals(1436486400, $task['date_due'], '', 2);
+
+ $task = $taskFinderModel->getById(2);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(TaskModel::RECURRING_STATUS_PENDING, $task['recurrence_status']);
+ $this->assertEquals(TaskModel::RECURRING_TRIGGER_CLOSE, $task['recurrence_trigger']);
+ $this->assertEquals(TaskModel::RECURRING_TIMEFRAME_DAYS, $task['recurrence_timeframe']);
+ $this->assertEquals(TaskModel::RECURRING_BASEDATE_TRIGGERDATE, $task['recurrence_basedate']);
+ $this->assertEquals(1, $task['recurrence_parent']);
+ $this->assertEquals(2, $task['recurrence_factor']);
+ $this->assertEquals($dateParser->removeTimeFromTimestamp(strtotime('+2 days')), $task['date_due'], '', 2);
+
+ $tags = $taskTagModel->getList(2);
+ $this->assertCount(2, $tags);
+ $this->assertArrayHasKey(1, $tags);
+ $this->assertArrayHasKey(2, $tags);
+ }
+}
diff --git a/tests/units/Model/TaskTagModelTest.php b/tests/units/Model/TaskTagModelTest.php
index 73bbeac1..3485368e 100644
--- a/tests/units/Model/TaskTagModelTest.php
+++ b/tests/units/Model/TaskTagModelTest.php
@@ -22,7 +22,7 @@ class TaskTagModelTest extends Base
$this->assertEquals(1, $tagModel->create(0, 'My tag 1'));
$this->assertEquals(2, $tagModel->create(0, 'My tag 2'));
- $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', 'My tag 3')));
+ $this->assertTrue($taskTagModel->save(1, 1, array('My tag 1', 'My tag 2', '', 'My tag 3')));
$tags = $taskTagModel->getTagsByTask(1);
$this->assertCount(3, $tags);
@@ -124,4 +124,25 @@ class TaskTagModelTest extends Base
$tags = $taskTagModel->getTagsByTasks(array());
$this->assertEquals(array(), $tags);
}
+
+ public function testGetTagIdNotAvailableInDestinationProject()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskTagModel = new TaskTagModel($this->container);
+ $tagModel = new TagModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test1')));
+
+ $this->assertEquals(1, $tagModel->create(0, 'T0'));
+ $this->assertEquals(2, $tagModel->create(2, 'T1'));
+ $this->assertEquals(3, $tagModel->create(2, 'T3'));
+ $this->assertEquals(4, $tagModel->create(1, 'T2'));
+ $this->assertEquals(5, $tagModel->create(1, 'T3'));
+ $this->assertTrue($taskTagModel->save(1, 1, array('T0', 'T2', 'T3')));
+
+ $this->assertEquals(array(4, 5), $taskTagModel->getTagIdsByTaskNotAvailableInProject(1, 2));
+ }
}
diff --git a/tests/units/Model/TaskTest.php b/tests/units/Model/TaskTest.php
deleted file mode 100644
index 89fc4dc1..00000000
--- a/tests/units/Model/TaskTest.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\TaskModel;
-use Kanboard\Model\TaskCreationModel;
-use Kanboard\Model\ProjectModel;
-
-class TaskTest extends Base
-{
- public function testRemove()
- {
- $t = new TaskModel($this->container);
- $tc = new TaskCreationModel($this->container);
- $p = new ProjectModel($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
-
- $this->assertTrue($t->remove(1));
- $this->assertFalse($t->remove(1234));
- }
-
- public function testGetTaskIdFromText()
- {
- $t = new TaskModel($this->container);
- $this->assertEquals(123, $t->getTaskIdFromText('My task #123'));
- $this->assertEquals(0, $t->getTaskIdFromText('My task 123'));
- }
-
- public function testRecurrenceSettings()
- {
- $t = new TaskModel($this->container);
-
- $statuses = $t->getRecurrenceStatusList();
- $this->assertCount(2, $statuses);
- $this->assertArrayHasKey(TaskModel::RECURRING_STATUS_NONE, $statuses);
- $this->assertArrayHasKey(TaskModel::RECURRING_STATUS_PENDING, $statuses);
- $this->assertArrayNotHasKey(TaskModel::RECURRING_STATUS_PROCESSED, $statuses);
-
- $triggers = $t->getRecurrenceTriggerList();
- $this->assertCount(3, $triggers);
- $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, $triggers);
- $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_LAST_COLUMN, $triggers);
- $this->assertArrayHasKey(TaskModel::RECURRING_TRIGGER_CLOSE, $triggers);
-
- $dates = $t->getRecurrenceBasedateList();
- $this->assertCount(2, $dates);
- $this->assertArrayHasKey(TaskModel::RECURRING_BASEDATE_DUEDATE, $dates);
- $this->assertArrayHasKey(TaskModel::RECURRING_BASEDATE_TRIGGERDATE, $dates);
-
- $timeframes = $t->getRecurrenceTimeframeList();
- $this->assertCount(3, $timeframes);
- $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_DAYS, $timeframes);
- $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_MONTHS, $timeframes);
- $this->assertArrayHasKey(TaskModel::RECURRING_TIMEFRAME_YEARS, $timeframes);
- }
-}
diff --git a/tests/units/User/Avatar/LetterAvatarProviderTest.php b/tests/units/User/Avatar/LetterAvatarProviderTest.php
index 0eb3ef8e..c145967b 100644
--- a/tests/units/User/Avatar/LetterAvatarProviderTest.php
+++ b/tests/units/User/Avatar/LetterAvatarProviderTest.php
@@ -23,7 +23,7 @@ class LetterAvatarProviderTest extends Base
{
$provider = new LetterAvatarProvider($this->container);
$user = array('id' => 123, 'name' => 'Kanboard Admin', 'username' => 'bob', 'email' => '');
- $expected = '<div class="avatar-letter" style="background-color: rgb(131, 224, 108)" title="Kanboard Admin">KA</div>';
+ $expected = '<div class="avatar-letter" style="background-color: rgb(120, 83, 58)" title="Kanboard Admin">KA</div>';
$this->assertEquals($expected, $provider->render($user, 48));
}
diff --git a/web.config b/web.config
index c86be8b4..1461fe2d 100644
--- a/web.config
+++ b/web.config
@@ -1,12 +1,17 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0"?>
<configuration>
<system.webServer>
+ <defaultDocument>
+ <files>
+ <clear />
+ <add value="index.php" />
+ </files>
+ </defaultDocument>
<rewrite>
<rules>
- <clear />
- <rule name="Kanboard-nice-urls" stopProcessing="true">
- <match url="^" ignoreCase="false" />
- <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
+ <rule name="Kanboard URL Rewrite" 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" />