Added post history

This commit is contained in:
Marcin Kurczewski 2014-09-26 20:41:28 +02:00
parent df939e0343
commit 9edc74f9a5
29 changed files with 995 additions and 27 deletions

2
TODO
View file

@ -7,8 +7,6 @@ everything related to posts:
- score - score
- editing - editing
- ability to loop video posts - ability to loop video posts
- post edit history (think
http://konachan.com/history?search=post%3A188614)
- 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

@ -53,6 +53,8 @@ changePostRelations = regularUser, powerUser, moderator, administrator
listTags = anonymous, regularUser, powerUser, moderator, administrator listTags = anonymous, regularUser, powerUser, moderator, administrator
viewHistory = anonymous, regularUser, powerUser, moderator, administrator
[users] [users]
minUserNameLength = 1 minUserNameLength = 1
maxUserNameLength = 32 maxUserNameLength = 32

View file

@ -0,0 +1,50 @@
table.history {
font-size: 80%;
}
table.history .addition {
color: forestgreen;
}
table.history .addition:before {
content: '+';
}
table.history .removal {
color: red;
}
table.history .removal:before {
content: '-';
}
table.history .time,
table.history .user,
table.history .subject {
white-space: nowrap;
}
table.history .user img {
vertical-align: middle;
}
table.history td {
padding: 0.1em 0.25em;
}
table.history ul {
margin: 0;
padding: 0;
list-style-type: none;
display: inline;
}
table.history ul:before {
content: ' (';
}
table.history ul:after {
content: ')';
}
table.history li {
display: inline;
}
table.history li:not(:last-of-type):after {
content: ', ';
}

View file

@ -124,3 +124,8 @@
#post-view-wrapper .post-edit-wrapper .file-handler { #post-view-wrapper .post-edit-wrapper .file-handler {
margin: 0.5em 0; margin: 0.5em 0;
} }
#post-view-wrapper .post-history-wrapper {
padding: 1em;
display: none;
}

View file

@ -32,6 +32,7 @@
<link rel="stylesheet" type="text/css" href="/css/post-list.css"/> <link rel="stylesheet" type="text/css" href="/css/post-list.css"/>
<link rel="stylesheet" type="text/css" href="/css/post.css"/> <link rel="stylesheet" type="text/css" href="/css/post.css"/>
<link rel="stylesheet" type="text/css" href="/css/home.css"/> <link rel="stylesheet" type="text/css" href="/css/home.css"/>
<link rel="stylesheet" type="text/css" href="/css/history.css"/>
<!-- /build --> <!-- /build -->
</head> </head>

View file

