Added post relations

This commit is contained in:
Marcin Kurczewski 2014-09-25 23:53:47 +02:00
parent 22b30c3e43
commit 5dc85b7dee
20 changed files with 213 additions and 18 deletions

2
TODO
View file

@ -6,11 +6,9 @@ everything related to posts:
- fav - fav
- score (see notes about scoring) - score (see notes about scoring)
- editing - editing
- relations
- ability to loop video posts - ability to loop video posts
- post edit history (think - post edit history (think
http://konachan.com/history?search=post%3A188614) http://konachan.com/history?search=post%3A188614)
- relations
- previous and next post (difficult) - previous and next post (difficult)
- extract Pager from PagedCollectionPresenter - extract Pager from PagedCollectionPresenter
- rename PagedCollectionPresenter to PagerPresenter - rename PagedCollectionPresenter to PagerPresenter

View file

@ -48,7 +48,8 @@ changePostSafety = powerUser, moderator, administrator
changePostSource = regularUser, powerUser, moderator, administrator changePostSource = regularUser, powerUser, moderator, administrator
changePostTags = regularUser, powerUser, moderator, administrator changePostTags = regularUser, powerUser, moderator, administrator
changePostContent = 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 listTags = anonymous, regularUser, powerUser, moderator, administrator

View file

@ -30,6 +30,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
changePostTags: 'changePostTags', changePostTags: 'changePostTags',
changePostContent: 'changePostContent', changePostContent: 'changePostContent',
changePostThumbnail: 'changePostThumbnail', changePostThumbnail: 'changePostThumbnail',
changePostRelations: 'changePostRelations',
listTags: 'listTags', listTags: 'listTags',
}; };

View file

@ -39,6 +39,7 @@ App.Presenters.PostPresenter = function(
editPrivileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags); editPrivileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags);
editPrivileges.canChangeContent = auth.hasPrivilege(auth.privileges.changePostContent); editPrivileges.canChangeContent = auth.hasPrivilege(auth.privileges.changePostContent);
editPrivileges.canChangeThumbnail = auth.hasPrivilege(auth.privileges.changePostThumbnail); editPrivileges.canChangeThumbnail = auth.hasPrivilege(auth.privileges.changePostThumbnail);
editPrivileges.canChangeRelations = auth.hasPrivilege(auth.privileges.changePostRelations);
promise.waitAll( promise.waitAll(
util.promiseTemplate('post'), util.promiseTemplate('post'),
@ -190,6 +191,10 @@ App.Presenters.PostPresenter = function(
formData.tags = tagInput.getTags().join(' '); formData.tags = tagInput.getTags().join(' ');
} }
if (editPrivileges.canChangeRelations) {
formData.relations = $form.find('[name=relations]').val();
}
if (post.tags.length === 0) { if (post.tags.length === 0) {
showEditError('No tags set.'); showEditError('No tags set.');
return; return;

View file

@ -39,6 +39,15 @@
</div> </div>
<% } %> <% } %>
<% if (privileges.canChangeRelations) { %>
<div class="form-row">
<label class="form-label" for="post-relations">Relations:</label>
<div class="form-input">
<input maxlength="200" type="text" name="relations" id="post-relations" placeholder="Post ids, separated with space" value="<%= _.pluck(post.relations, 'id').join(' ') %>"/>
</div>
</div>
<% } %>
<% if (privileges.canChangeContent) { %> <% if (privileges.canChangeContent) { %>
<div class="form-row"> <div class="form-row">
<label class="form-label" for="post-content">Content:</label> <label class="form-label" for="post-content">Content:</label>

View file

@ -94,6 +94,19 @@
</ul> </ul>
<% if (_.any(post.relations)) { %>
<h1>Related posts</h1>
<ul class="related">
<% _.each(post.relations, function(relatedPost) { %>
<li>
<a href="#/post/<%= relatedPost.id %>">
<%= relatedPost.idMarkdown %>
</a>
</li>
<% }) %>
</ul>
<% } %>
<% if (_.any(privileges) || _.any(editPrivileges)) { %> <% if (_.any(privileges) || _.any(editPrivileges)) { %>
<h1>Options</h1> <h1>Options</h1>

View file

@ -35,20 +35,20 @@ final class PostController extends AbstractController
public function getFeatured() public function getFeatured()
{ {
$post = $this->postService->getFeatured(); $post = $this->postService->getFeatured();
return $this->postViewProxy->fromEntity($post); return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
} }
public function getByNameOrId($postNameOrId) public function getByNameOrId($postNameOrId)
{ {
$post = $this->postService->getByNameOrId($postNameOrId); $post = $this->postService->getByNameOrId($postNameOrId);
return $this->postViewProxy->fromEntity($post); return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
} }
public function getFiltered() public function getFiltered()
{ {
$formData = new \Szurubooru\FormData\SearchFormData($this->inputReader); $formData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
$searchResult = $this->postService->getFiltered($formData); $searchResult = $this->postService->getFiltered($formData);
$entities = $this->postViewProxy->fromArray($searchResult->getEntities()); $entities = $this->postViewProxy->fromArray($searchResult->getEntities(), $this->getLightFetchConfig());
return [ return [
'data' => $entities, 'data' => $entities,
'pageSize' => $searchResult->getPageSize(), 'pageSize' => $searchResult->getPageSize(),
@ -66,7 +66,7 @@ final class PostController extends AbstractController
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS_ANONYMOUSLY); $this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS_ANONYMOUSLY);
$post = $this->postService->createPost($formData); $post = $this->postService->createPost($formData);
return $this->postViewProxy->fromEntity($post); return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
} }
public function updatePost($postNameOrId) public function updatePost($postNameOrId)
@ -91,7 +91,7 @@ final class PostController extends AbstractController
$this->postService->updatePost($post, $formData); $this->postService->updatePost($post, $formData);
$post = $this->postService->getByNameOrId($postNameOrId); $post = $this->postService->getByNameOrId($postNameOrId);
return $this->postViewProxy->fromEntity($post); return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
} }
public function deletePost($postNameOrId) public function deletePost($postNameOrId)
@ -105,4 +105,22 @@ final class PostController extends AbstractController
$post = $this->postService->getByNameOrId($postNameOrId); $post = $this->postService->getByNameOrId($postNameOrId);
$this->postService->featurePost($post); $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,
];
}
} }

