Fixed automatic featuring post

- Fixed main page view
- Code moved from StaticPagesController to PostModel
- Code split into semantically meaningful methods
- Allowed anonymous featuring through API
- Added protection against automatic featuring of hidden post
This commit is contained in:
Marcin Kurczewski 2014-05-11 23:40:09 +02:00
parent 6b40d6be7e
commit 8aa499a0b9
9 changed files with 262 additions and 61 deletions

View file

@ -1,16 +1,22 @@
<?php <?php
class FeaturePostJob extends AbstractPostJob class FeaturePostJob extends AbstractPostJob
{ {
const ANONYMOUS = 'anonymous';
public function execute() public function execute()
{ {
$post = $this->post; $post = $this->post;
PropertyModel::set(PropertyModel::FeaturedPostId, $post->getId()); PropertyModel::set(PropertyModel::FeaturedPostId, $post->getId());
PropertyModel::set(PropertyModel::FeaturedPostDate, time()); PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, Auth::getCurrentUser()->getName());
PropertyModel::set(PropertyModel::FeaturedPostUserName,
($this->hasArgument(self::ANONYMOUS) and $this->getArgument(self::ANONYMOUS))
? null
: Auth::getCurrentUser()->getName());
Logger::log('{user} featured {post} on main page', [ Logger::log('{user} featured {post} on main page', [
'user' => TextHelper::reprPost(Auth::getCurrentUser()), 'user' => TextHelper::reprPost(PropertyModel::get(PropertyModel::FeaturedPostUserName)),
'post' => TextHelper::reprPost($post)]); 'post' => TextHelper::reprPost($post)]);
return $post; return $post;

View file

@ -7,14 +7,14 @@ class StaticPagesController
$context->transport->postCount = PostModel::getCount(); $context->transport->postCount = PostModel::getCount();
$context->viewName = 'static-main'; $context->viewName = 'static-main';
$featuredPost = $this->getFeaturedPost(); PostModel::featureRandomPostIfNecessary();
$featuredPost = PostModel::getFeaturedPost();
if ($featuredPost) if ($featuredPost)
{ {
$context->featuredPost = $featuredPost; $context->featuredPost = $featuredPost;
$context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate); $context->featuredPostUnixTime = PropertyModel::get(PropertyModel::FeaturedPostUnixTime);
$context->featuredPostUser = UserModel::getByNameOrEmail( $context->featuredPostUser = UserModel::tryGetByNameOrEmail(
PropertyModel::get(PropertyModel::FeaturedPostUserName), PropertyModel::get(PropertyModel::FeaturedPostUserName));
false);
} }
} }
@ -34,24 +34,4 @@ class StaticPagesController
$context->path = TextHelper::absolutePath($config->help->paths[$tab]); $context->path = TextHelper::absolutePath($config->help->paths[$tab]);
$context->tab = $tab; $context->tab = $tab;
} }
private function getFeaturedPost()
{
$config = getConfig();
$featuredPostRotationTime = $config->misc->featuredPostMaxDays * 24 * 3600;
$featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId);
$featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
//check if too old
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
return PropertyModel::featureNewPost();
//check if post was deleted
$featuredPost = PostModel::tryGetById($featuredPostId);
if (!$featuredPost)
return PropertyModel::featureNewPost();
return $featuredPost;
}
} }

View file

@ -288,4 +288,59 @@ final class PostModel extends AbstractCrudModel
{ {
return TextHelper::absolutePath(getConfig()->main->filesPath . DS . $name); return TextHelper::absolutePath(getConfig()->main->filesPath . DS . $name);
} }
public static function getFeaturedPost()
{
$featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId);
if (!$featuredPostId)
return null;
return PostModel::tryGetById($featuredPostId);
}
public static function featureRandomPostIfNecessary()
{
$config = getConfig();
$featuredPostRotationTime = $config->misc->featuredPostMaxDays * 24 * 3600;
$featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId);
$featuredPostUnixTime = PropertyModel::get(PropertyModel::FeaturedPostUnixTime);
//check if too old
if (!$featuredPostId or $featuredPostUnixTime + $featuredPostRotationTime < time())
{
self::featureRandomPost();
return true;
}
//check if post was deleted
$featuredPost = PostModel::tryGetById($featuredPostId);
if (!$featuredPost)
{
self::featureRandomPost();
return true;
}
return false;
}
public static function featureRandomPost()
{
$stmt = (new Sql\SelectStatement)
->setColumn('id')
->setTable('post')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\NegationFunctor(new Sql\StringExpression('hidden')))
->add(new Sql\EqualsFunctor('type', new Sql\Binding(PostType::Image)))
->add(new Sql\EqualsFunctor('safety', new Sql\Binding(PostSafety::Safe))))
->setOrderBy(new Sql\RandomFunctor(), Sql\SelectStatement::ORDER_DESC);
$featuredPostId = Database::fetchOne($stmt)['id'];
if (!$featuredPostId)
return null;
PropertyModel::set(PropertyModel::FeaturedPostId, $featuredPostId);
PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, null);
}
} }