@ -33,6 +33,8 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
changePostRelations: 'changePostRelations', changePostRelations: 'changePostRelations',
listTags: 'listTags', listTags: 'listTags',
viewHistory: 'viewHistory',
}; };
function loginFromCredentials(userNameOrEmail, password, remember) { function loginFromCredentials(userNameOrEmail, password, remember) {

View file

@ -15,13 +15,19 @@ App.Presenters.PostPresenter = function(
var $el = jQuery('#content'); var $el = jQuery('#content');
var $messages = $el; var $messages = $el;
var postTemplate; var postTemplate;
var postEditTemplate; var postEditTemplate;
var postContentTemplate; var postContentTemplate;
var historyTemplate;
var post; var post;
var postHistory;
var postNameOrId; var postNameOrId;
var privileges = {}; var privileges = {};
var editPrivileges = {}; var editPrivileges = {};
var tagInput; var tagInput;
var postContentFileDropper; var postContentFileDropper;
var postThumbnailFileDropper; var postThumbnailFileDropper;
@ -33,6 +39,7 @@ App.Presenters.PostPresenter = function(
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts); privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts); privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts);
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
editPrivileges.canChangeSafety = auth.hasPrivilege(auth.privileges.changePostSafety); editPrivileges.canChangeSafety = auth.hasPrivilege(auth.privileges.changePostSafety);
editPrivileges.canChangeSource = auth.hasPrivilege(auth.privileges.changePostSource); editPrivileges.canChangeSource = auth.hasPrivilege(auth.privileges.changePostSource);
editPrivileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags); editPrivileges.canChangeTags = auth.hasPrivilege(auth.privileges.changePostTags);
@ -43,15 +50,17 @@ App.Presenters.PostPresenter = function(
promise.waitAll( promise.waitAll(
util.promiseTemplate('post'), util.promiseTemplate('post'),
util.promiseTemplate('post-edit'), util.promiseTemplate('post-edit'),
util.promiseTemplate('post-content')) util.promiseTemplate('post-content'),
util.promiseTemplate('history'))
.then(function( .then(function(
postTemplateHtml, postTemplateHtml,
postEditTemplateHtml, postEditTemplateHtml,
postContentTemplateHtml, postContentTemplateHtml,
response) { historyTemplateHtml) {
postTemplate = _.template(postTemplateHtml); postTemplate = _.template(postTemplateHtml);
postEditTemplate = _.template(postEditTemplateHtml); postEditTemplate = _.template(postEditTemplateHtml);
postContentTemplate = _.template(postContentTemplateHtml); postContentTemplate = _.template(postContentTemplateHtml);
historyTemplate = _.template(historyTemplateHtml);
reinit(args, loaded); reinit(args, loaded);
}).fail(showGenericError); }).fail(showGenericError);
@ -60,9 +69,14 @@ App.Presenters.PostPresenter = function(
function reinit(args, loaded) { function reinit(args, loaded) {
postNameOrId = args.postNameOrId; postNameOrId = args.postNameOrId;
promise.wait(api.get('/posts/' + postNameOrId)) promise.waitAll(
.then(function(response) { api.get('/posts/' + postNameOrId),
post = response.json; privileges.canViewHistory ?
api.get('/posts/' + postNameOrId + '/history') :
null)
.then(function(postResponse, postHistoryResponse) {
post = postResponse.json;
postHistory = postHistoryResponse && postHistoryResponse.json && postHistoryResponse.json.data;
topNavigationPresenter.changeTitle('@' + post.id); topNavigationPresenter.changeTitle('@' + post.id);
render(); render();
loaded(); loaded();
@ -92,9 +106,10 @@ App.Presenters.PostPresenter = function(
} }
$el.find('.post-edit-wrapper form').submit(editFormSubmitted); $el.find('.post-edit-wrapper form').submit(editFormSubmitted);
$el.find('.delete').click(deleteButtonClicked); $el.find('#sidebar .delete').click(deleteButtonClicked);
$el.find('.feature').click(featureButtonClicked); $el.find('#sidebar .feature').click(featureButtonClicked);
$el.find('.edit').click(editButtonClicked); $el.find('#sidebar .edit').click(editButtonClicked);
$el.find('#sidebar .history').click(historyButtonClicked);
} }
function renderSidebar() { function renderSidebar() {
@ -104,10 +119,12 @@ App.Presenters.PostPresenter = function(
function renderPostTemplate() { function renderPostTemplate() {
return postTemplate({ return postTemplate({
post: post, post: post,
postHistory: postHistory,
formatRelativeTime: util.formatRelativeTime, formatRelativeTime: util.formatRelativeTime,
formatFileSize: util.formatFileSize, formatFileSize: util.formatFileSize,
postContentTemplate: postContentTemplate, postContentTemplate: postContentTemplate,
postEditTemplate: postEditTemplate, postEditTemplate: postEditTemplate,
historyTemplate: historyTemplate,
privileges: privileges, privileges: privileges,
editPrivileges: editPrivileges, editPrivileges: editPrivileges,
}); });
@ -228,6 +245,23 @@ App.Presenters.PostPresenter = function(
}); });
} }
function historyButtonClicked(e) {
e.preventDefault();
if ($el.find('.post-history-wrapper').is(':visible')) {
hideHistory();
} else {
showHistory();
}
}
function hideHistory() {
$el.find('.post-history-wrapper').slideUp('slow');
}
function showHistory() {
$el.find('.post-history-wrapper').slideDown('slow');
}
function showEditError(response) { function showEditError(response) {
window.alert(response.json && response.json.error || response); window.alert(response.json && response.json.error || response);
} }

View file

@ -0,0 +1,63 @@
<table class="history">
<tbody>
<% _.each(history, function( historyEntry) { %>
<tr>
<td class="time">
<%= formatRelativeTime(historyEntry.time) %>
</td>
<td class="user">
<% var userName = historyEntry.user && historyEntry.user.name || '' %>
<% if (userName) { %>
<a href="#/user/<%= userName %>">
<% } %>
<img class="author-avatar"
src="/data/thumbnails/20x20/avatars/<%= userName || '!' %>"
alt="<%= userName || 'Anonymous user' %>"/>
<%= userName || 'Anonymous user' %>
<% if (userName) { %>
</a>
<% } %>
</td>
<td class="subject">
<% if (historyEntry.type == 0) { %>
<a href="#/post/<%= historyEntry.primaryKey %>">
@<%= historyEntry.primaryKey %>
</a>
<% } else { %>
?
<% } %>
</td>
<td class="difference">
<% if (historyEntry.operation == 1) { %>
deleted
<% } else { %>
changed
<% if (historyEntry.dataDifference) { %>
<ul><!--
--><% _.each(historyEntry.dataDifference['+'], function (difference) { %><!--
--><li class="addition difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + difference[1] %><!--
--></li><!--
--><% }) %><!--
--><% _.each(historyEntry.dataDifference['-'], function (difference) { %><!--
--><li class="removal difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + difference[1] %><!--
--></li><!--
--><% }) %><!--
--></ul>
<% } %>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>

View file

@ -20,11 +20,20 @@
<span class="right"> <span class="right">
featured by featured by
<% if (post.user.name) { %>
<a href="#/user/<%= post.user.name %>">
<% } %>
<img class="author-avatar" <img class="author-avatar"
src="/data/thumbnails/25x25/avatars/<%= post.user.name || '!' %>" src="/data/thumbnails/25x25/avatars/<%= post.user.name || '!' %>"
alt="<%= post.user.name || 'Anonymous user' %>"/> alt="<%= post.user.name || 'Anonymous user' %>"/>
<%= post.user.name || 'Anonymous user' %> <%= post.user.name || 'Anonymous user' %>
<% if (post.user.name) { %>
</a>
<% } %>
</span> </span>
</div> </div>

View file

@ -134,6 +134,14 @@
</a> </a>
</li> </li>
<% } %> <% } %>
<% if (privileges.canViewHistory) { %>
<li>
<a href="#" class="history">
History
</a>
</li>
<% } %>
</ul> </ul>
<% } %> <% } %>
@ -147,5 +155,14 @@
</div> </div>
<%= postContentTemplate({post: post}) %> <%= postContentTemplate({post: post}) %>
<% if (privileges.canViewHistory) { %>
<div class="post-history-wrapper">
<%= historyTemplate({
history: postHistory,
formatRelativeTime: formatRelativeTime
}) %>
</div>
<% } %>
</div> </div>
</div> </div>

View file

@ -0,0 +1,44 @@
<?php
namespace Szurubooru\Controllers;
final class HistoryController extends AbstractController
{
private $historyService;
private $privilegeService;
private $snapshotSearchParser;
private $inputReader;
private $snapshotViewProxy;
public function __construct(
\Szurubooru\Services\HistoryService $historyService,
\Szurubooru\Services\PrivilegeService $privilegeService,
\Szurubooru\SearchServices\Parsers\SnapshotSearchParser $snapshotSearchParser,
\Szurubooru\Helpers\InputReader $inputReader,
\Szurubooru\Controllers\ViewProxies\SnapshotViewProxy $snapshotViewProxy)
{
$this->historyService = $historyService;
$this->privilegeService = $privilegeService;
$this->snapshotSearchParser = $snapshotSearchParser;
$this->inputReader = $inputReader;
$this->snapshotViewProxy = $snapshotViewProxy;
}
public function registerRoutes(\Szurubooru\Router $router)
{
$router->get('/api/history', [$this, 'getFiltered']);
}
public function getFiltered()
{
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::VIEW_HISTORY);
$filter = $this->snapshotSearchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize(50);
$result = $this->historyService->getFiltered($filter);
$entities = $this->snapshotViewProxy->fromArray($result->getEntities());
return [
'data' => $entities,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
}

View file

@ -9,6 +9,7 @@ final class PostController extends AbstractController
private $postSearchParser; private $postSearchParser;
private $inputReader; private $inputReader;
private $postViewProxy; private $postViewProxy;
private $snapshotViewProxy;
public function __construct( public function __construct(
\Szurubooru\Config $config, \Szurubooru\Config $config,
@ -16,7 +17,8 @@ final class PostController extends AbstractController
\Szurubooru\Services\PostService $postService, \Szurubooru\Services\PostService $postService,
\Szurubooru\SearchServices\Parsers\PostSearchParser $postSearchParser, \Szurubooru\SearchServices\Parsers\PostSearchParser $postSearchParser,
\Szurubooru\Helpers\InputReader $inputReader, \Szurubooru\Helpers\InputReader $inputReader,
\Szurubooru\Controllers\ViewProxies\PostViewProxy $postViewProxy) \Szurubooru\Controllers\ViewProxies\PostViewProxy $postViewProxy,
\Szurubooru\Controllers\ViewProxies\SnapshotViewProxy $snapshotViewProxy)
{ {
$this->config = $config; $this->config = $config;
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
@ -24,30 +26,31 @@ final class PostController extends AbstractController
$this->postSearchParser = $postSearchParser; $this->postSearchParser = $postSearchParser;
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
$this->postViewProxy = $postViewProxy; $this->postViewProxy = $postViewProxy;
$this->snapshotViewProxy = $snapshotViewProxy;
} }
public function registerRoutes(\Szurubooru\Router $router) public function registerRoutes(\Szurubooru\Router $router)
{ {
$router->post('/api/posts', [$this, 'createPost']); $router->post('/api/posts', [$this, 'createPost']);
$router->get('/api/posts', [$this, 'getFiltered']); $router->get('/api/posts', [$this, 'getFiltered']);
$router->get('/api/posts/featured', [$this, 'getFeatured']);
$router->get('/api/posts/:postNameOrId', [$this, 'getByNameOrId']); $router->get('/api/posts/:postNameOrId', [$this, 'getByNameOrId']);
$router->get('/api/posts/:postNameOrId/history', [$this, 'getHistory']);
$router->put('/api/posts/:postNameOrId', [$this, 'updatePost']); $router->put('/api/posts/:postNameOrId', [$this, 'updatePost']);
$router->delete('/api/posts/:postNameOrId', [$this, 'deletePost']); $router->delete('/api/posts/:postNameOrId', [$this, 'deletePost']);
$router->post('/api/posts/:postNameOrId/feature', [$this, 'featurePost']); $router->post('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
$router->put('/api/posts/:postNameOrId/feature', [$this, 'featurePost']); $router->put('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
} }
public function getFeatured() public function getByNameOrId($postNameOrId)
{ {
$post = $this->postService->getFeatured(); $post = $this->getByNameOrIdWithoutProxy($postNameOrId);
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
} }
public function getByNameOrId($postNameOrId) public function getHistory($postNameOrId)
{ {
$post = $this->postService->getByNameOrId($postNameOrId); $post = $this->getByNameOrIdWithoutProxy($postNameOrId);
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()); return ['data' => $this->snapshotViewProxy->fromArray($this->postService->getHistory($post))];
} }
public function getFiltered() public function getFiltered()
@ -113,6 +116,14 @@ final class PostController extends AbstractController
$this->postService->featurePost($post); $this->postService->featurePost($post);
} }
private function getByNameOrIdWithoutProxy($postNameOrId)
{
if ($postNameOrId === 'featured')
return $this->postService->getFeatured();
else
return $this->postService->getByNameOrId($postNameOrId);
}
private function getFullFetchConfig() private function getFullFetchConfig()
{ {
return return

View file

@ -0,0 +1,29 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
class SnapshotViewProxy extends AbstractViewProxy
{
private $userViewProxy;
public function __construct(UserViewProxy $userViewProxy)
{
$this->userViewProxy = $userViewProxy;
}
public function fromEntity($snapshot, $config = [])
{
$result = new \StdClass;
if ($snapshot)
{
$result->time = $snapshot->getTime();
$result->type = $snapshot->getType();
$result->primaryKey = $snapshot->getPrimaryKey();
$result->operation = $snapshot->getOperation();
$result->user = $this->userViewProxy->fromEntity($snapshot->getUser());
$result->data = $snapshot->getData();
$result->dataDifference = $snapshot->getDataDifference();
}
return $result;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Szurubooru\Dao\EntityConverters;
class SnapshotEntityConverter extends AbstractEntityConverter implements IEntityConverter
{
public function toArray(\Szurubooru\Entities\Entity $entity)
{
return
[
'id' => $entity->getId(),
'time' => $entity->getTime(),
'type' => $entity->getType(),
'primaryKey' => $entity->getPrimaryKey(),
'userId' => $entity->getUserId(),
'operation' => $entity->getOperation(),
'data' => json_encode($entity->getData()),
'dataDifference' => json_encode($entity->getDataDifference()),
];
}
public function toBasicEntity(array $array)
{
$entity = new \Szurubooru\Entities\Snapshot(intval($array['id']));
$entity->setTime($array['time']);
$entity->setType(intval($array['type']));
$entity->setPrimaryKey($array['primaryKey']);
$entity->setUserId($array['userId']);
$entity->setOperation($array['operation']);
$entity->setData(json_decode($array['data'], true));
$entity->setDataDifference(json_decode($array['dataDifference'], true));
return $entity;
}
}

45
src/Dao/SnapshotDao.php Normal file
View file

@ -0,0 +1,45 @@
<?php
namespace Szurubooru\Dao;
class SnapshotDao extends AbstractDao
{
private $userDao;
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\UserDao $userDao)
{
parent::__construct(
$databaseConnection,
'snapshots',
new \Szurubooru\Dao\EntityConverters\SnapshotEntityConverter());
$this->userDao = $userDao;
}
public function findByTypeAndKey($type, $primaryKey)
{
$query = $this->fpdo
->from($this->tableName)
->where('type', $type)
->where('primaryKey', $primaryKey)
->orderBy('time DESC');
return $this->arrayToEntities(iterator_to_array($query));
}
public function afterLoad(\Szurubooru\Entities\Entity $snapshot)
{
$snapshot->setLazyLoader(
\Szurubooru\Entities\Snapshot::LAZY_LOADER_USER,
function (\Szurubooru\Entities\Snapshot $snapshot)
{
return $this->getUser($snapshot);
});
}
private function getUser(\Szurubooru\Entities\Snapshot $snapshot)
{
$userId = $snapshot->getUserId();
return $this->userDao->findById($userId);
}
}

View file

@ -220,14 +220,7 @@ final class Post extends Entity
public function setUser(\Szurubooru\Entities\User $user = null) public function setUser(\Szurubooru\Entities\User $user = null)
{ {
$this->lazySave(self::LAZY_LOADER_USER, $user); $this->lazySave(self::LAZY_LOADER_USER, $user);
if ($user) $this->userId = $user ? $user->getId() : null;
{
$this->userId = $user->getId();
}
else
{
$this->userId = null;
}
} }
public function getContent() public function getContent()

101
src/Entities/Snapshot.php Normal file
View file

@ -0,0 +1,101 @@
<?php
namespace Szurubooru\Entities;
final class Snapshot extends Entity
{
const TYPE_POST = 0;
const OPERATION_CHANGE = 0;
const OPERATION_DELETE = 1;
const LAZY_LOADER_USER = 'user';
private $time;
private $type;
private $primaryKey;
private $operation;
private $userId;
private $data;
private $dataDifference;
public function getTime()
{
return $this->time;
}
public function setTime($time)
{
$this->time = $time;
}
public function getType()
{
return $this->type;
}
public function setType($type)
{
$this->type = $type;
}
public function getPrimaryKey()
{
return $this->primaryKey;
}
public function setPrimaryKey($primaryKey)
{
$this->primaryKey = $primaryKey;
}
public function getOperation()
{
return $this->operation;
}
public function setOperation($operation)
{
$this->operation = $operation;
}
public function getUserId()
{
return $this->userId;
}
public function setUserId($userId)
{
$this->userId = $userId;
}
public function getData()
{
return $this->data;
}
public function setData($data)
{
$this->data = $data;
}
public function getDataDifference()
{
return $this->dataDifference;
}
public function setDataDifference($dataDifference)
{
$this->dataDifference = $dataDifference;
}
public function getUser()
{
return $this->lazyLoad(self::LAZY_LOADER_USER, null);
}
public function setUser(\Szurubooru\Entities\User $user = null)
{
$this->lazySave(self::LAZY_LOADER_USER, $user);
$this->userId = $user ? $user->getId() : null;
}
}

View file

@ -35,6 +35,11 @@ class EnumHelper
'youtube' => \Szurubooru\Entities\Post::POST_TYPE_YOUTUBE, 'youtube' => \Szurubooru\Entities\Post::POST_TYPE_YOUTUBE,
]; ];
private static $snapshotTypeMap =
[
'post' => \Szurubooru\Entities\Snapshot::TYPE_POST,
];
public static function accessRankToString($accessRank) public static function accessRankToString($accessRank)
{ {
return self::enumToString(self::$accessRankMap, $accessRank); return self::enumToString(self::$accessRankMap, $accessRank);
@ -70,6 +75,11 @@ class EnumHelper
return self::enumToString(self::$postTypeMap, $postType); return self::enumToString(self::$postTypeMap, $postType);
} }
public static function snapshotTypeFromString($snapshotTypeString)
{
return self::stringToEnum(self::$snapshotTypeMap, $snapshotTypeString);
}
private static function enumToString($enumMap, $enumValue) private static function enumToString($enumMap, $enumValue)
{ {
$reverseMap = array_flip($enumMap); $reverseMap = array_flip($enumMap);

View file

@ -33,4 +33,6 @@ class Privilege
const CHANGE_POST_RELATIONS = 'changePostRelations'; const CHANGE_POST_RELATIONS = 'changePostRelations';
const LIST_TAGS = 'listTags'; const LIST_TAGS = 'listTags';
const VIEW_HISTORY = 'viewHistory';
} }

View file

@ -0,0 +1,8 @@
<?php
namespace Szurubooru\SearchServices\Filters;
class SnapshotFilter extends BasicFilter implements IFilter
{
const REQUIREMENT_PRIMARY_KEY = 'primaryKey';
const REQUIREMENT_TYPE = 'type';
}

View file

@ -0,0 +1,41 @@
<?php
namespace Szurubooru\SearchServices\Parsers;
class SnapshotSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new \Szurubooru\SearchServices\Filters\SnapshotFilter;
}
protected function decorateFilterFromToken($filter, $token)
{
if (substr_count($token->getValue(), ',') !== 1)
throw new \BadMethodCallException('Not supported');
if ($token->isNegated())
throw new \BadMethodCallException('Not supported');
list ($type, $primaryKey) = explode(',', $token->getValue());
$requirement = new \Szurubooru\SearchServices\Requirements\Requirement();
$requirement->setType(\Szurubooru\SearchServices\Filters\SnapshotFilter::REQUIREMENT_PRIMARY_KEY);
$requirement->setValue($primaryKey);
$filter->addRequirement($requirement);
$requirement = new \Szurubooru\SearchServices\Requirements\Requirement();
$requirement->setType(\Szurubooru\SearchServices\Filters\SnapshotFilter::REQUIREMENT_TYPE);
$requirement->setValue(\Szurubooru\Helpers\EnumHelper::snapshotTypeFromString($type));
$filter->addRequirement($requirement);
}
protected function decorateFilterFromNamedToken($filter, $namedToken)
{
throw new \BadMethodCallException('Not supported');
}
protected function getOrderColumn($token)
{
throw new \BadMethodCallException('Not supported');
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace Szurubooru\Services;
class HistoryService
{
private $validator;
private $snapshotDao;
private $globalParamDao;
private $timeService;
private $authService;
private $transactionManager;
public function __construct(
\Szurubooru\Validator $validator,
\Szurubooru\Dao\SnapshotDao $snapshotDao,
\Szurubooru\Dao\GlobalParamDao $globalParamDao,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\AuthService $authService)
{
$this->validator = $validator;
$this->snapshotDao = $snapshotDao;
$this->globalParamDao = $globalParamDao;
$this->timeService = $timeService;
$this->authService = $authService;
$this->transactionManager = $transactionManager;
}
public function getFiltered(\Szurubooru\SearchServices\Filters\SnapshotFilter $filter)
{
$transactionFunc = function() use ($filter)
{
return $this->snapshotDao->findFiltered($filter);
};
return $this->transactionManager->rollback($transactionFunc);
}
public function saveSnapshot(\Szurubooru\Entities\Snapshot $snapshot)
{
$transactionFunc = function() use ($snapshot)
{
$otherSnapshots = $this->snapshotDao->findByTypeAndKey($snapshot->getType(), $snapshot->getPrimaryKey());
if ($otherSnapshots)
{
$lastSnapshot = array_shift($otherSnapshots);
if ($lastSnapshot->getData() === $snapshot->getData())
return $lastSnapshot;
$dataDifference = $this->getSnapshotDataDifference($snapshot->getData(), $lastSnapshot->getData());
$snapshot->setDataDifference($dataDifference);
}
else
{
$dataDifference = $this->getSnapshotDataDifference($snapshot->getData(), []);
$snapshot->setDataDifference($dataDifference);
}
$snapshot->setTime($this->timeService->getCurrentTime());
$snapshot->setUser($this->authService->getLoggedInUser());
return $this->snapshotDao->save($snapshot);
};
return $this->transactionManager->commit($transactionFunc);
}
public function getPostDeleteSnapshot(\Szurubooru\Entities\Post $post)
{
$snapshot = $this->getPostSnapshot($post);
$snapshot->setOperation(\Szurubooru\Entities\Snapshot::OPERATION_DELETE);
return $snapshot;
}
public function getPostChangeSnapshot(\Szurubooru\Entities\Post $post)
{
$featuredPostParam = $this->globalParamDao->findByKey(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST);
$isFeatured = ($featuredPostParam and intval($featuredPostParam->getValue()) === $post->getId());
$data =
[
'source' => $post->getSource(),
'safety' => \Szurubooru\Helpers\EnumHelper::postSafetyToString($post->getSafety()),
'contentChecksum' => $post->getContentChecksum(),
'featured' => $isFeatured,
'tags' =>
array_map(
function ($tag)
{
return $tag->getName();
},
$post->getTags()),
'relations' =>
array_map(
function ($post)
{
return $post->getId();
},
$post->getRelatedPosts()),
];
$snapshot = $this->getPostSnapshot($post);
$snapshot->setOperation(\Szurubooru\Entities\Snapshot::OPERATION_CHANGE);
$snapshot->setData($data);
return $snapshot;
}
public function getSnapshotDataDifference($newData, $oldData)
{
$diffFunction = function($base, $other)
{
$result = [];
foreach ($base as $key => $value)
{
if (is_array($base[$key]))
{
foreach ($base[$key] as $subValue)
{
if (!isset($other[$key]) or !in_array($subValue, $other[$key]))
$result[] = [$key, $subValue];
}
}
elseif (!isset($other[$key]) or $base[$key] !== $other[$key])
{
$result[] = [$key, $value];
}
}
return $result;
};
return [
'+' => $diffFunction($newData, $oldData),
'-' => $diffFunction($oldData, $newData),
];
}
private function getPostSnapshot(\Szurubooru\Entities\Post $post)
{
$snapshot = new \Szurubooru\Entities\Snapshot();
$snapshot->setType(\Szurubooru\Entities\Snapshot::TYPE_POST);
$snapshot->setPrimaryKey($post->getId());
return $snapshot;
}
}

View file

@ -11,6 +11,7 @@ class PostService
private $timeService; private $timeService;
private $authService; private $authService;
private $fileService; private $fileService;
private $historyService;
private $imageManipulator; private $imageManipulator;
public function __construct( public function __construct(
@ -22,6 +23,7 @@ class PostService
\Szurubooru\Services\AuthService $authService, \Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\TimeService $timeService, \Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\FileService $fileService, \Szurubooru\Services\FileService $fileService,
\Szurubooru\Services\HistoryService $historyService,
\Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator) \Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator)
{ {
$this->config = $config; $this->config = $config;
@ -32,6 +34,7 @@ class PostService
$this->timeService = $timeService; $this->timeService = $timeService;
$this->authService = $authService; $this->authService = $authService;
$this->fileService = $fileService; $this->fileService = $fileService;
$this->historyService = $historyService;
$this->imageManipulator = $imageManipulator; $this->imageManipulator = $imageManipulator;
} }
@ -82,6 +85,27 @@ class PostService
return $this->transactionManager->rollback($transactionFunc); return $this->transactionManager->rollback($transactionFunc);
} }
public function getHistory(\Szurubooru\Entities\Post $post)
{
$transactionFunc = function() use ($post)
{
$filter = new \Szurubooru\SearchServices\Filters\SnapshotFilter();
$requirement = new \Szurubooru\SearchServices\Requirement();
$requirement->setType(\Szurubooru\SearchServices\Filters\SnapshotFilter::REQUIREMENT_PRIMARY_KEY);
$requirement->setValue($post->getId());
$filter->addRequirement($requirement);
$requirement = new \Szurubooru\SearchServices\Requirement();
$requirement->setType(\Szurubooru\SearchServices\Filters\SnapshotFilter::REQUIREMENT_TYPE);
$requirement->setValue(\Szurubooru\Entities\Snapshot::TYPE_POST);
$filter->addRequirement($requirement);
return $this->historyService->getFiltered($filter)->getEntities();
};
return $this->transactionManager->rollback($transactionFunc);
}
public function createPost(\Szurubooru\FormData\UploadFormData $formData) public function createPost(\Szurubooru\FormData\UploadFormData $formData)
{ {
$transactionFunc = function() use ($formData) $transactionFunc = function() use ($formData)
@ -100,7 +124,10 @@ class PostService
$this->updatePostTags($post, $formData->tags); $this->updatePostTags($post, $formData->tags);
$this->updatePostContentFromStringOrUrl($post, $formData->content, $formData->url); $this->updatePostContentFromStringOrUrl($post, $formData->content, $formData->url);
return $this->postDao->save($post); $savedPost = $this->postDao->save($post);
$this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($savedPost));
return $savedPost;
}; };
return $this->transactionManager->commit($transactionFunc); return $this->transactionManager->commit($transactionFunc);
} }
@ -134,6 +161,7 @@ class PostService
if ($formData->relations !== null) if ($formData->relations !== null)
$this->updatePostRelations($post, $formData->relations); $this->updatePostRelations($post, $formData->relations);
$this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($post));
return $this->postDao->save($post); return $this->postDao->save($post);
}; };
return $this->transactionManager->commit($transactionFunc); return $this->transactionManager->commit($transactionFunc);
@ -246,8 +274,10 @@ class PostService
{ {
$relatedPosts = $this->postDao->findByIds($newRelatedPostIds); $relatedPosts = $this->postDao->findByIds($newRelatedPostIds);
foreach ($newRelatedPostIds as $postId) foreach ($newRelatedPostIds as $postId)
{
if (!isset($relatedPosts[$postId])) if (!isset($relatedPosts[$postId]))
throw new \DomainException('Post with id "' . $postId . '" was not found.'); throw new \DomainException('Post with id "' . $postId . '" was not found.');
}
$post->setRelatedPosts($relatedPosts); $post->setRelatedPosts($relatedPosts);
} }
@ -256,6 +286,7 @@ class PostService
{ {
$transactionFunc = function() use ($post) $transactionFunc = function() use ($post)
{ {
$this->historyService->saveSnapshot($this->historyService->getPostDeleteSnapshot($post));
$this->postDao->deleteById($post->getId()); $this->postDao->deleteById($post->getId());
}; };
$this->transactionManager->commit($transactionFunc); $this->transactionManager->commit($transactionFunc);
@ -265,6 +296,8 @@ class PostService
{ {
$transactionFunc = function() use ($post) $transactionFunc = function() use ($post)
{ {
$previousFeaturedPost = $this->getFeatured();
$post->setLastFeatureTime($this->timeService->getCurrentTime()); $post->setLastFeatureTime($this->timeService->getCurrentTime());
$post->setFeatureCount($post->getFeatureCount() + 1); $post->setFeatureCount($post->getFeatureCount() + 1);
$this->postDao->save($post); $this->postDao->save($post);
@ -272,6 +305,10 @@ class PostService
$globalParam->setKey(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST); $globalParam->setKey(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST);
$globalParam->setValue($post->getId()); $globalParam->setValue($post->getId());
$this->globalParamDao->save($globalParam); $this->globalParamDao->save($globalParam);
if ($previousFeaturedPost)
$this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($previousFeaturedPost));
$this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($post));
}; };
$this->transactionManager->commit($transactionFunc); $this->transactionManager->commit($transactionFunc);
} }