View file

@ -3,14 +3,14 @@ namespace Szurubooru\Controllers\ViewProxies;
abstract class AbstractViewProxy 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( return array_values(array_map(
function($entity) function($entity) use ($config)
{ {
return static::fromEntity($entity); return static::fromEntity($entity, $config);
}, },
$entities)); $entities));
} }

View file

@ -3,6 +3,10 @@ namespace Szurubooru\Controllers\ViewProxies;
class PostViewProxy extends AbstractViewProxy class PostViewProxy extends AbstractViewProxy
{ {
const FETCH_USER = 'fetchUser';
const FETCH_TAGS = 'fetchTags';
const FETCH_RELATIONS = 'fetchRelations';
private $tagViewProxy; private $tagViewProxy;
private $userViewProxy; private $userViewProxy;
@ -14,7 +18,7 @@ class PostViewProxy extends AbstractViewProxy
$this->userViewProxy = $userViewProxy; $this->userViewProxy = $userViewProxy;
} }
public function fromEntity($post) public function fromEntity($post, $config = [])
{ {
$result = new \StdClass; $result = new \StdClass;
if ($post) if ($post)
@ -34,9 +38,16 @@ class PostViewProxy extends AbstractViewProxy
$result->imageHeight = $post->getImageHeight(); $result->imageHeight = $post->getImageHeight();
$result->featureCount = $post->getFeatureCount(); $result->featureCount = $post->getFeatureCount();
$result->lastFeatureTime = $post->getLastFeatureTime(); $result->lastFeatureTime = $post->getLastFeatureTime();
$result->tags = $this->tagViewProxy->fromArray($post->getTags());
$result->originalFileSize = $post->getOriginalFileSize(); $result->originalFileSize = $post->getOriginalFileSize();
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()); $result->user = $this->userViewProxy->fromEntity($post->getUser());
if (!empty($config[self::FETCH_RELATIONS]))
$result->relations = $this->fromArray($post->getRelatedPosts());
} }
return $result; return $result;
} }

View file

