Added post featuring
This commit is contained in:
parent
0038df26d8
commit
97ca08cf44
24 changed files with 402 additions and 22 deletions
12
TODO
12
TODO
|
@ -5,9 +5,6 @@ everything related to posts:
|
||||||
- single post view
|
- single post view
|
||||||
- basic information
|
- basic information
|
||||||
- time of last edit
|
- time of last edit
|
||||||
- time of last feature
|
|
||||||
- how many times the post was featured
|
|
||||||
- feature
|
|
||||||
- fav
|
- fav
|
||||||
- score (see notes about scoring)
|
- score (see notes about scoring)
|
||||||
- editing
|
- editing
|
||||||
|
@ -30,11 +27,9 @@ everything related to posts:
|
||||||
- hiding posts
|
- hiding posts
|
||||||
- link to iqdb and/or tineye
|
- link to iqdb and/or tineye
|
||||||
|
|
||||||
- featured posts
|
- auto feature
|
||||||
- ability to feature the post
|
- number of posts
|
||||||
- show the post on main page
|
- total size of posts
|
||||||
- tag list
|
|
||||||
- uploader
|
|
||||||
|
|
||||||
- random post
|
- random post
|
||||||
- regard safety settings
|
- regard safety settings
|
||||||
|
@ -115,7 +110,6 @@ everything related to comments:
|
||||||
- score (see notes about scoring)
|
- score (see notes about scoring)
|
||||||
|
|
||||||
refactors:
|
refactors:
|
||||||
- centralize markdown prefix decorators
|
|
||||||
- add enum validation in IValidatables (needs refactors of enums and
|
- add enum validation in IValidatables (needs refactors of enums and
|
||||||
possible disposal of EnumHelper in favor of something more subtle)
|
possible disposal of EnumHelper in favor of something more subtle)
|
||||||
- (idea) keep denormalized data in separate tables, i.e. tag usages in
|
- (idea) keep denormalized data in separate tables, i.e. tag usages in
|
||||||
|
|
|
@ -42,6 +42,7 @@ listUnsafePosts = anonymous, regularUser, powerUser, moderator, administ
|
||||||
uploadPosts = regularUser, powerUser, moderator, administrator
|
uploadPosts = regularUser, powerUser, moderator, administrator
|
||||||
uploadPostsAnonymously = regularUser, powerUser, moderator, administrator
|
uploadPostsAnonymously = regularUser, powerUser, moderator, administrator
|
||||||
deletePosts = moderator, administrator
|
deletePosts = moderator, administrator
|
||||||
|
featurePosts = moderator, administrator
|
||||||
|
|
||||||
listTags = anonymous, regularUser, powerUser, moderator, administrator
|
listTags = anonymous, regularUser, powerUser, moderator, administrator
|
||||||
|
|
||||||
|
|
29
public_html/css/home.css
Normal file
29
public_html/css/home.css
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#home {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#home h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home .post {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
#home .post .left {
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
#home .post .right {
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home .post-footer,
|
||||||
|
#home .post-footer img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home .post-footer img {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
|
@ -31,6 +31,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="/css/user.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/user.css"/>
|
||||||
<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"/>
|
||||||
<!-- /build -->
|
<!-- /build -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
|
||||||
uploadPosts: 'uploadPosts',
|
uploadPosts: 'uploadPosts',
|
||||||
uploadPostsAnonymously: 'uploadPostsAnonymously',
|
uploadPostsAnonymously: 'uploadPostsAnonymously',
|
||||||
deletePosts: 'deletePosts',
|
deletePosts: 'deletePosts',
|
||||||
|
featurePosts: 'featurePosts',
|
||||||
|
|
||||||
listTags: 'listTags',
|
listTags: 'listTags',
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,20 +2,50 @@ var App = App || {};
|
||||||
App.Presenters = App.Presenters || {};
|
App.Presenters = App.Presenters || {};
|
||||||
|
|
||||||
App.Presenters.HomePresenter = function(
|
App.Presenters.HomePresenter = function(
|
||||||
|
_,
|
||||||
jQuery,
|
jQuery,
|
||||||
topNavigationPresenter) {
|
util,
|
||||||
|
promise,
|
||||||
|
api,
|
||||||
|
topNavigationPresenter,
|
||||||
|
messagePresenter) {
|
||||||
|
|
||||||
var $el = jQuery('#content');
|
var $el = jQuery('#content');
|
||||||
|
var homeTemplate;
|
||||||
|
var postContentTemplate;
|
||||||
|
var post;
|
||||||
|
|
||||||
function init(args, loaded) {
|
function init(args, loaded) {
|
||||||
topNavigationPresenter.select('home');
|
topNavigationPresenter.select('home');
|
||||||
topNavigationPresenter.changeTitle('Home');
|
topNavigationPresenter.changeTitle('Home');
|
||||||
|
|
||||||
|
promise.waitAll(
|
||||||
|
util.promiseTemplate('home'),
|
||||||
|
util.promiseTemplate('post-content'),
|
||||||
|
api.get('/posts/featured'))
|
||||||
|
.then(function(
|
||||||
|
homeTemplateHtml,
|
||||||
|
postContentTemplateHtml,
|
||||||
|
response) {
|
||||||
|
homeTemplate = _.template(homeTemplateHtml);
|
||||||
|
postContentTemplate = _.template(postContentTemplateHtml);
|
||||||
|
|
||||||
|
post = response.json;
|
||||||
render();
|
render();
|
||||||
loaded();
|
loaded();
|
||||||
|
|
||||||
|
}).fail(function(response) {
|
||||||
|
messagePresenter.showError($el, response.json && response.json.error || response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
$el.html('Home placeholder');
|
$el.html(homeTemplate({
|
||||||
|
post: post,
|
||||||
|
postContentTemplate: postContentTemplate,
|
||||||
|
title: topNavigationPresenter.getBaseTitle(),
|
||||||
|
formatRelativeTime: util.formatRelativeTime,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -25,4 +55,4 @@ App.Presenters.HomePresenter = function(
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
App.DI.register('homePresenter', ['jQuery', 'topNavigationPresenter'], App.Presenters.HomePresenter);
|
App.DI.register('homePresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.HomePresenter);
|
||||||
|
|
|
@ -25,6 +25,7 @@ App.Presenters.PostPresenter = function(
|
||||||
topNavigationPresenter.select('posts');
|
topNavigationPresenter.select('posts');
|
||||||
|
|
||||||
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
|
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
|
||||||
|
privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts);
|
||||||
|
|
||||||
promise.waitAll(
|
promise.waitAll(
|
||||||
util.promiseTemplate('post'),
|
util.promiseTemplate('post'),
|
||||||
|
@ -34,7 +35,6 @@ App.Presenters.PostPresenter = function(
|
||||||
postTemplateHtml,
|
postTemplateHtml,
|
||||||
postContentTemplateHtml,
|
postContentTemplateHtml,
|
||||||
response) {
|
response) {
|
||||||
$messages = $el.find('.messages');
|
|
||||||
postTemplate = _.template(postTemplateHtml);
|
postTemplate = _.template(postTemplateHtml);
|
||||||
postContentTemplate = _.template(postContentTemplateHtml);
|
postContentTemplate = _.template(postContentTemplateHtml);
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ App.Presenters.PostPresenter = function(
|
||||||
|
|
||||||
}).fail(function(response) {
|
}).fail(function(response) {
|
||||||
$el.empty();
|
$el.empty();
|
||||||
messagePresenter.showError($messages, response.json && response.json.error || response);
|
showGenericError(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,23 +57,45 @@ App.Presenters.PostPresenter = function(
|
||||||
postContentTemplate: postContentTemplate,
|
postContentTemplate: postContentTemplate,
|
||||||
privileges: privileges,
|
privileges: privileges,
|
||||||
}));
|
}));
|
||||||
|
$messages = $el.find('.messages');
|
||||||
|
|
||||||
$el.find('.delete').click(deleteButtonClicked);
|
$el.find('.delete').click(deleteButtonClicked);
|
||||||
|
$el.find('.feature').click(featureButtonClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteButtonClicked(e) {
|
function deleteButtonClicked(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
messagePresenter.hideMessages($messages);
|
||||||
if (window.confirm('Do you really want to delete this post?')) {
|
if (window.confirm('Do you really want to delete this post?')) {
|
||||||
deletePost();
|
deletePost();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePost() {
|
function deletePost() {
|
||||||
api.delete('/posts/' + post.id).then(function(response) {
|
api.delete('/posts/' + post.id)
|
||||||
|
.then(function(response) {
|
||||||
router.navigate('#/posts');
|
router.navigate('#/posts');
|
||||||
}).fail(function(response) {
|
}).fail(showGenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function featureButtonClicked(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
messagePresenter.hideMessages($messages);
|
||||||
|
if (window.confirm('Do you want to feature this post on fron page?')) {
|
||||||
|
featurePost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function featurePost() {
|
||||||
|
api.post('/posts/' + post.id + '/feature')
|
||||||
|
.then(function(response) {
|
||||||
|
router.navigate('#/home');
|
||||||
|
})
|
||||||
|
.fail(showGenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGenericError(response) {
|
||||||
messagePresenter.showError($messages, response.json && response.json.error || response);
|
messagePresenter.showError($messages, response.json && response.json.error || response);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -47,6 +47,10 @@ App.Presenters.TopNavigationPresenter = function(
|
||||||
$el.find('li.' + selectedElement).find('a').addClass('active');
|
$el.find('li.' + selectedElement).find('a').addClass('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBaseTitle() {
|
||||||
|
return baseTitle;
|
||||||
|
}
|
||||||
|
|
||||||
function changeTitle(subTitle) {
|
function changeTitle(subTitle) {
|
||||||
var newTitle = baseTitle;
|
var newTitle = baseTitle;
|
||||||
if (subTitle) {
|
if (subTitle) {
|
||||||
|
@ -59,6 +63,7 @@ App.Presenters.TopNavigationPresenter = function(
|
||||||
init: init,
|
init: init,
|
||||||
render: render,
|
render: render,
|
||||||
select: select,
|
select: select,
|
||||||
|
getBaseTitle: getBaseTitle,
|
||||||
changeTitle: changeTitle,
|
changeTitle: changeTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
30
public_html/templates/home.tpl
Normal file
30
public_html/templates/home.tpl
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<div id="home">
|
||||||
|
<h1><%= title %></h1>
|
||||||
|
|
||||||
|
<% if (post) { %>
|
||||||
|
<div class="post">
|
||||||
|
<%= postContentTemplate({post: post}) %>
|
||||||
|
<div class="post-footer">
|
||||||
|
|
||||||
|
<span class="left">
|
||||||
|
<a href="#/post/<%= post.id %>">
|
||||||
|
<%= post.idMarkdown %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
uploaded
|
||||||
|
<%= formatRelativeTime(post.uploadTime) %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="right">
|
||||||
|
featured by
|
||||||
|
<img class="author-avatar"
|
||||||
|
src="/data/thumbnails/25x25/avatars/<%= post.user.name || '!' %>"
|
||||||
|
alt="<%= post.user.name || 'Anonymous user' %>"/>
|
||||||
|
|
||||||
|
<%= post.user.name || 'Anonymous user' %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -3,6 +3,6 @@
|
||||||
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>"
|
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>"
|
||||||
href="#/post/<%= post.id %>">
|
href="#/post/<%= post.id %>">
|
||||||
|
|
||||||
<img class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="@<%= post.id %>"/>
|
<img class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -69,6 +69,13 @@
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (post.featureCount > 0) { %>
|
||||||
|
<li>
|
||||||
|
Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %>
|
||||||
|
<small>(<%= formatRelativeTime(post.lastFeatureTime) %>)</small>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<% if (post.source) { %>
|
<% if (post.source) { %>
|
||||||
<li>
|
<li>
|
||||||
Source: <!--
|
Source: <!--
|
||||||
|
@ -91,12 +98,22 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (privileges.canFeaturePosts) { %>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="feature">
|
||||||
|
Feature
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="post-view">
|
<div id="post-view">
|
||||||
|
<div class="messages"></div>
|
||||||
|
|
||||||
<%= postContentTemplate({post: post}) %>
|
<%= postContentTemplate({post: post}) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,8 +24,17 @@ final class PostController extends AbstractController
|
||||||
{
|
{
|
||||||
$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->delete('/api/posts/:postNameOrId', [$this, 'deletePost']);
|
$router->delete('/api/posts/:postNameOrId', [$this, 'deletePost']);
|
||||||
|
$router->post('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
|
||||||
|
$router->put('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFeatured()
|
||||||
|
{
|
||||||
|
$post = $this->postService->getFeatured();
|
||||||
|
return $this->postViewProxy->fromEntity($post);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getByNameOrId($postNameOrId)
|
public function getByNameOrId($postNameOrId)
|
||||||
|
@ -64,4 +73,10 @@ final class PostController extends AbstractController
|
||||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||||
$this->postService->deletePost($post);
|
$this->postService->deletePost($post);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function featurePost($postNameOrId)
|
||||||
|
{
|
||||||
|
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||||
|
$this->postService->featurePost($post);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ class PostViewProxy extends AbstractViewProxy
|
||||||
if ($post)
|
if ($post)
|
||||||
{
|
{
|
||||||
$result->id = $post->getId();
|
$result->id = $post->getId();
|
||||||
|
$result->idMarkdown = $post->getIdMarkdown();
|
||||||
$result->name = $post->getName();
|
$result->name = $post->getName();
|
||||||
$result->uploadTime = $post->getUploadTime();
|
$result->uploadTime = $post->getUploadTime();
|
||||||
$result->lastEditTime = $post->getLastEditTime();
|
$result->lastEditTime = $post->getLastEditTime();
|
||||||
|
@ -31,6 +32,8 @@ class PostViewProxy extends AbstractViewProxy
|
||||||
$result->source = $post->getSource();
|
$result->source = $post->getSource();
|
||||||
$result->imageWidth = $post->getImageWidth();
|
$result->imageWidth = $post->getImageWidth();
|
||||||
$result->imageHeight = $post->getImageHeight();
|
$result->imageHeight = $post->getImageHeight();
|
||||||
|
$result->featureCount = $post->getFeatureCount();
|
||||||
|
$result->lastFeatureTime = $post->getLastFeatureTime();
|
||||||
$result->tags = $this->tagViewProxy->fromArray($post->getTags());
|
$result->tags = $this->tagViewProxy->fromArray($post->getTags());
|
||||||
$result->originalFileSize = $post->getOriginalFileSize();
|
$result->originalFileSize = $post->getOriginalFileSize();
|
||||||
$result->user = $this->userViewProxy->fromEntity($post->getUser());
|
$result->user = $this->userViewProxy->fromEntity($post->getUser());
|
||||||
|
|
23
src/Dao/EntityConverters/GlobalParamEntityConverter.php
Normal file
23
src/Dao/EntityConverters/GlobalParamEntityConverter.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao\EntityConverters;
|
||||||
|
|
||||||
|
class GlobalParamEntityConverter extends AbstractEntityConverter implements IEntityConverter
|
||||||
|
{
|
||||||
|
public function toArray(\Szurubooru\Entities\Entity $entity)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
'id' => $entity->getId(),
|
||||||
|
'key' => $entity->getKey(),
|
||||||
|
'value' => $entity->getValue(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toBasicEntity(array $array)
|
||||||
|
{
|
||||||
|
$entity = new \Szurubooru\Entities\GlobalParam($array['id']);
|
||||||
|
$entity->setKey($array['key']);
|
||||||
|
$entity->setValue($array['value']);
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||||
'imageHeight' => $entity->getImageHeight(),
|
'imageHeight' => $entity->getImageHeight(),
|
||||||
'originalFileSize' => $entity->getOriginalFileSize(),
|
'originalFileSize' => $entity->getOriginalFileSize(),
|
||||||
'originalFileName' => $entity->getOriginalFileName(),
|
'originalFileName' => $entity->getOriginalFileName(),
|
||||||
|
'featureCount' => $entity->getFeatureCount(),
|
||||||
|
'lastFeatureTime' => $entity->getLastFeatureTime(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +42,8 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||||
$entity->setImageHeight($array['imageHeight']);
|
$entity->setImageHeight($array['imageHeight']);
|
||||||
$entity->setOriginalFileSize($array['originalFileSize']);
|
$entity->setOriginalFileSize($array['originalFileSize']);
|
||||||
$entity->setOriginalFileName($array['originalFileName']);
|
$entity->setOriginalFileName($array['originalFileName']);
|
||||||
|
$entity->setFeatureCount(intval($array['featureCount']));
|
||||||
|
$entity->setLastFeatureTime($array['lastFeatureTime']);
|
||||||
$entity->setMeta(\Szurubooru\Entities\Post::META_TAG_COUNT, intval($array['tagCount']));
|
$entity->setMeta(\Szurubooru\Entities\Post::META_TAG_COUNT, intval($array['tagCount']));
|
||||||
return $entity;
|
return $entity;
|
||||||
}
|
}
|
||||||
|
|
34
src/Dao/GlobalParamDao.php
Normal file
34
src/Dao/GlobalParamDao.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao;
|
||||||
|
|
||||||
|
class GlobalParamDao extends AbstractDao implements ICrudDao
|
||||||
|
{
|
||||||
|
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
$databaseConnection,
|
||||||
|
'globals',
|
||||||
|
new \Szurubooru\Dao\EntityConverters\GlobalParamEntityConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(&$entity)
|
||||||
|
{
|
||||||
|
if (!$entity->getId())
|
||||||
|
{
|
||||||
|
$otherEntityWithThisKey = $this->findByKey($entity->getKey());
|
||||||
|
if ($otherEntityWithThisKey)
|
||||||
|
$entity->setId($otherEntityWithThisKey->getId());
|
||||||
|
}
|
||||||
|
parent::save($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByKey($key)
|
||||||
|
{
|
||||||
|
return $this->findOneBy('key', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteByKey($key)
|
||||||
|
{
|
||||||
|
return $this->deleteBy('key', $key);
|
||||||
|
}
|
||||||
|
}
|
30
src/Entities/GlobalParam.php
Normal file
30
src/Entities/GlobalParam.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Entities;
|
||||||
|
|
||||||
|
class GlobalParam extends Entity
|
||||||
|
{
|
||||||
|
const KEY_FEATURED_POST = 'featuredPost';
|
||||||
|
|
||||||
|
private $key;
|
||||||
|
private $value;
|
||||||
|
|
||||||
|
public function getKey()
|
||||||
|
{
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKey($key)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue()
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue($value)
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ final class Post extends Entity
|
||||||
protected $imageHeight;
|
protected $imageHeight;
|
||||||
protected $originalFileSize;
|
protected $originalFileSize;
|
||||||
protected $originalFileName;
|
protected $originalFileName;
|
||||||
|
protected $featureCount = 0;
|
||||||
|
protected $lastFeatureTime;
|
||||||
|
|
||||||
public function getIdMarkdown()
|
public function getIdMarkdown()
|
||||||
{
|
{
|
||||||
|
@ -168,6 +170,26 @@ final class Post extends Entity
|
||||||
$this->originalFileName = $originalFileName;
|
$this->originalFileName = $originalFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFeatureCount()
|
||||||
|
{
|
||||||
|
return $this->featureCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFeatureCount($featureCount)
|
||||||
|
{
|
||||||
|
$this->featureCount = $featureCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastFeatureTime()
|
||||||
|
{
|
||||||
|
return $this->lastFeatureTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastFeatureTime($lastFeatureTime)
|
||||||
|
{
|
||||||
|
$this->lastFeatureTime = $lastFeatureTime;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTags()
|
public function getTags()
|
||||||
{
|
{
|
||||||
return $this->lazyLoad(self::LAZY_LOADER_TAGS, []);
|
return $this->lazyLoad(self::LAZY_LOADER_TAGS, []);
|
||||||
|
|
|
@ -24,6 +24,7 @@ class Privilege
|
||||||
const UPLOAD_POSTS = 'uploadPosts';
|
const UPLOAD_POSTS = 'uploadPosts';
|
||||||
const UPLOAD_POSTS_ANONYMOUSLY = 'uploadPostsAnonymously';
|
const UPLOAD_POSTS_ANONYMOUSLY = 'uploadPostsAnonymously';
|
||||||
const DELETE_POSTS = 'deletePosts';
|
const DELETE_POSTS = 'deletePosts';
|
||||||
|
const FEATURE_POSTS = 'featurePosts';
|
||||||
|
|
||||||
const LIST_TAGS = 'listTags';
|
const LIST_TAGS = 'listTags';
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ class PostService
|
||||||
private $validator;
|
private $validator;
|
||||||
private $transactionManager;
|
private $transactionManager;
|
||||||
private $postDao;
|
private $postDao;
|
||||||
|
private $globalParamDao;
|
||||||
private $postSearchParser;
|
private $postSearchParser;
|
||||||
private $timeService;
|
private $timeService;
|
||||||
private $authService;
|
private $authService;
|
||||||
|
@ -18,6 +19,7 @@ class PostService
|
||||||
\Szurubooru\Validator $validator,
|
\Szurubooru\Validator $validator,
|
||||||
\Szurubooru\Dao\TransactionManager $transactionManager,
|
\Szurubooru\Dao\TransactionManager $transactionManager,
|
||||||
\Szurubooru\Dao\PostDao $postDao,
|
\Szurubooru\Dao\PostDao $postDao,
|
||||||
|
\Szurubooru\Dao\GlobalParamDao $globalParamDao,
|
||||||
\Szurubooru\SearchServices\Parsers\PostSearchParser $postSearchParser,
|
\Szurubooru\SearchServices\Parsers\PostSearchParser $postSearchParser,
|
||||||
\Szurubooru\Services\AuthService $authService,
|
\Szurubooru\Services\AuthService $authService,
|
||||||
\Szurubooru\Services\TimeService $timeService,
|
\Szurubooru\Services\TimeService $timeService,
|
||||||
|
@ -28,6 +30,7 @@ class PostService
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
$this->transactionManager = $transactionManager;
|
$this->transactionManager = $transactionManager;
|
||||||
$this->postDao = $postDao;
|
$this->postDao = $postDao;
|
||||||
|
$this->globalParamDao = $globalParamDao;
|
||||||
$this->postSearchParser = $postSearchParser;
|
$this->postSearchParser = $postSearchParser;
|
||||||
$this->timeService = $timeService;
|
$this->timeService = $timeService;
|
||||||
$this->authService = $authService;
|
$this->authService = $authService;
|
||||||
|
@ -72,6 +75,18 @@ class PostService
|
||||||
return $this->transactionManager->rollback($transactionFunc);
|
return $this->transactionManager->rollback($transactionFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFeatured()
|
||||||
|
{
|
||||||
|
$transactionFunc = function()
|
||||||
|
{
|
||||||
|
$globalParam = $this->globalParamDao->findByKey(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST);
|
||||||
|
if (!$globalParam)
|
||||||
|
return null;
|
||||||
|
return $this->getByNameOrId($globalParam->getValue());
|
||||||
|
};
|
||||||
|
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)
|
||||||
|
@ -199,6 +214,21 @@ class PostService
|
||||||
$this->transactionManager->commit($transactionFunc);
|
$this->transactionManager->commit($transactionFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function featurePost(\Szurubooru\Entities\Post $post)
|
||||||
|
{
|
||||||
|
$transactionFunc = function() use ($post)
|
||||||
|
{
|
||||||
|
$post->setLastFeatureTime($this->timeService->getCurrentTime());
|
||||||
|
$post->setFeatureCount($post->getFeatureCount() + 1);
|
||||||
|
$this->postDao->save($post);
|
||||||
|
$globalParam = new \Szurubooru\Entities\GlobalParam();
|
||||||
|
$globalParam->setKey(\Szurubooru\Entities\GlobalParam::KEY_FEATURED_POST);
|
||||||
|
$globalParam->setValue($post->getId());
|
||||||
|
$this->globalParamDao->save($globalParam);
|
||||||
|
};
|
||||||
|
$this->transactionManager->commit($transactionFunc);
|
||||||
|
}
|
||||||
|
|
||||||
private function assertNoPostWithThisContentChecksum(\Szurubooru\Entities\Post $parent)
|
private function assertNoPostWithThisContentChecksum(\Szurubooru\Entities\Post $parent)
|
||||||
{
|
{
|
||||||
$checksumToCheck = $parent->getContentChecksum();
|
$checksumToCheck = $parent->getContentChecksum();
|
||||||
|
|
20
src/Upgrades/Upgrade07.php
Normal file
20
src/Upgrades/Upgrade07.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Upgrades;
|
||||||
|
|
||||||
|
class Upgrade07 implements IUpgrade
|
||||||
|
{
|
||||||
|
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
|
||||||
|
{
|
||||||
|
$pdo = $databaseConnection->getPDO();
|
||||||
|
|
||||||
|
$pdo->exec('ALTER TABLE posts ADD COLUMN featureCount INTEGER NOT NULL DEFAULT 0');
|
||||||
|
$pdo->exec('ALTER TABLE posts ADD COLUMN lastFeatureTime TIMESTAMP');
|
||||||
|
|
||||||
|
$pdo->exec('CREATE TABLE globals
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
key TEXT UNIQUE NOT NULL,
|
||||||
|
value TEXT
|
||||||
|
)');
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ return [
|
||||||
$container->get(\Szurubooru\Upgrades\Upgrade04::class),
|
$container->get(\Szurubooru\Upgrades\Upgrade04::class),
|
||||||
$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),
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
64
tests/Dao/GlobalParamDaoTest.php
Normal file
64
tests/Dao/GlobalParamDaoTest.php
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Tests\Dao;
|
||||||
|
|
||||||
|
class GlobalParamDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
|
{
|
||||||
|
public function testSettingValues()
|
||||||
|
{
|
||||||
|
$expected = new \Szurubooru\Entities\GlobalParam();
|
||||||
|
$expected->setKey('key');
|
||||||
|
$expected->setValue('test');
|
||||||
|
|
||||||
|
$globalParamDao = $this->getGlobalParamDao();
|
||||||
|
$globalParamDao->save($expected);
|
||||||
|
|
||||||
|
$actual = $globalParamDao->findByKey($expected->getKey());
|
||||||
|
$this->assertEntitiesEqual($actual, $expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInsertingSameKeyTwice()
|
||||||
|
{
|
||||||
|
$param1 = new \Szurubooru\Entities\GlobalParam();
|
||||||
|
$param1->setKey('key');
|
||||||
|
$param1->setValue('value1');
|
||||||
|
|
||||||
|
$param2 = new \Szurubooru\Entities\GlobalParam();
|
||||||
|
$param2->setKey('key');
|
||||||
|
$param2->setValue('value2');
|
||||||
|
|
||||||
|
$globalParamDao = $this->getGlobalParamDao();
|
||||||
|
$globalParamDao->save($param1);
|
||||||
|
$globalParamDao->save($param2);
|
||||||
|
|
||||||
|
$this->assertEquals([$param2], array_values($globalParamDao->findAll()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdatingValues()
|
||||||
|
{
|
||||||
|
$expected = new \Szurubooru\Entities\GlobalParam();
|
||||||
|
$expected->setKey('key');
|
||||||
|
$expected->setValue('test');
|
||||||
|
|
||||||
|
$globalParamDao = $this->getGlobalParamDao();
|
||||||
|
$globalParamDao->save($expected);
|
||||||
|
|
||||||
|
$expected->setKey('key2');
|
||||||
|
$expected->setValue('test2');
|
||||||
|
$globalParamDao->save($expected);
|
||||||
|
|
||||||
|
$actual = $globalParamDao->findByKey($expected->getKey());
|
||||||
|
$this->assertEntitiesEqual($actual, $expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetrievingUnknownKeys()
|
||||||
|
{
|
||||||
|
$globalParamDao = $this->getGlobalParamDao();
|
||||||
|
$actual = $globalParamDao->findByKey('hey i dont exist');
|
||||||
|
$this->assertNull($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getGlobalParamDao()
|
||||||
|
{
|
||||||
|
return new \Szurubooru\Dao\GlobalParamDao($this->databaseConnection);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
||||||
private $validatorMock;
|
private $validatorMock;
|
||||||
private $transactionManagerMock;
|
private $transactionManagerMock;
|
||||||
private $postDaoMock;
|
private $postDaoMock;
|
||||||
|
private $globalParamDaoMock;
|
||||||
private $postSearchParserMock;
|
private $postSearchParserMock;
|
||||||
private $authServiceMock;
|
private $authServiceMock;
|
||||||
private $timeServiceMock;
|
private $timeServiceMock;
|
||||||
|
@ -19,6 +20,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
||||||
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
|
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
|
||||||
$this->transactionManagerMock = $this->mockTransactionManager();
|
$this->transactionManagerMock = $this->mockTransactionManager();
|
||||||
$this->postDaoMock = $this->mock(\Szurubooru\Dao\PostDao::class);
|
$this->postDaoMock = $this->mock(\Szurubooru\Dao\PostDao::class);
|
||||||
|
$this->globalParamDaoMock = $this->mock(\Szurubooru\Dao\GlobalParamDao::class);
|
||||||
$this->postSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\PostSearchParser::class);
|
$this->postSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\PostSearchParser::class);
|
||||||
$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);
|
||||||
|
@ -178,6 +180,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
||||||
$this->validatorMock,
|
$this->validatorMock,
|
||||||
$this->transactionManagerMock,
|
$this->transactionManagerMock,
|
||||||
$this->postDaoMock,
|
$this->postDaoMock,
|
||||||
|
$this->globalParamDaoMock,
|
||||||
$this->postSearchParserMock,
|
$this->postSearchParserMock,
|
||||||
$this->authServiceMock,
|
$this->authServiceMock,
|
||||||
$this->timeServiceMock,
|
$this->timeServiceMock,
|
||||||
|
|
Loading…
Reference in a new issue