View file

@ -16,4 +16,3 @@ class Upgrade08 implements IUpgrade
)'); )');
} }
} }

View file

@ -0,0 +1,40 @@
<?php
namespace Szurubooru\Upgrades;
class Upgrade09 implements IUpgrade
{
private $postDao;
private $historyService;
public function __construct(
\Szurubooru\Dao\PostDao $postDao,
\Szurubooru\Services\HistoryService $historyService)
{
$this->postDao = $postDao;
$this->historyService = $historyService;
}
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
{
$pdo = $databaseConnection->getPDO();
$pdo->exec('DROP TABLE IF EXISTS snapshots');
$pdo->exec('CREATE TABLE snapshots
(
id INTEGER PRIMARY KEY NOT NULL,
time TIMESTAMP NOT NULL,
type INTEGER NOT NULL,
primaryKey TEXT NOT NULL,
operation INTEGER NOT NULL,
userId INTEGER,
data BLOB,
dataDifference BLOB
)');
foreach ($this->postDao->findAll() as $post)
{
$this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($post));
}
}
}

View file

@ -24,6 +24,7 @@ return [
$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), $container->get(\Szurubooru\Upgrades\Upgrade08::class),
$container->get(\Szurubooru\Upgrades\Upgrade09::class),
]; ];
}), }),
@ -35,6 +36,7 @@ return [
$container->get(\Szurubooru\Controllers\PostController::class), $container->get(\Szurubooru\Controllers\PostController::class),
$container->get(\Szurubooru\Controllers\PostContentController::class), $container->get(\Szurubooru\Controllers\PostContentController::class),
$container->get(\Szurubooru\Controllers\GlobalParamController::class), $container->get(\Szurubooru\Controllers\GlobalParamController::class),
$container->get(\Szurubooru\Controllers\HistoryController::class),
]; ];
}), }),
]; ];

