summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Auth/Ldap.php2
-rw-r--r--app/Controller/User.php4
-rw-r--r--app/Core/Lexer.php2
-rw-r--r--app/Locale/da_DK/translations.php2
-rw-r--r--app/Locale/de_DE/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.php2
-rw-r--r--app/Locale/hu_HU/translations.php2
-rw-r--r--app/Locale/it_IT/translations.php2
-rw-r--r--app/Locale/ja_JP/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/ru_RU/translations.php2
-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/TaskFilter.php80
-rw-r--r--app/Model/TaskFinder.php3
-rw-r--r--app/Model/User.php8
-rw-r--r--app/Template/listing/show.php18
-rw-r--r--app/Template/search/results.php20
-rw-r--r--app/Template/user/create_local.php (renamed from app/Template/user/new.php)1
-rw-r--r--app/Template/user/create_remote.php57
-rw-r--r--app/Template/user/index.php3
-rw-r--r--app/Template/user/layout.php3
-rw-r--r--app/constants.php1
-rw-r--r--config.default.php3
-rw-r--r--docs/config.markdown3
-rw-r--r--docs/ldap-authentication.markdown18
-rw-r--r--docs/search.markdown20
-rw-r--r--docs/user-management.markdown16
-rw-r--r--tests/units/LexerTest.php25
-rw-r--r--tests/units/TaskFilterTest.php108
37 files changed, 389 insertions, 40 deletions
diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php
index 3ee6ec9b..c1459b4e 100644
--- a/app/Auth/Ldap.php
+++ b/app/Auth/Ldap.php
@@ -46,7 +46,7 @@ class Ldap extends Base
else {
// We create automatically a new user
- if ($this->createUser($username, $result['name'], $result['email'])) {
+ if (LDAP_ACCOUNT_CREATION && $this->createUser($username, $result['name'], $result['email'])) {
$user = $this->user->getByUsername($username);
}
else {
diff --git a/app/Controller/User.php b/app/Controller/User.php
index 189d068d..10a3a931 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -60,7 +60,9 @@ class User extends Base
*/
public function create(array $values = array(), array $errors = array())
{
- $this->response->html($this->template->layout('user/new', array(
+ $is_remote = $this->request->getIntegerParam('remote') == 1 || (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1);
+
+ $this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array(
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
index 3887dc82..0a237254 100644
--- a/app/Core/Lexer.php
+++ b/app/Core/Lexer.php
@@ -33,6 +33,7 @@ class Lexer
"/^(category:)/" => 'T_CATEGORY',
"/^(column:)/" => 'T_COLUMN',
"/^(project:)/" => 'T_PROJECT',
+ "/^(swimlane:)/" => 'T_SWIMLANE',
"/^(ref:)/" => 'T_REFERENCE',
"/^(reference:)/" => 'T_REFERENCE',
"/^(\s+)/" => 'T_WHITESPACE',
@@ -116,6 +117,7 @@ class Lexer
case 'T_CATEGORY':
case 'T_COLUMN':
case 'T_PROJECT':
+ case 'T_SWIMLANE':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index c40574d9..5f55f3e8 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 37f759b2..90c029ba 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index f5259012..a2efe2c5 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 4093947f..ddbb26c3 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index 558c4f4d..1c4c76ee 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -998,4 +998,6 @@ return array(
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si vous cochez la case « Interdir le formulaire d\'authentification », les identifiants entrés dans le formulaire d\'authentification seront ignorés.',
'By @%s on Gitlab' => 'Par @%s sur Gitlab',
'Gitlab issue comment created' => 'Commentaire créé sur un ticket Gitlab',
+ 'New remote user' => 'Créer un utilisateur distant',
+ 'New local user' => 'Créer un utilisateur local',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 5d82f3e1..f1bd1453 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 5493d218..6dd60c18 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 3ced5ea1..e52f82b6 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 688f988d..ed8be05b 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index c61ec77c..9f8641a0 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index b5f4d0e0..68de0368 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 40e49952..3b7d678c 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 7a731c15..f3369341 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 73c94287..db552359 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index ae81e16a..c4d608bd 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 61210ddd..afb88744 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index 37bbda95..cd2518fd 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -996,4 +996,6 @@ return array(
// 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
// 'By @%s on Gitlab' => '',
// 'Gitlab issue comment created' => '',
+ // 'New remote user' => '',
+ // 'New local user' => '',
);
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 377ec3c6..0dbadbf8 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -71,6 +71,9 @@ class TaskFilter extends Base
case 'T_REFERENCE':
$this->filterByReference($value);
break;
+ case 'T_SWIMLANE':
+ $this->filterBySwimlaneName($value);
+ break;
}
}
@@ -101,6 +104,25 @@ class TaskFilter extends Base
}
/**
+ * Create a new subtask query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function createSubtaskQuery()
+ {
+ return $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ User::TABLE.'.name',
+ User::TABLE.'.username'
+ )
+ ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
+ ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
+ }
+
+ /**
* Clone the filter
*
* @access public
@@ -247,6 +269,30 @@ class TaskFilter extends Base
}
/**
+ * Filter by swimlane name
+ *
+ * @access public
+ * @param array $values List of swimlane name
+ * @return TaskFilter
+ */
+ public function filterBySwimlaneName(array $values)
+ {
+ $this->query->beginOr();
+
+ foreach ($values as $swimlane) {
+ if ($swimlane === 'default') {
+ $this->query->eq(Task::TABLE.'.swimlane_id', 0);
+ }
+ else {
+ $this->query->ilike(Swimlane::TABLE.'.name', $swimlane);
+ $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'");
+ }
+ }
+
+ $this->query->closeOr();
+ }
+
+ /**
* Filter by category id
*
* @access public
@@ -313,7 +359,6 @@ class TaskFilter extends Base
$this->query->beginOr();
foreach ($values as $assignee) {
-
switch ($assignee) {
case 'me':
$this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId());
@@ -327,7 +372,40 @@ class TaskFilter extends Base
}
}
+ $this->filterBySubtaskAssignee($values);
+
$this->query->closeOr();
+
+ return $this;
+ }
+
+ /**
+ * Filter by subtask assignee names
+ *
+ * @access public
+ * @param array $values List of assignees
+ * @return TaskFilter
+ */
+ public function filterBySubtaskAssignee(array $values)
+ {
+ $subtaskQuery = $this->createSubtaskQuery();
+ $subtaskQuery->beginOr();
+
+ foreach ($values as $assignee) {
+ if ($assignee === 'me') {
+ $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId());
+ }
+ else {
+ $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%');
+ $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%');
+ }
+ }
+
+ $subtaskQuery->closeOr();
+
+ $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id'));
+
+ return $this;
}
/**
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 2b0453a5..47a67a35 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -88,11 +88,14 @@ class TaskFinder extends Base
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
Board::TABLE.'.title AS column_name',
+ Swimlane::TABLE.'.name AS swimlane_name',
+ Project::TABLE.'.default_swimlane',
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE);
}
diff --git a/app/Model/User.php b/app/Model/User.php
index 36b7194c..b6804abc 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -377,6 +377,7 @@ class User extends Base
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Email('email', t('Email address invalid')),
new Validators\Integer('is_admin', t('This value must be an integer')),
+ new Validators\Integer('is_ldap_user', t('This value must be an integer')),
);
}
@@ -409,7 +410,12 @@ class User extends Base
new Validators\Required('username', t('The username is required')),
);
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
+ if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+ }
+ else {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
+ }
return array(
$v->execute(),
diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php
index 06940678..fc8a607b 100644
--- a/app/Template/listing/show.php
+++ b/app/Template/listing/show.php
@@ -10,13 +10,12 @@
<table class="table-fixed table-small">
<tr>
<th class="column-5"><?= $paginator->order(t('Id'), 'tasks.id') ?></th>
- <th class="column-8"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th>
- <th class="column-8"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Swimlane'), 'tasks.swimlane_id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th>
<th><?= $paginator->order(t('Title'), 'tasks.title') ?></th>
<th class="column-10"><?= $paginator->order(t('Assignee'), 'users.username') ?></th>
<th class="column-10"><?= $paginator->order(t('Due date'), 'tasks.date_due') ?></th>
- <th class="column-10"><?= $paginator->order(t('Date created'), 'tasks.date_creation') ?></th>
- <th class="column-10"><?= $paginator->order(t('Date completed'), 'tasks.date_completed') ?></th>
<th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></th>
</tr>
<?php foreach ($paginator->getCollection() as $task): ?>
@@ -25,6 +24,9 @@
<?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
</td>
<td>
+ <?= $this->e($task['swimlane_name'] ?: $task['default_swimlane']) ?>
+ </td>
+ <td>
<?= $this->e($task['column_name']) ?>
</td>
<td>
@@ -44,14 +46,6 @@
<?= dt('%B %e, %Y', $task['date_due']) ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_creation']) ?>
- </td>
- <td>
- <?php if ($task['date_completed']): ?>
- <?= dt('%B %e, %Y', $task['date_completed']) ?>
- <?php endif ?>
- </td>
- <td>
<?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?>
<?= t('Open') ?>
<?php else: ?>
diff --git a/app/Template/search/results.php b/app/Template/search/results.php
index 1d8cc6e2..04cb6a19 100644
--- a/app/Template/search/results.php
+++ b/app/Template/search/results.php
@@ -1,14 +1,13 @@
<table class="table-fixed table-small">
<tr>
<th class="column-8"><?= $paginator->order(t('Project'), 'tasks.project_id') ?></th>
- <th class="column-8"><?= $paginator->order(t('Id'), 'tasks.id') ?></th>
- <th class="column-8"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th>
- <th class="column-8"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th>
+ <th class="column-5"><?= $paginator->order(t('Id'), 'tasks.id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Swimlane'), 'tasks.swimlane_id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th>
<th><?= $paginator->order(t('Title'), 'tasks.title') ?></th>
<th class="column-10"><?= $paginator->order(t('Assignee'), 'users.username') ?></th>
<th class="column-10"><?= $paginator->order(t('Due date'), 'tasks.date_due') ?></th>
- <th class="column-10"><?= $paginator->order(t('Date created'), 'tasks.date_creation') ?></th>
- <th class="column-10"><?= $paginator->order(t('Date completed'), 'tasks.date_completed') ?></th>
<th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></th>
</tr>
<?php foreach ($paginator->getCollection() as $task): ?>
@@ -20,6 +19,9 @@
<?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
</td>
<td>
+ <?= $this->e($task['swimlane_name'] ?: $task['default_swimlane']) ?>
+ </td>
+ <td>
<?= $this->e($task['column_name']) ?>
</td>
<td>
@@ -39,14 +41,6 @@
<?= dt('%B %e, %Y', $task['date_due']) ?>
</td>
<td>
- <?= dt('%B %e, %Y', $task['date_creation']) ?>
- </td>
- <td>
- <?php if ($task['date_completed']): ?>
- <?= dt('%B %e, %Y', $task['date_completed']) ?>
- <?php endif ?>
- </td>
- <td>
<?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?>
<?= t('Open') ?>
<?php else: ?>
diff --git a/app/Template/user/new.php b/app/Template/user/create_local.php
index 0db1e824..aeec300f 100644
--- a/app/Template/user/new.php
+++ b/app/Template/user/create_local.php
@@ -2,6 +2,7 @@
<div class="page-header">
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
</ul>
</div>
<section>
diff --git a/app/Template/user/create_remote.php b/app/Template/user/create_remote.php
new file mode 100644
index 00000000..52661585
--- /dev/null
+++ b/app/Template/user/create_remote.php
@@ -0,0 +1,57 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
+ </ul>
+ </div>
+ <form method="post" action="<?= $this->url->href('user', 'save') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('is_ldap_user', array('is_ldap_user' => 1)) ?>
+
+ <div class="form-column">
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Email'), 'email') ?>
+ <?= $this->form->email('email', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Google Id'), 'google_id') ?>
+ <?= $this->form->password('google_id', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Github Id'), 'github_id') ?>
+ <?= $this->form->password('github_id', $values, $errors) ?><br/>
+ </div>
+
+ <div class="form-column">
+ <?= $this->form->label(t('Add project member'), 'project_id') ?>
+ <?= $this->form->select('project_id', $projects, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Timezone'), 'timezone') ?>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Language'), 'language') ?>
+ <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+
+ <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
+ <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
+ <?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'index') ?>
+ </div>
+ </form>
+ <div class="alert alert-info">
+ <ul>
+ <li><?= t('Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.') ?></li>
+ <li><?= t('If you check the box "Disallow login form", credentials entered in the login form will be ignored.') ?></li>
+ </ul>
+ </div>
+</section> \ No newline at end of file
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index fc575466..edf043a6 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -2,7 +2,8 @@
<div class="page-header">
<?php if ($this->user->isAdmin()): ?>
<ul>
- <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New user'), 'user', 'create') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
</ul>
<?php endif ?>
</div>
diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php
index e60ab77d..a27f359b 100644
--- a/app/Template/user/layout.php
+++ b/app/Template/user/layout.php
@@ -3,7 +3,8 @@
<?php if ($this->user->isAdmin()): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
- <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New user'), 'user', 'create') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
</ul>
<?php endif ?>
</div>
diff --git a/app/constants.php b/app/constants.php
index 7270db23..83fba468 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -35,6 +35,7 @@ defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname
defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail');
defined('LDAP_ACCOUNT_ID') or define('LDAP_ACCOUNT_ID', '');
defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
+defined('LDAP_ACCOUNT_CREATION') or define('LDAP_ACCOUNT_CREATION', true);
// Google authentication
defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false);
diff --git a/config.default.php b/config.default.php
index 38453455..c392dcad 100644
--- a/config.default.php
+++ b/config.default.php
@@ -109,6 +109,9 @@ define('LDAP_ACCOUNT_ID', 'samaccountname');
// Set to true if you want to preserve the case
define('LDAP_USERNAME_CASE_SENSITIVE', false);
+// Automatically create user account
+define('LDAP_ACCOUNT_CREATION', true);
+
// Enable/disable Google authentication
define('GOOGLE_AUTH', false);
diff --git a/docs/config.markdown b/docs/config.markdown
index aca605ca..45ba7a91 100644
--- a/docs/config.markdown
+++ b/docs/config.markdown
@@ -135,6 +135,9 @@ define('LDAP_ACCOUNT_ID', 'samaccountname');
// By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive)
// Set to true if you want to preserve the case
define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
+// Automatically create user account
+define('LDAP_ACCOUNT_CREATION', true);
```
Google Authentication settings
diff --git a/docs/ldap-authentication.markdown b/docs/ldap-authentication.markdown
index 2428194d..8c7e5ff5 100644
--- a/docs/ldap-authentication.markdown
+++ b/docs/ldap-authentication.markdown
@@ -17,7 +17,7 @@ When the LDAP authentication is activated, the login process work like that:
1. Try first to authenticate the user by using the database
2. If the user is not found inside the database, a LDAP authentication is performed
-3. If the LDAP authentication is successful, a local user is created automatically with no password and marked as LDAP user.
+3. If the LDAP authentication is successful, by default a local user is created automatically with no password and marked as LDAP user.
### Differences between a local user and a LDAP user are the following:
@@ -85,6 +85,22 @@ define('LDAP_ACCOUNT_ID', 'samaccountname');
// By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive)
// Set to true if you want to preserve the case
define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
+// Automatically create user account
+define('LDAP_ACCOUNT_CREATION', true);
+```
+
+### Disable automatic account creation
+
+By default, Kanboard will create automatically a user account if nothing is found.
+
+You can disable this behavior if you prefer to create user accounts manually to restrict Kanboard to only some people.
+
+Just change the value of `LDAP_ACCOUNT_CREATION` to `false`:
+
+```php
+// Automatically create user account
+define('LDAP_ACCOUNT_CREATION', false);
```
### LDAP bind type
diff --git a/docs/search.markdown b/docs/search.markdown
index 3d00b158..4674a07e 100644
--- a/docs/search.markdown
+++ b/docs/search.markdown
@@ -63,6 +63,8 @@ Query for my assigned tasks
assignee:me
```
+Note: Results will also include subtasks assignee with the status todo or in progress.
+
Search by color
---------------
@@ -125,6 +127,13 @@ Attribute: **description**
Example: `description:"text search"`
+Search by external reference
+----------------------------
+
+The task reference is an external id of your task, by example a ticket number from another software.
+
+- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
+
Search by category
------------------
@@ -151,9 +160,12 @@ Attribute: **column**
- Find tasks by column name: `column:"Work in progress"`
- Find tasks for several columns: `column:"Backlog" column:ready`
-Search by external reference
-----------------------------
+Search by swimlane
+------------------
-The task reference is an external id of your task, by example a ticket number from another software.
+Attribute: **swimlane**
+
+- Find tasks by swimlane: `swimlane:"Version 42"`
+- Find tasks in the default swimlane: `swimlane:default`
+- Find tasks into several swimlanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
-- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
diff --git a/docs/user-management.markdown b/docs/user-management.markdown
index bd9cdb17..98691ddd 100644
--- a/docs/user-management.markdown
+++ b/docs/user-management.markdown
@@ -1,8 +1,8 @@
User management
===============
-Type of users
--------------
+Group of users
+--------------
Kanboard use a basic permission system, there is two kind of users:
@@ -18,22 +18,30 @@ There is also permissions defined at the project level, users can be seen as:
Project managers have more privileges than a simple user member.
+Local and remote users
+----------------------
+
+- A local user is an account that use the database to store credentials. Local users use the login form for the authentication.
+- A remote user is an account that use an external system to store credentials. By example, it can be LDAP, Github or Google accounts. Authentication of these users can be done through the login form or not.
+
Add a new user
--------------
To add a new user, you must be administrator.
1. From the dashboard, go to the menu **User Management**
-2. On the top, you have a link **New user**
+2. On the top, you have a link **New local user** or **New remote user**
3. Fill the form and save
![New user](http://kanboard.net/screenshots/documentation/new-user.png)
-When you create a new user, you have to specify at least those values:
+When you create a **local user**, you have to specify at least those values:
- **username**: This is the unique identifier of your user (login)
- **password**: The password of your user must have at least 6 characters
+For **remote users**, only the username is mandatory. You can also pre-link Github or Google accounts if you already know their unique id.
+
Edit users
----------
diff --git a/tests/units/LexerTest.php b/tests/units/LexerTest.php
index bf0ffdd0..8710f79a 100644
--- a/tests/units/LexerTest.php
+++ b/tests/units/LexerTest.php
@@ -6,6 +6,31 @@ use Core\Lexer;
class LexerTest extends Base
{
+ public function testSwimlaneQuery()
+ {
+ $lexer = new Lexer;
+
+ $this->assertEquals(
+ array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'Version 42', 'token' => 'T_STRING')),
+ $lexer->tokenize('swimlane:"Version 42"')
+ );
+
+ $this->assertEquals(
+ array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'v3', 'token' => 'T_STRING')),
+ $lexer->tokenize('swimlane:v3')
+ );
+
+ $this->assertEquals(
+ array('T_SWIMLANE' => array('v3')),
+ $lexer->map($lexer->tokenize('swimlane:v3'))
+ );
+
+ $this->assertEquals(
+ array('T_SWIMLANE' => array('Version 42', 'v3')),
+ $lexer->map($lexer->tokenize('swimlane:"Version 42" swimlane:v3'))
+ );
+ }
+
public function testAssigneeQuery()
{
$lexer = new Lexer;
diff --git a/tests/units/TaskFilterTest.php b/tests/units/TaskFilterTest.php
index 14a9b1c0..47fe4e35 100644
--- a/tests/units/TaskFilterTest.php
+++ b/tests/units/TaskFilterTest.php
@@ -8,7 +8,9 @@ use Model\TaskFilter;
use Model\TaskCreation;
use Model\DateParser;
use Model\Category;
+use Model\Subtask;
use Model\Config;
+use Model\Swimlane;
class TaskFilterTest extends Base
{
@@ -286,6 +288,64 @@ class TaskFilterTest extends Base
$this->assertEmpty($tasks);
}
+ public function testSearchWithSwimlane()
+ {
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $tf = new TaskFilter($this->container);
+ $s = new Swimlane($this->container);
+
+ $this->assertEquals(1, $p->create(array('name' => 'My project A')));
+ $this->assertEquals(1, $s->create(1, 'Version 1.1'));
+ $this->assertEquals(2, $s->create(1, 'Version 1.2'));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'swimlane_id' => 1)));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'swimlane_id' => 2)));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'swimlane_id' => 0)));
+
+ $tf->search('swimlane:"Version 1.1"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
+
+ $tf->search('swimlane:"versioN 1.2"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task2', $tasks[0]['title']);
+ $this->assertEquals('Version 1.2', $tasks[0]['swimlane_name']);
+
+ $tf->search('swimlane:"Default swimlane"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task3', $tasks[0]['title']);
+ $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
+ $this->assertEquals('', $tasks[0]['swimlane_name']);
+
+ $tf->search('swimlane:default');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task3', $tasks[0]['title']);
+ $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
+ $this->assertEquals('', $tasks[0]['swimlane_name']);
+
+ $tf->search('swimlane:"Version 1.1" swimlane:"Version 1.2"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
+ $this->assertEquals('task2', $tasks[1]['title']);
+ $this->assertEquals('Version 1.2', $tasks[1]['swimlane_name']);
+
+ $tf->search('swimlane:"not found"');
+ $tasks = $tf->findAll();
+ $this->assertEmpty($tasks);
+ }
+
public function testSearchWithColumn()
{
$p = new Project($this->container);
@@ -469,6 +529,54 @@ class TaskFilterTest extends Base
$this->assertEquals('Bob at work', $tasks[1]['title']);
}
+ public function testSearchWithAssigneeIncludingSubtasks()
+ {
+ $p = new Project($this->container);
+ $u = new User($this->container);
+ $tc = new TaskCreation($this->container);
+ $s = new Subtask($this->container);
+ $tf = new TaskFilter($this->container);
+
+ $this->assertEquals(1, $p->create(array('name' => 'test')));
+ $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Paul Ryan')));
+
+ $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 2)));
+ $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'status' => 1, 'user_id' => 0)));
+
+ $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'task2', 'owner_id' => 0)));
+ $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 2, 'status' => 1, 'user_id' => 2)));
+
+ $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'task3', 'owner_id' => 0)));
+ $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 3, 'user_id' => 1)));
+
+ $tf->search('assignee:bob');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('task2', $tasks[1]['title']);
+
+ $tf->search('assignee:"Paul Ryan"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('task2', $tasks[1]['title']);
+
+ $tf->search('assignee:nobody');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task2', $tasks[0]['title']);
+ $this->assertEquals('task3', $tasks[1]['title']);
+
+ $tf->search('assignee:admin');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task3', $tasks[0]['title']);
+ }
+
public function testCopy()
{
$tf = new TaskFilter($this->container);