View file

@ -6,11 +6,17 @@ final class PropertyModel implements IModel
{ {
const FeaturedPostId = 0; const FeaturedPostId = 0;
const FeaturedPostUserName = 1; const FeaturedPostUserName = 1;
const FeaturedPostDate = 2; const FeaturedPostUnixTime = 2;
const DbVersion = 3; const DbVersion = 3;
static $allProperties = null; static $allProperties;
static $loaded = false; static $loaded;
public static function init()
{
self::$allProperties = null;
self::$loaded = false;
}
public static function getTableName() public static function getTableName()
{ {
@ -19,8 +25,9 @@ final class PropertyModel implements IModel
public static function loadIfNecessary() public static function loadIfNecessary()
{ {
if (!self::$loaded) if (self::$loaded)
{ return;
self::$loaded = true; self::$loaded = true;
self::$allProperties = []; self::$allProperties = [];
$stmt = new Sql\SelectStatement(); $stmt = new Sql\SelectStatement();
@ -29,7 +36,6 @@ final class PropertyModel implements IModel
foreach (Database::fetchAll($stmt) as $row) foreach (Database::fetchAll($stmt) as $row)
self::$allProperties[$row['prop_id']] = $row['value']; self::$allProperties[$row['prop_id']] = $row['value'];
} }
}
public static function get($propertyId) public static function get($propertyId)
{ {
@ -68,23 +74,4 @@ final class PropertyModel implements IModel
self::$allProperties[$propertyId] = $value; self::$allProperties[$propertyId] = $value;
}); });
} }
public static function featureNewPost()
{
$stmt = (new Sql\SelectStatement)
->setColumn('id')
->setTable('post')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('type', new Sql\Binding(PostType::Image)))
->add(new Sql\EqualsFunctor('safety', new Sql\Binding(PostSafety::Safe))))
->setOrderBy(new Sql\RandomFunctor(), Sql\SelectStatement::ORDER_DESC);
$featuredPostId = Database::fetchOne($stmt)['id'];
if (!$featuredPostId)
return null;
self::set(self::FeaturedPostId, $featuredPostId);
self::set(self::FeaturedPostDate, time());
self::set(self::FeaturedPostUserName, null);
return PostModel::getById($featuredPostId);
}
} }

View file

@ -48,7 +48,7 @@ Assets::addStylesheet('static-main.css');
</a>, </a>,
<?php endif ?> <?php endif ?>
<?php $x = round((time() - $this->context->featuredPostDate) / (24 * 3600.)) ?> <?php $x = round((time() - $this->context->featuredPostUnixTime) / (24 * 3600.)) ?>
<?php if ($x == 0): ?> <?php if ($x == 0): ?>
today today
<?php elseif ($x == 1):?> <?php elseif ($x == 1):?>

View file