View file

@ -0,0 +1,55 @@
<?php
namespace Szurubooru\Tests\Dao;
class SnapshotDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
public function setUp()
{
parent::setUp();
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
}
public function testSaving()
{
$snapshot = $this->getTestSnapshot();
$snapshotDao = $this->getSnapshotDao();
$snapshotDao->save($snapshot);
$this->assertNotNull($snapshot->getId());
$this->assertEntitiesEqual($snapshot, $snapshotDao->findById($snapshot->getId()));
}
public function testUserLazyLoader()
{
$snapshot = $this->getTestSnapshot();
$snapshot->setUser(new \Szurubooru\Entities\User(5));
$this->assertEquals(5, $snapshot->getUserId());
$snapshotDao = $this->getSnapshotDao();
$snapshotDao->save($snapshot);
$savedSnapshot = $snapshotDao->findById($snapshot->getId());
$this->assertEquals(5, $savedSnapshot->getUserId());
$this->userDaoMock
->expects($this->once())
->method('findById');
$savedSnapshot->getUser();
}
private function getTestSnapshot()
{
$snapshot = new \Szurubooru\Entities\Snapshot();
$snapshot->setType(\Szurubooru\Entities\Snapshot::TYPE_POST);
$snapshot->setData(['wake up', 'neo', ['follow' => 'white rabbit']]);
$snapshot->setPrimaryKey(1);
$snapshot->setTime('whateveer');
$snapshot->setUserId(null);
$snapshot->setOperation(\Szurubooru\Entities\Snapshot::OPERATION_CHANGE);
return $snapshot;
}
private function getSnapshotDao()
{
return new \Szurubooru\Dao\SnapshotDao(
$this->databaseConnection,
$this->userDaoMock);
}
}

