From 5dc85b7dee5c71d17cece7b23cff44bcdfc18b92 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Thu, 25 Sep 2014 23:53:47 +0200 Subject: [PATCH] Added post relations --- TODO | 2 - data/config.ini | 3 +- public_html/js/Auth.js | 1 + public_html/js/Presenters/PostPresenter.js | 5 ++ public_html/templates/post-edit.tpl | 9 +++ public_html/templates/post.tpl | 13 +++++ src/Controllers/PostController.php | 28 ++++++++-- .../ViewProxies/AbstractViewProxy.php | 8 +-- src/Controllers/ViewProxies/PostViewProxy.php | 17 +++++- src/Controllers/ViewProxies/TagViewProxy.php | 2 +- .../ViewProxies/TokenViewProxy.php | 2 +- src/Controllers/ViewProxies/UserViewProxy.php | 2 +- src/Dao/PostDao.php | 55 +++++++++++++++++++ src/Entities/Post.php | 11 ++++ src/FormData/PostEditFormData.php | 8 +++ src/Privilege.php | 1 + src/Services/PostService.php | 13 +++++ src/Upgrades/Upgrade08.php | 19 +++++++ src/di.php | 1 + tests/Dao/PostDaoTest.php | 31 +++++++++++ 20 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 src/Upgrades/Upgrade08.php diff --git a/TODO b/TODO index 32fb1635..84ce3738 100644 --- a/TODO +++ b/TODO @@ -6,11 +6,9 @@ everything related to posts: - fav - score (see notes about scoring) - editing - - relations - ability to loop video posts - post edit history (think http://konachan.com/history?search=post%3A188614) - - relations - previous and next post (difficult) - extract Pager from PagedCollectionPresenter - rename PagedCollectionPresenter to PagerPresenter diff --git a/data/config.ini b/data/config.ini index cd8e89a7..d83ef15f 100644 --- a/data/config.ini +++ b/data/config.ini @@ -48,7 +48,8 @@ changePostSafety = powerUser, moderator, administrator changePostSource = regularUser, powerUser, moderator, administrator changePostTags = regularUser, powerUser, moderator, administrator changePostContent = regularUser, powerUser, moderator, administrator -changePostThumbnail = regularUser, powerUser, moderator, administrator +changePostThumbnail = powerUser, moderator, administrator +changePostRelations = regularUser, powerUser, moderator, administrator listTags = anonymous, regularUser, powerUser, moderator, administrator diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js index 14e52219..330f60b1 100644 --- a/public_html/js/Auth.js +++ b/public_html/js/Auth.js @@ -30,6 +30,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) { changePostTags: 'changePostTags', changePostContent: 'changePostContent', changePostThumbnail: 'changePostThumbnail', + changePostRelations: 'changePostRelations', listTags: 'listTags', }; diff --git a/public_html/js/Presenters/PostPresenter.js b/public_html/js/Presenters/PostPresenter.js index d8561b07..19508eef 100644 --- a/public_html/js/Presenters/PostPresenter.js +++ b/public_html/js/Presenters/PostPresenter.js @@ -39,6 +39,7 @@ App.Presenters.PostPresenter = function( editPrivileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags); editPrivileges.canChangeContent = auth.hasPrivilege(auth.privileges.changePostContent); editPrivileges.canChangeThumbnail = auth.hasPrivilege(auth.privileges.changePostThumbnail); + editPrivileges.canChangeRelations = auth.hasPrivilege(auth.privileges.changePostRelations); promise.waitAll( util.promiseTemplate('post'), @@ -190,6 +191,10 @@ App.Presenters.PostPresenter = function( formData.tags = tagInput.getTags().join(' '); } + if (editPrivileges.canChangeRelations) { + formData.relations = $form.find('[name=relations]').val(); + } + if (post.tags.length === 0) { showEditError('No tags set.'); return; diff --git a/public_html/templates/post-edit.tpl b/public_html/templates/post-edit.tpl index 8fcfb365..d5baf4d5 100644 --- a/public_html/templates/post-edit.tpl +++ b/public_html/templates/post-edit.tpl @@ -39,6 +39,15 @@ <% } %> + <% if (privileges.canChangeRelations) { %> +
+ +
+ +
+
+ <% } %> + <% if (privileges.canChangeContent) { %>
diff --git a/public_html/templates/post.tpl b/public_html/templates/post.tpl index 6b98697e..71767368 100644 --- a/public_html/templates/post.tpl +++ b/public_html/templates/post.tpl @@ -94,6 +94,19 @@ + <% if (_.any(post.relations)) { %> +

Related posts

+ + <% } %> + <% if (_.any(privileges) || _.any(editPrivileges)) { %>

Options

diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 44b30df0..ba755ebc 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -35,20 +35,20 @@ final class PostController extends AbstractController public function getFeatured() { $post = $this->postService->getFeatured(); - return $this->postViewProxy->fromEntity($post); + return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); } public function getByNameOrId($postNameOrId) { $post = $this->postService->getByNameOrId($postNameOrId); - return $this->postViewProxy->fromEntity($post); + return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); } public function getFiltered() { $formData = new \Szurubooru\FormData\SearchFormData($this->inputReader); $searchResult = $this->postService->getFiltered($formData); - $entities = $this->postViewProxy->fromArray($searchResult->getEntities()); + $entities = $this->postViewProxy->fromArray($searchResult->getEntities(), $this->getLightFetchConfig()); return [ 'data' => $entities, 'pageSize' => $searchResult->getPageSize(), @@ -66,7 +66,7 @@ final class PostController extends AbstractController $this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS_ANONYMOUSLY); $post = $this->postService->createPost($formData); - return $this->postViewProxy->fromEntity($post); + return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); } public function updatePost($postNameOrId) @@ -91,7 +91,7 @@ final class PostController extends AbstractController $this->postService->updatePost($post, $formData); $post = $this->postService->getByNameOrId($postNameOrId); - return $this->postViewProxy->fromEntity($post); + return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); } public function deletePost($postNameOrId) @@ -105,4 +105,22 @@ final class PostController extends AbstractController $post = $this->postService->getByNameOrId($postNameOrId); $this->postService->featurePost($post); } + + private function getFullFetchConfig() + { + return + [ + \Szurubooru\Controllers\ViewProxies\PostViewProxy::FETCH_RELATIONS => true, + \Szurubooru\Controllers\ViewProxies\PostViewProxy::FETCH_TAGS => true, + \Szurubooru\Controllers\ViewProxies\PostViewProxy::FETCH_USER => true, + ]; + } + + private function getLightFetchConfig() + { + return + [ + \Szurubooru\Controllers\ViewProxies\PostViewProxy::FETCH_TAGS => true, + ]; + } } diff --git a/src/Controllers/ViewProxies/AbstractViewProxy.php b/src/Controllers/ViewProxies/AbstractViewProxy.php index 4fa93f7c..b59e6a2f 100644 --- a/src/Controllers/ViewProxies/AbstractViewProxy.php +++ b/src/Controllers/ViewProxies/AbstractViewProxy.php @@ -3,14 +3,14 @@ namespace Szurubooru\Controllers\ViewProxies; abstract class AbstractViewProxy { - public abstract function fromEntity($entity); + public abstract function fromEntity($entity, $config = []); - public function fromArray($entities) + public function fromArray($entities, $config = []) { return array_values(array_map( - function($entity) + function($entity) use ($config) { - return static::fromEntity($entity); + return static::fromEntity($entity, $config); }, $entities)); } diff --git a/src/Controllers/ViewProxies/PostViewProxy.php b/src/Controllers/ViewProxies/PostViewProxy.php index 24c68825..1123d934 100644 --- a/src/Controllers/ViewProxies/PostViewProxy.php +++ b/src/Controllers/ViewProxies/PostViewProxy.php @@ -3,6 +3,10 @@ namespace Szurubooru\Controllers\ViewProxies; class PostViewProxy extends AbstractViewProxy { + const FETCH_USER = 'fetchUser'; + const FETCH_TAGS = 'fetchTags'; + const FETCH_RELATIONS = 'fetchRelations'; + private $tagViewProxy; private $userViewProxy; @@ -14,7 +18,7 @@ class PostViewProxy extends AbstractViewProxy $this->userViewProxy = $userViewProxy; } - public function fromEntity($post) + public function fromEntity($post, $config = []) { $result = new \StdClass; if ($post) @@ -34,9 +38,16 @@ class PostViewProxy extends AbstractViewProxy $result->imageHeight = $post->getImageHeight(); $result->featureCount = $post->getFeatureCount(); $result->lastFeatureTime = $post->getLastFeatureTime(); - $result->tags = $this->tagViewProxy->fromArray($post->getTags()); $result->originalFileSize = $post->getOriginalFileSize(); - $result->user = $this->userViewProxy->fromEntity($post->getUser()); + + if (!empty($config[self::FETCH_TAGS])) + $result->tags = $this->tagViewProxy->fromArray($post->getTags()); + + if (!empty($config[self::FETCH_USER])) + $result->user = $this->userViewProxy->fromEntity($post->getUser()); + + if (!empty($config[self::FETCH_RELATIONS])) + $result->relations = $this->fromArray($post->getRelatedPosts()); } return $result; } diff --git a/src/Controllers/ViewProxies/TagViewProxy.php b/src/Controllers/ViewProxies/TagViewProxy.php index dfcb417b..6a4ad7d4 100644 --- a/src/Controllers/ViewProxies/TagViewProxy.php +++ b/src/Controllers/ViewProxies/TagViewProxy.php @@ -3,7 +3,7 @@ namespace Szurubooru\Controllers\ViewProxies; class TagViewProxy extends AbstractViewProxy { - public function fromEntity($tag) + public function fromEntity($tag, $config = []) { $result = new \StdClass; if ($tag) diff --git a/src/Controllers/ViewProxies/TokenViewProxy.php b/src/Controllers/ViewProxies/TokenViewProxy.php index c2a3e828..71d76023 100644 --- a/src/Controllers/ViewProxies/TokenViewProxy.php +++ b/src/Controllers/ViewProxies/TokenViewProxy.php @@ -3,7 +3,7 @@ namespace Szurubooru\Controllers\ViewProxies; class TokenViewProxy extends AbstractViewProxy { - public function fromEntity($token) + public function fromEntity($token, $config = []) { $result = new \StdClass; if ($token) diff --git a/src/Controllers/ViewProxies/UserViewProxy.php b/src/Controllers/ViewProxies/UserViewProxy.php index ce3445a3..8fd0d2b4 100644 --- a/src/Controllers/ViewProxies/UserViewProxy.php +++ b/src/Controllers/ViewProxies/UserViewProxy.php @@ -10,7 +10,7 @@ class UserViewProxy extends AbstractViewProxy $this->privilegeService = $privilegeService; } - public function fromEntity($user) + public function fromEntity($user, $config = []) { $result = new \StdClass; if ($user) diff --git a/src/Dao/PostDao.php b/src/Dao/PostDao.php index c9622c8a..32e6f174 100644 --- a/src/Dao/PostDao.php +++ b/src/Dao/PostDao.php @@ -76,6 +76,13 @@ class PostDao extends AbstractDao implements ICrudDao { return $this->getTags($post); }); + + $post->setLazyLoader( + \Szurubooru\Entities\Post::LAZY_LOADER_RELATED_POSTS, + function(\Szurubooru\Entities\Post $post) + { + return $this->getRelatedPosts($post); + }); } protected function afterSave(\Szurubooru\Entities\Entity $post) @@ -83,6 +90,7 @@ class PostDao extends AbstractDao implements ICrudDao $this->syncContent($post); $this->syncThumbnailSourceContent($post); $this->syncTags($post); + $this->syncPostRelations($post); } private function getTags(\Szurubooru\Entities\Post $post) @@ -95,6 +103,28 @@ class PostDao extends AbstractDao implements ICrudDao return $this->userDao->findById($post->getUserId()); } + private function getRelatedPosts(\Szurubooru\Entities\Post $post) + { + $query = $this->fpdo + ->from('postRelations') + ->where('post1id = :post1id OR post2id = :post2id', [ + ':post1id' => $post->getId(), + ':post2id' => $post->getId()]); + + $relatedPostIds = []; + foreach ($query as $arrayEntity) + { + $post1id = intval($arrayEntity['post1id']); + $post2id = intval($arrayEntity['post2id']); + if ($post1id !== $post->getId()) + $relatedPostIds[] = $post1id; + if ($post2id !== $post->getId()) + $relatedPostIds[] = $post2id; + } + + return $this->findByIds($relatedPostIds); + } + private function syncContent(\Szurubooru\Entities\Post $post) { $targetPath = $post->getContentPath(); @@ -154,4 +184,29 @@ class PostDao extends AbstractDao implements ICrudDao $this->fpdo->deleteFrom('postTags')->where('postId', $post->getId())->where('tagId', $tagId)->execute(); } } + + private function syncPostRelations(\Szurubooru\Entities\Post $post) + { + $this->fpdo->deleteFrom('postRelations')->where('post1id', $post->getId())->execute(); + $this->fpdo->deleteFrom('postRelations')->where('post2id', $post->getId())->execute(); + + $relatedPostIds = array_filter(array_unique(array_map( + function ($post) + { + if (!$post->getId()) + throw new \RuntimeException('Unsaved entities found'); + return $post->getId(); + }, + $post->getRelatedPosts()))); + + foreach ($relatedPostIds as $postId) + { + $this->fpdo + ->insertInto('postRelations') + ->values([ + 'post1id' => $post->getId(), + 'post2id' => $postId]) + ->execute(); + } + } } diff --git a/src/Entities/Post.php b/src/Entities/Post.php index 9c2bb978..7e396040 100644 --- a/src/Entities/Post.php +++ b/src/Entities/Post.php @@ -16,6 +16,7 @@ final class Post extends Entity const LAZY_LOADER_TAGS = 'tags'; const LAZY_LOADER_CONTENT = 'content'; const LAZY_LOADER_THUMBNAIL_SOURCE_CONTENT = 'thumbnailSourceContent'; + const LAZY_LOADER_RELATED_POSTS = 'relatedPosts'; const META_TAG_COUNT = 'tagCount'; @@ -201,6 +202,16 @@ final class Post extends Entity $this->setMeta(self::META_TAG_COUNT, count($tags)); } + public function getRelatedPosts() + { + return $this->lazyLoad(self::LAZY_LOADER_RELATED_POSTS, []); + } + + public function setRelatedPosts(array $relatedPosts) + { + $this->lazySave(self::LAZY_LOADER_RELATED_POSTS, $relatedPosts); + } + public function getUser() { return $this->lazyLoad(self::LAZY_LOADER_USER, null); diff --git a/src/FormData/PostEditFormData.php b/src/FormData/PostEditFormData.php index 98f2d4c7..756b910f 100644 --- a/src/FormData/PostEditFormData.php +++ b/src/FormData/PostEditFormData.php @@ -8,6 +8,7 @@ class PostEditFormData implements \Szurubooru\IValidatable public $safety; public $source; public $tags; + public $relations; public $seenEditTime; @@ -20,6 +21,7 @@ class PostEditFormData implements \Szurubooru\IValidatable $this->safety = \Szurubooru\Helpers\EnumHelper::postSafetyFromString($inputReader->safety); $this->source = $inputReader->source; $this->tags = preg_split('/[\s+]/', $inputReader->tags); + $this->relations = array_filter(preg_split('/[\s+]/', $inputReader->relations)); $this->seenEditTime = $inputReader->seenEditTime; } } @@ -30,5 +32,11 @@ class PostEditFormData implements \Szurubooru\IValidatable if ($this->source !== null) $validator->validatePostSource($this->source); + + if ($this->relations) + { + foreach ($this->relations as $relatedPostId) + $validator->validateNumber($relatedPostId); + } } } diff --git a/src/Privilege.php b/src/Privilege.php index 7116ffec..804e7e34 100644 --- a/src/Privilege.php +++ b/src/Privilege.php @@ -30,6 +30,7 @@ class Privilege const CHANGE_POST_TAGS = 'changePostTags'; const CHANGE_POST_CONTENT = 'changePostContent'; const CHANGE_POST_THUMBNAIL = 'changePostThumbnail'; + const CHANGE_POST_RELATIONS = 'changePostRelations'; const LIST_TAGS = 'listTags'; } diff --git a/src/Services/PostService.php b/src/Services/PostService.php index 491b593f..522169b6 100644 --- a/src/Services/PostService.php +++ b/src/Services/PostService.php @@ -136,6 +136,9 @@ class PostService if ($formData->tags !== null) $this->updatePostTags($post, $formData->tags); + if ($formData->relations !== null) + $this->updatePostRelations($post, $formData->relations); + return $this->postDao->save($post); }; return $this->transactionManager->commit($transactionFunc); @@ -244,6 +247,16 @@ class PostService $post->setTags($tags); } + private function updatePostRelations(\Szurubooru\Entities\Post $post, array $newRelatedPostIds) + { + $relatedPosts = $this->postDao->findByIds($newRelatedPostIds); + foreach ($newRelatedPostIds as $postId) + if (!isset($relatedPosts[$postId])) + throw new \DomainException('Post with id "' . $postId . '" was not found.'); + + $post->setRelatedPosts($relatedPosts); + } + public function deletePost(\Szurubooru\Entities\Post $post) { $transactionFunc = function() use ($post) diff --git a/src/Upgrades/Upgrade08.php b/src/Upgrades/Upgrade08.php new file mode 100644 index 00000000..a26c2bf2 --- /dev/null +++ b/src/Upgrades/Upgrade08.php @@ -0,0 +1,19 @@ +getPDO(); + + $pdo->exec('CREATE TABLE postRelations + ( + id INTEGER PRIMARY KEY NOT NULL, + post1id INTEGER NOT NULL, + post2id INTEGER NOT NULL, + UNIQUE (post1id, post2id) + )'); + } +} + diff --git a/src/di.php b/src/di.php index 3cb17fe2..4d7a369e 100644 --- a/src/di.php +++ b/src/di.php @@ -23,6 +23,7 @@ return [ $container->get(\Szurubooru\Upgrades\Upgrade05::class), $container->get(\Szurubooru\Upgrades\Upgrade06::class), $container->get(\Szurubooru\Upgrades\Upgrade07::class), + $container->get(\Szurubooru\Upgrades\Upgrade08::class), ]; }), diff --git a/tests/Dao/PostDaoTest.php b/tests/Dao/PostDaoTest.php index 5d9e66ac..95226395 100644 --- a/tests/Dao/PostDaoTest.php +++ b/tests/Dao/PostDaoTest.php @@ -159,6 +159,37 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase $this->assertEquals(2, count($tagDao->findAll())); } + public function testSavingUnsavedRelations() + { + $post1 = $this->getPost(); + $post2 = $this->getPost(); + $testPosts = [$post1, $post2]; + + $postDao = $this->getPostDao(); + $post = $this->getPost(); + $post->setRelatedPosts($testPosts); + $this->setExpectedException(\Exception::class, 'Unsaved entities found'); + $postDao->save($post); + } + + public function testSavingRelations() + { + $post1 = $this->getPost(); + $post2 = $this->getPost(); + $testPosts = [$post1, $post2]; + + $postDao = $this->getPostDao(); + $postDao->save($post1); + $postDao->save($post2); + $post = $this->getPost(); + $post->setRelatedPosts($testPosts); + $postDao->save($post); + + $savedPost = $postDao->findById($post->getId()); + $this->assertEntitiesEqual($testPosts, $post->getRelatedPosts()); + $this->assertEquals(2, count($savedPost->getRelatedPosts())); + } + public function testSavingUser() { $testUser = new \Szurubooru\Entities\User(5);