@ -84,6 +84,7 @@ function prepareEnvironment($testEnvironment)
Access::init(); Access::init();
Logger::init(); Logger::init();
Mailer::init(); Mailer::init();
PropertyModel::init();
\Chibi\Database::connect( \Chibi\Database::connect(
$config->main->dbDriver, $config->main->dbDriver,

View file

@ -0,0 +1,49 @@
<?php
class FeaturePostJobTest extends AbstractTest
{
public function testFeaturing()
{
$this->grantAccess('featurePost');
$user = $this->mockUser();
$this->login($user);
$post1 = $this->mockPost($user);
$post2 = $this->mockPost($user);
$this->assert->doesNotThrow(function() use ($post2)
{
Api::run(
new FeaturePostJob(),
[
FeaturePostJob::POST_ID => $post2->getId()
]);
});
$this->assert->areEqual($post2->getId(), PropertyModel::get(PropertyModel::FeaturedPostId));
$this->assert->areEqual($user->getName(), PropertyModel::get(PropertyModel::FeaturedPostUserName));
$this->assert->isNotNull(PropertyModel::get(PropertyModel::FeaturedPostUnixTime));
}
public function testAnonymousFeaturing()
{
$this->grantAccess('featurePost');
$user = $this->mockUser();
$this->login($user);
$post1 = $this->mockPost($user);
$post2 = $this->mockPost($user);
$this->assert->doesNotThrow(function() use ($post2)
{
Api::run(
new FeaturePostJob(),
[
FeaturePostJob::POST_ID => $post2->getId(),
FeaturePostJob::ANONYMOUS => true,
]);
});
$this->assert->areEqual($post2->getId(), PropertyModel::get(PropertyModel::FeaturedPostId));
$this->assert->areEqual(null, PropertyModel::get(PropertyModel::FeaturedPostUserName));
}
}

View file

@ -0,0 +1,102 @@
<?php
class PostModelTest extends AbstractTest
{
public function testFeaturedPostRetrieval()
{
$post = $this->assert->doesNotThrow(function()
{
return PostModel::getFeaturedPost();
});
$this->assert->areEqual(null, $post);
}
public function testFeaturingNoPost()
{
PostModel::featureRandomPost();
$post = $this->assert->doesNotThrow(function()
{
return PostModel::getFeaturedPost();
});
$this->assert->areEqual(null, $post);
}
public function testFeaturingRandomPost()
{
$post = $this->mockPost(Auth::getCurrentUser());
PostModel::featureRandomPost();
$this->assert->areEqual($post->getId(), (int) PropertyModel::get(PropertyModel::FeaturedPostId));
}
public function testFeaturingIllegalPosts()
{
$posts = [];
foreach (range(0, 5) as $i)
$posts []= $this->mockPost(Auth::getCurrentUser());
$posts[0]->setSafety(new PostSafety(PostSafety::Sketchy));
$posts[1]->setSafety(new PostSafety(PostSafety::Sketchy));
$posts[2]->setHidden(true);
$posts[3]->setType(new PostType(PostType::Youtube));
$posts[4]->setType(new PostType(PostType::Flash));
$posts[5]->setType(new PostType(PostType::Video));
foreach ($posts as $post)
PostModel::save($post);
PostModel::featureRandomPost();
$this->assert->areEqual(null, PropertyModel::get(PropertyModel::FeaturedPostId));
}
public function testAutoFeaturingFirstTime()
{
$this->mockPost(Auth::getCurrentUser());
$this->assert->doesNotThrow(function()
{
PostModel::featureRandomPostIfNecessary();
});
$this->assert->isNotNull(PostModel::getFeaturedPost());
}
public function testAutoFeaturingTooSoon()
{
$this->mockPost(Auth::getCurrentUser());
$this->assert->isTrue(PostModel::featureRandomPostIfNecessary());
$this->assert->isFalse(PostModel::featureRandomPostIfNecessary());
$this->assert->isNotNull(PostModel::getFeaturedPost());
}
public function testAutoFeaturingOutdated()
{
$post = $this->mockPost(Auth::getCurrentUser());
$minTimestamp = getConfig()->misc->featuredPostMaxDays * 24 * 3600;
$this->assert->isTrue(PostModel::featureRandomPostIfNecessary());
PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time() - $minTimestamp - 1);
$this->assert->isTrue(PostModel::featureRandomPostIfNecessary());
PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time() - $minTimestamp + 1);
$this->assert->isFalse(PostModel::featureRandomPostIfNecessary());
$this->assert->isNotNull(PostModel::getFeaturedPost());
}
public function testAutoFeaturingDeletedPost()
{
$post = $this->mockPost(Auth::getCurrentUser());
$this->assert->isTrue(PostModel::featureRandomPostIfNecessary());
$this->assert->isNotNull(PostModel::getFeaturedPost());
PostModel::remove($post);
$anotherPost = $this->mockPost(Auth::getCurrentUser());
$this->assert->isTrue(PostModel::featureRandomPostIfNecessary());
$this->assert->isNotNull(PostModel::getFeaturedPost());
}
}

View file

@ -0,0 +1,21 @@
<?php
class PropertyModelTest extends AbstractTest
{
public function testGetAndSet()
{
$this->assert->doesNotThrow(function()
{
PropertyModel::set(PropertyModel::FeaturedPostId, 100);
});
$this->assert->areEqual(100, PropertyModel::get(PropertyModel::FeaturedPostId));
}
public function testGetAndSetWithArbitraryKeys()
{
$this->assert->doesNotThrow(function()
{
PropertyModel::set('something', 100);
});
$this->assert->areEqual(100, PropertyModel::get('something'));
}
}