View file

@ -0,0 +1,183 @@
<?php
namespace Szurubooru\Tests\Services;
class HistoryServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
private $validatorMock;
private $snapshotDaoMock;
private $globalParamDaoMock;
private $timeServiceMock;
private $authServiceMock;
private $transactionManagerMock;
public static function snapshotDataDifferenceProvider()
{
yield
[
[],
[],
['+' => [], '-' => []]
];
yield
[
['key' => 'unchangedValue'],
['key' => 'unchangedValue'],
['+' => [], '-' => []]
];
yield
[
['key' => 'newValue'],
[],
[
'+' => [['key', 'newValue']],
'-' => []
]
];
yield
[
[],
['key' => 'deletedValue'],
[
'+' => [],
'-' => [['key', 'deletedValue']]
]
];
yield
[
['key' => 'changedValue'],
['key' => 'oldValue'],
[
'+' => [['key', 'changedValue']],
'-' => [['key', 'oldValue']]
]
];
yield
[
['key' => []],
['key' => []],
[
'+' => [],
'-' => []
]
];
yield
[
['key' => ['newArrayElement']],
['key' => []],
[
'+' => [['key', 'newArrayElement']],
'-' => []
]
];
yield
[
['key' => []],
['key' => ['removedArrayElement']],
[
'+' => [],
'-' => [['key', 'removedArrayElement']]
]
];
yield
[
['key' => ['unchangedValue', 'newValue']],
['key' => ['unchangedValue', 'oldValue']],
[
'+' => [['key', 'newValue']],
'-' => [['key', 'oldValue']]
]
];
}
public function setUp()
{
parent::setUp();
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->snapshotDaoMock = $this->mock(\Szurubooru\Dao\SnapshotDao::class);
$this->globalParamDaoMock = $this->mock(\Szurubooru\Dao\GlobalParamDao::class);
$this->transactionManagerMock = $this->mock(\Szurubooru\Dao\TransactionManager::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class);
}
public function testPostChangeSnapshot()
{
$tag1 = new \Szurubooru\Entities\Tag();
$tag2 = new \Szurubooru\Entities\Tag();
$tag1->setName('tag1');
$tag2->setName('tag2');
$post1 = new \Szurubooru\Entities\Post(1);
$post2 = new \Szurubooru\Entities\Post(2);
$post = new \Szurubooru\Entities\Post(5);
$post->setTags([$tag1, $tag2]);
$post->setRelatedPosts([$post1, $post2]);
$post->setContentChecksum('checksum');
$post->setSafety(\Szurubooru\Entities\Post::POST_SAFETY_SKETCHY);
$post->setSource('amazing source');
$historyService = $this->getHistoryService();
$snapshot = $historyService->getPostChangeSnapshot($post);
$this->assertEquals([
'source' => 'amazing source',
'safety' => 'sketchy',
'contentChecksum' => 'checksum',
'featured' => false,
'tags' => ['tag1', 'tag2'],
'relations' => [1, 2]
], $snapshot->getData());
$this->assertEquals(\Szurubooru\Entities\Snapshot::TYPE_POST, $snapshot->getType());
$this->assertEquals(5, $snapshot->getPrimaryKey());
return $post;
}
/**
* @depends testPostChangeSnapshot
*/
public function testPostChangeSnapshotFeature($post)
{
$param = new \Szurubooru\Entities\GlobalParam;
$param->setValue($post->getId());
$this->globalParamDaoMock
->expects($this->once())
->method('findByKey')
->with(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST)
->willReturn($param);
$historyService = $this->getHistoryService();
$snapshot = $historyService->getPostChangeSnapshot($post);
$this->assertTrue($snapshot->getData()['featured']);
}
/**
* @dataProvider snapshotDataDifferenceProvider
*/
public function testSnapshotDataDifference($newData, $oldData, $expectedResult)
{
$historyService = $this->getHistoryService();
$this->assertEquals($expectedResult, $historyService->getSnapshotDataDifference($newData, $oldData));
}
private function getHistoryService()
{
return new \Szurubooru\Services\HistoryService(
$this->validatorMock,
$this->snapshotDaoMock,
$this->globalParamDaoMock,
$this->transactionManagerMock,
$this->timeServiceMock,
$this->authServiceMock);
}
}

