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

View file

@ -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

View file

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

View file

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

View file

@ -39,6 +39,15 @@
</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) { %>
<div class="form-row">
<label class="form-label" for="post-content">Content:</label>

View file

@ -94,6 +94,19 @@
</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)) { %>
<h1>Options</h1>

View file

@ -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,
];
}
}

View file

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

View file

@ -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();
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;
}

View file

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

View file

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

View file

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

View file

@ -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();
}
}
}

View file

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

View file

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

View file

@ -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';
}

View file

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

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\Upgrade06::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()));
}
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);