diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-10-17 15:27:43 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-10-17 15:27:43 -0400 |
commit | 9283fb88d80cb355ff98364a9a57b657fc511d98 (patch) | |
tree | 52c99cd8adf28e3eb2c30c3d625817b48a8cd4ec | |
parent | 9153c6ff0ddb3170928d33599d9178e67ca466b6 (diff) |
Add metadata for users, tasks and projects
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | app/Model/Config.php | 68 | ||||
-rw-r--r-- | app/Model/Metadata.php | 98 | ||||
-rw-r--r-- | app/Model/ProjectMetadata.php | 30 | ||||
-rw-r--r-- | app/Model/Setting.php | 96 | ||||
-rw-r--r-- | app/Model/TaskMetadata.php | 30 | ||||
-rw-r--r-- | app/Model/UserMetadata.php | 30 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 37 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 37 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 37 | ||||
-rw-r--r-- | app/ServiceProvider/ClassProvider.php | 3 | ||||
-rw-r--r-- | tests/units/Model/ConfigTest.php | 25 | ||||
-rw-r--r-- | tests/units/Model/ProjectMetadataTest.php | 47 | ||||
-rw-r--r-- | tests/units/Model/TaskMetadataTest.php | 37 | ||||
-rw-r--r-- | tests/units/Model/UserMetadataTest.php | 33 |
15 files changed, 553 insertions, 56 deletions
@@ -15,6 +15,7 @@ Improvements: * Allow to change comments sorting * Add the possibility to append or not custom filters * Make mail transports pluggable +* Add Task, User and Project metadata for plugin creators Version 1.0.19 -------------- diff --git a/app/Model/Config.php b/app/Model/Config.php index 6561efc8..cf634f80 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -12,16 +12,9 @@ use Kanboard\Core\Session; * @package model * @author Frederic Guillot */ -class Config extends Base +class Config extends Setting { /** - * SQL table name - * - * @var string - */ - const TABLE = 'settings'; - - /** * Get available currencies * * @access public @@ -170,8 +163,7 @@ class Config extends Base public function get($name, $default_value = '') { if (! Session::isOpen()) { - $value = $this->db->table(self::TABLE)->eq('option', $name)->findOneColumn('value'); - return $value ?: $default_value; + return $this->getOption($name, $default_value); } // Cache config in session @@ -187,43 +179,6 @@ class Config extends Base } /** - * Get all settings - * - * @access public - * @return array - */ - public function getAll() - { - return $this->db->hashtable(self::TABLE)->getAll('option', 'value'); - } - - /** - * Save settings in the database - * - * @access public - * @param $values array Settings values - * @return boolean - */ - public function save(array $values) - { - foreach ($values as $option => $value) { - - // Be sure that a trailing slash is there for the url - if ($option === 'application_url' && ! empty($value) && substr($value, -1) !== '/') { - $value .= '/'; - } - - $result = $this->db->table(self::TABLE)->eq('option', $option)->update(array('value' => $value)); - - if (! $result) { - return false; - } - } - - return true; - } - - /** * Reload settings in the session and the translations * * @access public @@ -310,8 +265,21 @@ class Config extends Base */ public function regenerateToken($option) { - return $this->db->table(self::TABLE) - ->eq('option', $option) - ->update(array('value' => Security::generateToken())); + $this->save(array($option => Security::generateToken())); + } + + /** + * Prepare data before save + * + * @access public + * @return array + */ + public function prepare(array $values) + { + if (! empty($values['application_url']) && substr($values['application_url'], -1) !== '/') { + $values['application_url'] = $values['application_url'].'/'; + } + + return $values; } } diff --git a/app/Model/Metadata.php b/app/Model/Metadata.php new file mode 100644 index 00000000..83c8f499 --- /dev/null +++ b/app/Model/Metadata.php @@ -0,0 +1,98 @@ +<?php + +namespace Kanboard\Model; + +/** + * Metadata + * + * @package model + * @author Frederic Guillot + */ +abstract class Metadata extends Base +{ + /** + * Define the entity key + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getEntityKey(); + + /** + * Get all metadata for the entity + * + * @access public + * @param integer $entity_id + * @return array + */ + public function getAll($entity_id) + { + return $this->db + ->hashtable(static::TABLE) + ->eq($this->getEntityKey(), $entity_id) + ->asc('name') + ->getAll('name', 'value'); + } + + /** + * Get a metadata for the given entity + * + * @access public + * @param integer $entity_id + * @param string $name + * @param string $default + * @return mixed + */ + public function get($entity_id, $name, $default = '') + { + return $this->db + ->table(static::TABLE) + ->eq($this->getEntityKey(), $entity_id) + ->eq('name', $name) + ->findOneColumn('value') ?: $default; + } + + /** + * Return true if a metadata exists + * + * @access public + * @param integer $entity_id + * @param string $name + * @return boolean + */ + public function exists($entity_id, $name) + { + return $this->db + ->table(static::TABLE) + ->eq($this->getEntityKey(), $entity_id) + ->eq('name', $name) + ->exists(); + } + + /** + * Update or insert new metadata + * + * @access public + * @param integer $entity_id + * @param array $values + */ + public function save($entity_id, array $values) + { + $results = array(); + + $this->db->startTransaction(); + + foreach ($values as $key => $value) { + if ($this->exists($entity_id, $key)) { + $results[] = $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $key)->update(array('value' => $value)); + } else { + $results[] = $this->db->table(static::TABLE)->insert(array('name' => $key, 'value' => $value, $this->getEntityKey() => $entity_id)); + } + } + + $this->db->closeTransaction(); + + return ! in_array(false, $results, true); + } +} diff --git a/app/Model/ProjectMetadata.php b/app/Model/ProjectMetadata.php new file mode 100644 index 00000000..85498053 --- /dev/null +++ b/app/Model/ProjectMetadata.php @@ -0,0 +1,30 @@ +<?php + +namespace Kanboard\Model; + +/** + * Project Metadata + * + * @package model + * @author Frederic Guillot + */ +class ProjectMetadata extends Metadata +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_has_metadata'; + + /** + * Define the entity key + * + * @access protected + * @return string + */ + protected function getEntityKey() + { + return 'project_id'; + } +} diff --git a/app/Model/Setting.php b/app/Model/Setting.php new file mode 100644 index 00000000..3507d424 --- /dev/null +++ b/app/Model/Setting.php @@ -0,0 +1,96 @@ +<?php + +namespace Kanboard\Model; + +/** + * Application Settings + * + * @package model + * @author Frederic Guillot + */ +abstract class Setting extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'settings'; + + /** + * Prepare data before save + * + * @abstract + * @access public + * @return array + */ + abstract public function prepare(array $values); + + /** + * Get all settings + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->hashtable(self::TABLE)->getAll('option', 'value'); + } + + /** + * Get a setting value + * + * @access public + * @param string $name + * @param string $default + * @return mixed + */ + public function getOption($name, $default = '') + { + return $this->db + ->table(self::TABLE) + ->eq('option', $name) + ->findOneColumn('value') ?: $default; + } + + /** + * Return true if a setting exists + * + * @access public + * @param string $name + * @return boolean + */ + public function exists($name) + { + return $this->db + ->table(self::TABLE) + ->eq('option', $name) + ->exists(); + } + + /** + * Update or insert new settings + * + * @access public + * @param array $values + */ + public function save(array $values) + { + $results = array(); + $values = $this->prepare($values); + + $this->db->startTransaction(); + + foreach ($values as $option => $value) { + if ($this->exists($option)) { + $results[] = $this->db->table(self::TABLE)->eq('option', $option)->update(array('value' => $value)); + } else { + $results[] = $this->db->table(self::TABLE)->insert(array('option' => $option, 'value' => $value)); + } + } + + $this->db->closeTransaction(); + + return ! in_array(false, $results, true); + } +} diff --git a/app/Model/TaskMetadata.php b/app/Model/TaskMetadata.php new file mode 100644 index 00000000..1fd18415 --- /dev/null +++ b/app/Model/TaskMetadata.php @@ -0,0 +1,30 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task Metadata + * + * @package model + * @author Frederic Guillot + */ +class TaskMetadata extends Metadata +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_metadata'; + + /** + * Define the entity key + * + * @access protected + * @return string + */ + protected function getEntityKey() + { + return 'task_id'; + } +} diff --git a/app/Model/UserMetadata.php b/app/Model/UserMetadata.php new file mode 100644 index 00000000..6f1222e2 --- /dev/null +++ b/app/Model/UserMetadata.php @@ -0,0 +1,30 @@ +<?php + +namespace Kanboard\Model; + +/** + * User Metadata + * + * @package model + * @author Frederic Guillot + */ +class UserMetadata extends Metadata +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'user_has_metadata'; + + /** + * Define the entity key + * + * @access protected + * @return string + */ + protected function getEntityKey() + { + return 'user_id'; + } +}
\ No newline at end of file diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 852dca92..b85c185e 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,40 @@ use PDO; use Kanboard\Core\Security; use Kanboard\Model\Link; -const VERSION = 92; +const VERSION = 93; + +function version_93($pdo) +{ + $pdo->exec(" + CREATE TABLE user_has_metadata ( + user_id INT NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, name) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE project_has_metadata ( + project_id INT NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, name) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE task_has_metadata ( + task_id INT NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + UNIQUE(task_id, name) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_92($pdo) { @@ -14,7 +47,7 @@ function version_92($pdo) CREATE TABLE project_has_notification_types ( id INT NOT NULL AUTO_INCREMENT, project_id INT NOT NULL, - notification_type VARCHAR(50), + notification_type VARCHAR(50) NOT NULL, PRIMARY KEY(id), FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE UNIQUE(project_id, notification_type), diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index ac8b15ee..1a399cfc 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,40 @@ use PDO; use Kanboard\Core\Security; use Kanboard\Model\Link; -const VERSION = 72; +const VERSION = 73; + +function version_73($pdo) +{ + $pdo->exec(" + CREATE TABLE user_has_metadata ( + user_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE project_has_metadata ( + project_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_metadata ( + task_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + value VARCHAR(255) DEFAULT '', + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + UNIQUE(task_id, name) + ) + "); +} function version_72($pdo) { @@ -14,7 +47,7 @@ function version_72($pdo) CREATE TABLE project_has_notification_types ( id SERIAL PRIMARY KEY, project_id INTEGER NOT NULL, - notification_type VARCHAR(50), + notification_type VARCHAR(50) NOT NULL, FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, UNIQUE(project_id, notification_type) ) diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index a1d5ee86..1a60443f 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,40 @@ use Kanboard\Core\Security; use PDO; use Kanboard\Model\Link; -const VERSION = 87; +const VERSION = 88; + +function version_88($pdo) +{ + $pdo->exec(" + CREATE TABLE user_has_metadata ( + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + value TEXT DEFAULT '', + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE project_has_metadata ( + project_id INTEGER NOT NULL, + name TEXT NOT NULL, + value TEXT DEFAULT '', + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_metadata ( + task_id INTEGER NOT NULL, + name TEXT NOT NULL, + value TEXT DEFAULT '', + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + UNIQUE(task_id, name) + ) + "); +} function version_87($pdo) { @@ -14,7 +47,7 @@ function version_87($pdo) CREATE TABLE project_has_notification_types ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, - notification_type TEXT, + notification_type TEXT NOT NULL, FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, UNIQUE(project_id, notification_type) ) diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index b969c289..e50a6297 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -41,6 +41,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectIntegration', 'ProjectPermission', 'ProjectNotification', + 'ProjectMetadata', 'Subtask', 'SubtaskExport', 'SubtaskTimeTracking', @@ -59,6 +60,7 @@ class ClassProvider implements ServiceProviderInterface 'TaskStatus', 'TaskValidator', 'TaskImport', + 'TaskMetadata', 'Transition', 'User', 'UserImport', @@ -67,6 +69,7 @@ class ClassProvider implements ServiceProviderInterface 'UserNotificationType', 'UserNotificationFilter', 'UserUnreadNotification', + 'UserMetadata', ), 'Formatter' => array( 'TaskFilterGanttFormatter', diff --git a/tests/units/Model/ConfigTest.php b/tests/units/Model/ConfigTest.php index 7670daac..17617ceb 100644 --- a/tests/units/Model/ConfigTest.php +++ b/tests/units/Model/ConfigTest.php @@ -7,6 +7,31 @@ use Kanboard\Core\Session; class ConfigTest extends Base { + public function testCRUDOperations() + { + $c = new Config($this->container); + + $this->assertTrue($c->save(array('key1' => 'value1'))); + $this->assertTrue($c->save(array('key1' => 'value2'))); + $this->assertTrue($c->save(array('key2' => 'value2'))); + + $this->assertEquals('value2', $c->getOption('key1')); + $this->assertEquals('value2', $c->getOption('key2')); + $this->assertEquals('', $c->getOption('key3')); + $this->assertEquals('default', $c->getOption('key3', 'default')); + + $this->assertTrue($c->exists('key1')); + $this->assertFalse($c->exists('key3')); + + $this->assertTrue($c->save(array('key1' => 'value1'))); + + $this->assertArrayHasKey('key1', $c->getAll()); + $this->assertArrayHasKey('key2', $c->getAll()); + + $this->assertContains('value1', $c->getAll()); + $this->assertContains('value2', $c->getAll()); + } + public function testSaveApplicationUrl() { $c = new Config($this->container); diff --git a/tests/units/Model/ProjectMetadataTest.php b/tests/units/Model/ProjectMetadataTest.php new file mode 100644 index 00000000..43d66b7b --- /dev/null +++ b/tests/units/Model/ProjectMetadataTest.php @@ -0,0 +1,47 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\Project; +use Kanboard\Model\ProjectMetadata; + +class ProjectMetadataTest extends Base +{ + public function testOperations() + { + $p = new Project($this->container); + $pm = new ProjectMetadata($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'project #1'))); + $this->assertEquals(2, $p->create(array('name' => 'project #2'))); + + $this->assertTrue($pm->save(1, array('key1' => 'value1'))); + $this->assertTrue($pm->save(1, array('key1' => 'value2'))); + $this->assertTrue($pm->save(2, array('key1' => 'value1'))); + $this->assertTrue($pm->save(2, array('key2' => 'value2'))); + + $this->assertEquals('value2', $pm->get(1, 'key1')); + $this->assertEquals('value1', $pm->get(2, 'key1')); + $this->assertEquals('', $pm->get(2, 'key3')); + $this->assertEquals('default', $pm->get(2, 'key3', 'default')); + + $this->assertTrue($pm->exists(2, 'key1')); + $this->assertFalse($pm->exists(2, 'key3')); + + $this->assertEquals(array('key1' => 'value2'), $pm->getAll(1)); + $this->assertEquals(array('key1' => 'value1', 'key2' => 'value2'), $pm->getAll(2)); + } + + public function testAutomaticRemove() + { + $p = new Project($this->container); + $pm = new ProjectMetadata($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'project #1'))); + $this->assertTrue($pm->save(1, array('key1' => 'value1'))); + + $this->assertTrue($pm->exists(1, 'key1')); + $this->assertTrue($p->remove(1)); + $this->assertFalse($pm->exists(1, 'key1')); + } +} diff --git a/tests/units/Model/TaskMetadataTest.php b/tests/units/Model/TaskMetadataTest.php new file mode 100644 index 00000000..9ce7d6be --- /dev/null +++ b/tests/units/Model/TaskMetadataTest.php @@ -0,0 +1,37 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\Project; +use Kanboard\Model\TaskCreation; +use Kanboard\Model\TaskMetadata; + +class TaskMetadataTest extends Base +{ + public function testOperations() + { + $p = new Project($this->container); + $tm = new TaskMetadata($this->container); + $tc = new TaskCreation($this->container); + + $this->assertEquals(1, $p->create(array('name' => 'project #1'))); + $this->assertEquals(1, $tc->create(array('title' => 'task #1', 'project_id' => 1))); + $this->assertEquals(2, $tc->create(array('title' => 'task #2', 'project_id' => 1))); + + $this->assertTrue($tm->save(1, array('key1' => 'value1'))); + $this->assertTrue($tm->save(1, array('key1' => 'value2'))); + $this->assertTrue($tm->save(2, array('key1' => 'value1'))); + $this->assertTrue($tm->save(2, array('key2' => 'value2'))); + + $this->assertEquals('value2', $tm->get(1, 'key1')); + $this->assertEquals('value1', $tm->get(2, 'key1')); + $this->assertEquals('', $tm->get(2, 'key3')); + $this->assertEquals('default', $tm->get(2, 'key3', 'default')); + + $this->assertTrue($tm->exists(2, 'key1')); + $this->assertFalse($tm->exists(2, 'key3')); + + $this->assertEquals(array('key1' => 'value2'), $tm->getAll(1)); + $this->assertEquals(array('key1' => 'value1', 'key2' => 'value2'), $tm->getAll(2)); + } +} diff --git a/tests/units/Model/UserMetadataTest.php b/tests/units/Model/UserMetadataTest.php new file mode 100644 index 00000000..cc1fff12 --- /dev/null +++ b/tests/units/Model/UserMetadataTest.php @@ -0,0 +1,33 @@ +<?php + +require_once __DIR__.'/../Base.php'; + +use Kanboard\Model\User; +use Kanboard\Model\UserMetadata; + +class UserMetadataTest extends Base +{ + public function testOperations() + { + $m = new UserMetadata($this->container); + $u = new User($this->container); + + $this->assertEquals(2, $u->create(array('username' => 'foobar'))); + + $this->assertTrue($m->save(1, array('key1' => 'value1'))); + $this->assertTrue($m->save(1, array('key1' => 'value2'))); + $this->assertTrue($m->save(2, array('key1' => 'value1'))); + $this->assertTrue($m->save(2, array('key2' => 'value2'))); + + $this->assertEquals('value2', $m->get(1, 'key1')); + $this->assertEquals('value1', $m->get(2, 'key1')); + $this->assertEquals('', $m->get(2, 'key3')); + $this->assertEquals('default', $m->get(2, 'key3', 'default')); + + $this->assertTrue($m->exists(2, 'key1')); + $this->assertFalse($m->exists(2, 'key3')); + + $this->assertEquals(array('key1' => 'value2'), $m->getAll(1)); + $this->assertEquals(array('key1' => 'value1', 'key2' => 'value2'), $m->getAll(2)); + } +} |