@ -3,7 +3,7 @@ namespace Szurubooru\Controllers\ViewProxies;
class TagViewProxy extends AbstractViewProxy class TagViewProxy extends AbstractViewProxy
{ {
public function fromEntity($tag) public function fromEntity($tag, $config = [])
{ {
$result = new \StdClass; $result = new \StdClass;
if ($tag) if ($tag)

View file

@ -3,7 +3,7 @@ namespace Szurubooru\Controllers\ViewProxies;
class TokenViewProxy extends AbstractViewProxy class TokenViewProxy extends AbstractViewProxy
{ {
public function fromEntity($token) public function fromEntity($token, $config = [])
{ {
$result = new \StdClass; $result = new \StdClass;
if ($token) if ($token)

View file

@ -10,7 +10,7 @@ class UserViewProxy extends AbstractViewProxy
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
} }
public function fromEntity($user) public function fromEntity($user, $config = [])
{ {
$result = new \StdClass; $result = new \StdClass;
if ($user) if ($user)

View file

@ -76,6 +76,13 @@ class PostDao extends AbstractDao implements ICrudDao
{ {
return $this->getTags($post); 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) protected function afterSave(\Szurubooru\Entities\Entity $post)
@ -83,6 +90,7 @@ class PostDao extends AbstractDao implements ICrudDao
$this->syncContent($post); $this->syncContent($post);
$this->syncThumbnailSourceContent($post); $this->syncThumbnailSourceContent($post);
$this->syncTags($post); $this->syncTags($post);
$this->syncPostRelations($post);
} }
private function getTags(\Szurubooru\Entities\Post $post) private function getTags(\Szurubooru\Entities\Post $post)
@ -95,6 +103,28 @@ class PostDao extends AbstractDao implements ICrudDao
return $this->userDao->findById($post->getUserId()); 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) private function syncContent(\Szurubooru\Entities\Post $post)
{ {
$targetPath = $post->getContentPath(); $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(); $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();
}
}
} }

View file

@ -16,6 +16,7 @@ final class Post extends Entity
const LAZY_LOADER_TAGS = 'tags'; const LAZY_LOADER_TAGS = 'tags';
const LAZY_LOADER_CONTENT = 'content'; const LAZY_LOADER_CONTENT = 'content';
const LAZY_LOADER_THUMBNAIL_SOURCE_CONTENT = 'thumbnailSourceContent'; const LAZY_LOADER_THUMBNAIL_SOURCE_CONTENT = 'thumbnailSourceContent';
const LAZY_LOADER_RELATED_POSTS = 'relatedPosts';
const META_TAG_COUNT = 'tagCount'; const META_TAG_COUNT = 'tagCount';
@ -201,6 +202,16 @@ final class Post extends Entity
$this->setMeta(self::META_TAG_COUNT, count($tags)); $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() public function getUser()
{ {
return $this->lazyLoad(self::LAZY_LOADER_USER, null); return $this->lazyLoad(self::LAZY_LOADER_USER, null);

View file

@ -8,6 +8,7 @@ class PostEditFormData implements \Szurubooru\IValidatable
public $safety; public $safety;
public $source; public $source;
public $tags; public $tags;
public $relations;
public $seenEditTime; public $seenEditTime;
@ -20,6 +21,7 @@ class PostEditFormData implements \Szurubooru\IValidatable
$this->safety = \Szurubooru\Helpers\EnumHelper::postSafetyFromString($inputReader->safety); $this->safety = \Szurubooru\Helpers\EnumHelper::postSafetyFromString($inputReader->safety);
$this->source = $inputReader->source; $this->source = $inputReader->source;
$this->tags = preg_split('/[\s+]/', $inputReader->tags); $this->tags = preg_split('/[\s+]/', $inputReader->tags);
$this->relations = array_filter(preg_split('/[\s+]/', $inputReader->relations));
$this->seenEditTime = $inputReader->seenEditTime; $this->seenEditTime = $inputReader->seenEditTime;
} }
} }
@ -30,5 +32,11 @@ class PostEditFormData implements \Szurubooru\IValidatable
if ($this->source !== null) if ($this->source !== null)
$validator->validatePostSource($this->source); $validator->validatePostSource($this->source);
if ($this->relations)
{
foreach ($this->relations as $relatedPostId)
$validator->validateNumber($relatedPostId);
}
} }
} }

View file

@ -30,6 +30,7 @@ class Privilege
const CHANGE_POST_TAGS = 'changePostTags'; const CHANGE_POST_TAGS = 'changePostTags';
const CHANGE_POST_CONTENT = 'changePostContent'; const CHANGE_POST_CONTENT = 'changePostContent';
const CHANGE_POST_THUMBNAIL = 'changePostThumbnail'; const CHANGE_POST_THUMBNAIL = 'changePostThumbnail';
const CHANGE_POST_RELATIONS = 'changePostRelations';
const LIST_TAGS = 'listTags'; const LIST_TAGS = 'listTags';
} }

View file

@ -136,6 +136,9 @@ class PostService
if ($formData->tags !== null) if ($formData->tags !== null)
$this->updatePostTags($post, $formData->tags); $this->updatePostTags($post, $formData->tags);
if ($formData->relations !== null)
$this->updatePostRelations($post, $formData->relations);
return $this->postDao->save($post); return $this->postDao->save($post);
}; };
return $this->transactionManager->commit($transactionFunc); return $this->transactionManager->commit($transactionFunc);
@ -244,6 +247,16 @@ class PostService
$post->setTags($tags); $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) public function deletePost(\Szurubooru\Entities\Post $post)
{ {
$transactionFunc = function() use ($post) $transactionFunc = function() use ($post)

View file

@ -0,0 +1,19 @@
<?php
namespace Szurubooru\Upgrades;
class Upgrade08 implements IUpgrade
{
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
{
$pdo = $databaseConnection->getPDO();
$pdo->exec('CREATE TABLE postRelations
(
id INTEGER PRIMARY KEY NOT NULL,
post1id INTEGER NOT NULL,
post2id INTEGER NOT NULL,
UNIQUE (post1id, post2id)
)');
}
}

View file

@ -23,6 +23,7 @@ return [
$container->get(\Szurubooru\Upgrades\Upgrade05::class), $container->get(\Szurubooru\Upgrades\Upgrade05::class),
$container->get(\Szurubooru\Upgrades\Upgrade06::class), $container->get(\Szurubooru\Upgrades\Upgrade06::class),
$container->get(\Szurubooru\Upgrades\Upgrade07::class), $container->get(\Szurubooru\Upgrades\Upgrade07::class),
$container->get(\Szurubooru\Upgrades\Upgrade08::class),
]; ];
}), }),

View file

@ -159,6 +159,37 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
$this->assertEquals(2, count($tagDao->findAll())); $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() public function testSavingUser()
{ {
$testUser = new \Szurubooru\Entities\User(5); $testUser = new \Szurubooru\Entities\User(5);