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);