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
class FeaturePostJob extends AbstractPostJob
{
const ANONYMOUS = 'anonymous';
public function execute()
{
$post = $this->post;
PropertyModel::set(PropertyModel::FeaturedPostId, $post->getId());
PropertyModel::set(PropertyModel::FeaturedPostDate, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, Auth::getCurrentUser()->getName());
PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time());
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', [
'user' => TextHelper::reprPost(Auth::getCurrentUser()),
'user' => TextHelper::reprPost(PropertyModel::get(PropertyModel::FeaturedPostUserName)),
'post' => TextHelper::reprPost($post)]);
return $post;

View file

@ -7,14 +7,14 @@ class StaticPagesController
$context->transport->postCount = PostModel::getCount();
$context->viewName = 'static-main';
$featuredPost = $this->getFeaturedPost();
PostModel::featureRandomPostIfNecessary();
$featuredPost = PostModel::getFeaturedPost();
if ($featuredPost)
{
$context->featuredPost = $featuredPost;
$context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
$context->featuredPostUser = UserModel::getByNameOrEmail(
PropertyModel::get(PropertyModel::FeaturedPostUserName),
false);
$context->featuredPostUnixTime = PropertyModel::get(PropertyModel::FeaturedPostUnixTime);
$context->featuredPostUser = UserModel::tryGetByNameOrEmail(
PropertyModel::get(PropertyModel::FeaturedPostUserName));
}
}
@ -34,24 +34,4 @@ class StaticPagesController
$context->path = TextHelper::absolutePath($config->help->paths[$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);
}
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 FeaturedPostUserName = 1;
const FeaturedPostDate = 2;
const FeaturedPostUnixTime = 2;
const DbVersion = 3;
static $allProperties = null;
static $loaded = false;
static $allProperties;
static $loaded;
public static function init()
{
self::$allProperties = null;
self::$loaded = false;
}
public static function getTableName()
{
@ -19,16 +25,16 @@ final class PropertyModel implements IModel
public static function loadIfNecessary()
{
if (!self::$loaded)
{
self::$loaded = true;
self::$allProperties = [];
$stmt = new Sql\SelectStatement();
$stmt ->setColumn('*');
$stmt ->setTable('property');
foreach (Database::fetchAll($stmt) as $row)
self::$allProperties[$row['prop_id']] = $row['value'];
}
if (self::$loaded)
return;
self::$loaded = true;
self::$allProperties = [];
$stmt = new Sql\SelectStatement();
$stmt ->setColumn('*');
$stmt ->setTable('property');
foreach (Database::fetchAll($stmt) as $row)
self::$allProperties[$row['prop_id']] = $row['value'];
}
public static function get($propertyId)
@ -68,23 +74,4 @@ final class PropertyModel implements IModel
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>,
<?php endif ?>
<?php $x = round((time() - $this->context->featuredPostDate) / (24 * 3600.)) ?>
<?php $x = round((time() - $this->context->featuredPostUnixTime) / (24 * 3600.)) ?>
<?php if ($x == 0): ?>
today
<?php elseif ($x == 1):?>

View file

@ -84,6 +84,7 @@ function prepareEnvironment($testEnvironment)
Access::init();
Logger::init();
Mailer::init();
PropertyModel::init();
\Chibi\Database::connect(
$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'));
}
}