View file

@ -11,6 +11,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $authServiceMock; private $authServiceMock;
private $timeServiceMock; private $timeServiceMock;
private $fileServiceMock; private $fileServiceMock;
private $historyServiceMock;
private $imageManipulatorMock; private $imageManipulatorMock;
public function setUp() public function setUp()
@ -23,6 +24,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class); $this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
$this->historyServiceMock = $this->mock(\Szurubooru\Services\HistoryService::class);
$this->configMock->set('database/maxPostSize', 1000000); $this->configMock->set('database/maxPostSize', 1000000);
$this->imageManipulatorMock = $this->mock(\Szurubooru\Services\ImageManipulation\ImageManipulator::class); $this->imageManipulatorMock = $this->mock(\Szurubooru\Services\ImageManipulation\ImageManipulator::class);
} }
@ -37,6 +39,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->authServiceMock->expects($this->once())->method('getLoggedInUser')->willReturn(new \Szurubooru\Entities\User(5)); $this->authServiceMock->expects($this->once())->method('getLoggedInUser')->willReturn(new \Szurubooru\Entities\User(5));
$this->historyServiceMock->expects($this->once())->method('getPostChangeSnapshot')->willReturn(new \Szurubooru\Entities\Snapshot());
$this->postService = $this->getPostService(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
@ -65,6 +68,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->imageManipulatorMock->expects($this->once())->method('getImageWidth')->willReturn(640); $this->imageManipulatorMock->expects($this->once())->method('getImageWidth')->willReturn(640);
$this->imageManipulatorMock->expects($this->once())->method('getImageHeight')->willReturn(480); $this->imageManipulatorMock->expects($this->once())->method('getImageHeight')->willReturn(480);
$this->historyServiceMock->expects($this->once())->method('getPostChangeSnapshot')->willReturn(new \Szurubooru\Entities\Snapshot());
$this->postService = $this->getPostService(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
@ -85,6 +89,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->contentFileName = 'blah'; $formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->historyServiceMock->expects($this->once())->method('getPostChangeSnapshot')->willReturn(new \Szurubooru\Entities\Snapshot());
$this->postService = $this->getPostService(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
@ -103,6 +108,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->contentFileName = 'blah'; $formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->historyServiceMock->expects($this->once())->method('getPostChangeSnapshot')->willReturn(new \Szurubooru\Entities\Snapshot());
$this->postService = $this->getPostService(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
@ -165,6 +171,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->authServiceMock->expects($this->never())->method('getLoggedInUser'); $this->authServiceMock->expects($this->never())->method('getLoggedInUser');
$this->historyServiceMock->expects($this->once())->method('getPostChangeSnapshot')->willReturn(new \Szurubooru\Entities\Snapshot());
$this->postService = $this->getPostService(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
@ -182,6 +189,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->authServiceMock, $this->authServiceMock,
$this->timeServiceMock, $this->timeServiceMock,
$this->fileServiceMock, $this->fileServiceMock,
$this->historyServiceMock,
$this->imageManipulatorMock); $this->imageManipulatorMock);
} }
} }