From 9283fb88d80cb355ff98364a9a57b657fc511d98 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 17 Oct 2015 15:27:43 -0400 Subject: Add metadata for users, tasks and projects --- ChangeLog | 1 + app/Model/Config.php | 68 ++++++--------------- app/Model/Metadata.php | 98 +++++++++++++++++++++++++++++++ app/Model/ProjectMetadata.php | 30 ++++++++++ app/Model/Setting.php | 96 ++++++++++++++++++++++++++++++ app/Model/TaskMetadata.php | 30 ++++++++++ app/Model/UserMetadata.php | 30 ++++++++++ app/Schema/Mysql.php | 37 +++++++++++- app/Schema/Postgres.php | 37 +++++++++++- app/Schema/Sqlite.php | 37 +++++++++++- app/ServiceProvider/ClassProvider.php | 3 + tests/units/Model/ConfigTest.php | 25 ++++++++ tests/units/Model/ProjectMetadataTest.php | 47 +++++++++++++++ tests/units/Model/TaskMetadataTest.php | 37 ++++++++++++ tests/units/Model/UserMetadataTest.php | 33 +++++++++++ 15 files changed, 553 insertions(+), 56 deletions(-) create mode 100644 app/Model/Metadata.php create mode 100644 app/Model/ProjectMetadata.php create mode 100644 app/Model/Setting.php create mode 100644 app/Model/TaskMetadata.php create mode 100644 app/Model/UserMetadata.php create mode 100644 tests/units/Model/ProjectMetadataTest.php create mode 100644 tests/units/Model/TaskMetadataTest.php create mode 100644 tests/units/Model/UserMetadataTest.php diff --git a/ChangeLog b/ChangeLog index 21066286..64a4b2c8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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,15 +12,8 @@ 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 * @@ -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 @@ -186,43 +178,6 @@ class Config extends Base return $default_value; } - /** - * 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 * @@ -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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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)); + } +} -- cgit v1.2.3