Rewrote the news backend.

This commit is contained in:
Pachira 2023-07-15 17:02:46 +00:00
parent 69c6b6f2ac
commit bff42c26ab
25 changed files with 1113 additions and 634 deletions

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsCategory;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -11,16 +10,17 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->
return;
}
$categoriesPagination = new Pagination(NewsCategory::countAll(true), 15);
$news = $msz->getNews();
$pagination = new Pagination($news->countAllCategories(true), 15);
if(!$categoriesPagination->hasValidOffset()) {
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
$categories = NewsCategory::all($categoriesPagination, true);
$categories = $news->getAllCategories(true, $pagination);
Template::render('manage.news.categories', [
'news_categories' => $categories,
'categories_pagination' => $categoriesPagination,
'categories_pagination' => $pagination,
]);

View file

@ -1,9 +1,8 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\AuditLog;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsCategoryNotFoundException;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -13,39 +12,62 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->
return;
}
$categoryId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$news = $msz->getNews();
$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$loadCategoryInfo = fn() => $news->getCategoryById($categoryId);
if($categoryId > 0)
if(empty($categoryId))
$isNew = true;
else
try {
$categoryInfo = NewsCategory::byId($categoryId);
Template::set('category_info', $categoryInfo);
} catch(NewsCategoryNotFoundException $ex) {
$isNew = false;
$categoryInfo = $loadCategoryInfo();
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
if(!empty($_POST['category']) && CSRF::validateRequest()) {
if(!isset($categoryInfo)) {
$categoryInfo = new NewsCategory;
$isNew = true;
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) {
$news->deleteCategory($categoryInfo);
AuditLog::create(AuditLog::NEWS_CATEGORY_DELETE, [$categoryInfo->getId()]);
url_redirect('manage-news-categories');
} else render_error(403);
return;
}
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$name = trim((string)filter_input(INPUT_POST, 'nc_name'));
$description = trim((string)filter_input(INPUT_POST, 'nc_desc'));
$hidden = !empty($_POST['nc_hidden']);
if($isNew) {
$categoryInfo = $news->createCategory($name, $description, $hidden);
} else {
if($name === $categoryInfo->getName())
$name = null;
if($description === $categoryInfo->getDescription())
$description = null;
if($hidden === $categoryInfo->isHidden())
$hidden = null;
if($name !== null || $description !== null || $hidden !== null)
$news->updateCategory($categoryInfo, $name, $description, $hidden);
}
$categoryInfo->setName($_POST['category']['name'])
->setDescription($_POST['category']['description'])
->setHidden(!empty($_POST['category']['hidden']))
->save();
AuditLog::create(
empty($isNew)
? AuditLog::NEWS_CATEGORY_EDIT
: AuditLog::NEWS_CATEGORY_CREATE,
$isNew ? AuditLog::NEWS_CATEGORY_CREATE : AuditLog::NEWS_CATEGORY_EDIT,
[$categoryInfo->getId()]
);
if(!empty($isNew)) {
header('Location: ' . url('manage-news-category', ['category' => $categoryInfo->getId()]));
if($isNew) {
url_redirect('manage-news-category', ['category' => $categoryInfo->getId()]);
return;
}
} else $categoryInfo = $loadCategoryInfo();
break;
}
Template::render('manage.news.category');
Template::render('manage.news.category', [
'category_new' => $isNew,
'category_info' => $categoryInfo ?? null,
]);

View file

@ -1,10 +1,8 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\AuditLog;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsPost;
use Misuzu\News\NewsPostNotFoundException;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -14,52 +12,74 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->
return;
}
$postId = (int)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
if($postId > 0)
$news = $msz->getNews();
$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
$loadPostInfo = fn() => $news->getPostById($postId);
if(empty($postId))
$isNew = true;
else
try {
$postInfo = NewsPost::byId($postId);
Template::set('post_info', $postInfo);
} catch(NewsPostNotFoundException $ex) {
$isNew = false;
$postInfo = $loadPostInfo();
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
$categories = NewsCategory::all(null, true);
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) {
$news->deletePost($postInfo);
AuditLog::create(AuditLog::NEWS_POST_DELETE, [$postInfo->getId()]);
url_redirect('manage-news-posts');
} else render_error(403);
return;
}
if(!empty($_POST['post']) && CSRF::validateRequest()) {
if(!isset($postInfo)) {
$postInfo = new NewsPost;
$isNew = true;
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$title = trim((string)filter_input(INPUT_POST, 'np_title'));
$category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT);
$featured = !empty($_POST['np_featured']);
$body = trim((string)filter_input(INPUT_POST, 'np_body'));
if($isNew) {
$postInfo = $news->createPost($category, $title, $body, $featured, User::getCurrent());
} else {
if($category === $postInfo->getCategoryId())
$category = null;
if($title === $postInfo->getTitle())
$title = null;
if($body === $postInfo->getBody())
$body = null;
if($featured === $postInfo->isFeatured())
$featured = null;
if($category !== null || $title !== null || $body !== null || $featured !== null)
$news->updatePost($postInfo, $category, $title, $body, $featured);
}
$currentUserId = User::getCurrent()->getId();
$postInfo->setTitle( $_POST['post']['title'])
->setText($_POST['post']['text'])
->setCategoryId($_POST['post']['category'])
->setFeatured(!empty($_POST['post']['featured']));
if(!empty($isNew))
$postInfo->setUserId($currentUserId);
$postInfo->save();
AuditLog::create(
empty($isNew)
? AuditLog::NEWS_POST_EDIT
: AuditLog::NEWS_POST_CREATE,
$isNew ? AuditLog::NEWS_POST_CREATE : AuditLog::NEWS_POST_EDIT,
[$postInfo->getId()]
);
if(!empty($isNew)) {
if($isNew) {
if($postInfo->isFeatured()) {
// Twitter integration used to be here, replace with Railgun Pulse integration
}
header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()]));
url_redirect('manage-news-post', ['post' => $postInfo->getId()]);
return;
}
} else $postInfo = $loadPostInfo();
break;
}
$categories = [];
foreach($news->getAllCategories(true) as $categoryInfo)
$categories[$categoryInfo->getId()] = $categoryInfo->getName();
Template::render('manage.news.post', [
'categories' => $categories,
'post_new' => $isNew,
'post_info' => $postInfo ?? null,
]);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsPost;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -11,16 +10,24 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->
return;
}
$postsPagination = new Pagination(NewsPost::countAll(false, true, true), 15);
$news = $msz->getNews();
$pagination = new Pagination($news->countAllPosts(
includeScheduled: true,
includeDeleted: true
), 15);
if(!$postsPagination->hasValidOffset()) {
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
$posts = NewsPost::all($postsPagination, false, true, true);
$posts = $news->getAllPosts(
includeScheduled: true,
includeDeleted: true,
pagination: $pagination
);
Template::render('manage.news.posts', [
'news_posts' => $posts,
'posts_pagination' => $postsPagination,
'posts_pagination' => $pagination,
]);

View file

@ -1,7 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsPost;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
require_once '../misuzu.php';
@ -11,7 +12,48 @@ $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
if(!empty($searchQuery)) {
$forumTopics = forum_topic_listing_search($searchQuery, User::hasCurrent() ? User::getCurrent()->getId() : 0);
$forumPosts = forum_post_search($searchQuery);
$newsPosts = NewsPost::bySearchQuery($searchQuery);
// this sure is an expansion
$news = $msz->getNews();
$newsPosts = [];
$newsPostInfos = $news->getPostsBySearchQuery($searchQuery);
$newsUserInfos = [];
$newsCategoryInfos = [];
foreach($newsPostInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $newsUserInfos)) {
$userInfo = $newsUserInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$newsUserInfos[$userId] = $userInfo;
}
if(array_key_exists($categoryId, $newsCategoryInfos))
$categoryInfo = $newsCategoryInfos[$categoryId];
else
$newsCategoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$newsPosts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'comments_count' => $commentsCount,
];
}
$findUsers = DB::prepare('
SELECT u.`user_id`, u.`username`, u.`user_country`,

View file

@ -31,8 +31,10 @@ class AuditLog {
public const NEWS_POST_CREATE = 'NEWS_POST_CREATE';
public const NEWS_POST_EDIT = 'NEWS_POST_EDIT';
public const NEWS_POST_DELETE = 'NEWS_POST_DELETE';
public const NEWS_CATEGORY_CREATE = 'NEWS_CATEGORY_CREATE';
public const NEWS_CATEGORY_EDIT = 'NEWS_CATEGORY_EDIT';
public const NEWS_CATEGORY_DELETE = 'NEWS_CATEGORY_DELETE';
public const FORUM_TOPIC_DELETE = 'FORUM_TOPIC_DELETE';
public const FORUM_TOPIC_RESTORE = 'FORUM_TOPIC_RESTORE';
@ -83,8 +85,10 @@ class AuditLog {
self::NEWS_POST_CREATE => 'Created news post #%d.',
self::NEWS_POST_EDIT => 'Edited news post #%d.',
self::NEWS_POST_DELETE => 'Deleted news post #%d.',
self::NEWS_CATEGORY_CREATE => 'Created news category #%d.',
self::NEWS_CATEGORY_EDIT => 'Edited news category #%d.',
self::NEWS_CATEGORY_DELETE => 'Deleted news category #%d.',
self::FORUM_POST_EDIT => 'Edited forum post #%d.',
self::FORUM_POST_DELETE => 'Deleted forum post #%d.',

View file

@ -6,9 +6,11 @@ use Misuzu\Config\IConfig;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\News\NewsPost;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserNotFoundException;
final class HomeHandler extends Handler {
public function index($response, $request): void {
@ -27,8 +29,10 @@ final class HomeHandler extends Handler {
'same_as' => Config::get('social.linked', IConfig::T_ARR),
] : null;
$featuredNews = NewsPost::all(new Pagination(3), true);
$featuredNews = $this->context->getNews()->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(3)
);
$stats = DB::query(
'SELECT'
@ -102,7 +106,49 @@ final class HomeHandler extends Handler {
}
public function home($response, $request): void {
$featuredNews = NewsPost::all(new Pagination(5), true);
$news = $this->context->getNews();
$featuredNews = [];
$userInfos = [];
$categoryInfos = [];
$featuredNewsInfos = $news->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(5)
);
foreach($featuredNewsInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
if(array_key_exists($categoryId, $categoryInfos))
$categoryInfo = $categoryInfos[$categoryId];
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$featuredNews[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'comments_count' => $commentsCount,
];
}
$stats = DB::query(
'SELECT'

View file

@ -1,44 +1,95 @@
<?php
namespace Misuzu\Http\Handlers;
use RuntimeException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Config\IConfig;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsPost;
use Misuzu\News\NewsCategoryNotFoundException;
use Misuzu\News\NewsPostNotFoundException;
use Misuzu\News\NewsCategoryInfo;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
final class NewsHandler extends Handler {
public function index($response, $request) {
$categories = NewsCategory::all();
$newsPagination = new Pagination(NewsPost::countAll(true), 5);
private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array {
$news = $this->context->getNews();
$posts = [];
$userInfos = [];
if(!$newsPagination->hasValidOffset())
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
if(array_key_exists($categoryId, $categoryInfos))
$categoryInfo = $categoryInfos[$categoryId];
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$posts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'comments_count' => $commentsCount,
];
}
return $posts;
}
public function index($response, $request) {
$news = $this->context->getNews();
$categories = $news->getAllCategories();
$pagination = new Pagination($news->countAllPosts(onlyFeatured: true), 5);
if(!$pagination->hasValidOffset())
return 404;
$postInfos = $news->getAllPosts(onlyFeatured: true, pagination: $pagination);
$posts = $this->fetchPostInfo($postInfos);
$response->setContent(Template::renderRaw('news.index', [
'categories' => $categories,
'posts' => NewsPost::all($newsPagination, true),
'news_pagination' => $newsPagination,
'news_categories' => $categories,
'news_posts' => $posts,
'news_pagination' => $pagination,
]));
}
public function viewCategory($response, $request, string $fileName) {
$categoryId = (int)pathinfo($fileName, PATHINFO_FILENAME);
$news = $this->context->getNews();
$categoryId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
try {
$categoryInfo = NewsCategory::byId($categoryId);
} catch(NewsCategoryNotFoundException $ex) {
$categoryInfo = $news->getCategoryById($categoryId);
} catch(RuntimeException $ex) {
return 404;
}
@ -49,41 +100,64 @@ final class NewsHandler extends Handler {
elseif($type !== '')
return 404;
$categoryPagination = new Pagination(NewsPost::countByCategory($categoryInfo), 5);
if(!$categoryPagination->hasValidOffset())
$pagination = new Pagination($news->countPostsByCategory($categoryInfo), 5);
if(!$pagination->hasValidOffset())
return 404;
$postInfos = $news->getPostsByCategory($categoryInfo, pagination: $pagination);
$posts = $this->fetchPostInfo($postInfos, [$categoryInfo->getId() => $categoryInfo]);
$response->setContent(Template::renderRaw('news.category', [
'category_info' => $categoryInfo,
'posts' => $categoryInfo->posts($categoryPagination),
'news_pagination' => $categoryPagination,
'news_category' => $categoryInfo,
'news_posts' => $posts,
'news_pagination' => $pagination,
]));
}
public function viewPost($response, $request, int $postId) {
public function viewPost($response, $request, string $postId) {
$news = $this->context->getNews();
try {
$postInfo = NewsPost::byId($postId);
} catch(NewsPostNotFoundException $ex) {
$postInfo = $news->getPostById($postId);
} catch(RuntimeException $ex) {
return 404;
}
if(!$postInfo->isPublished() || $postInfo->isDeleted())
return 404;
$postInfo->ensureCommentsCategory();
$commentsInfo = $postInfo->getCommentsCategory();
$categoryInfo = $news->getCategoryByPost($postInfo);
if($postInfo->hasCommentsCategoryId()) {
$commentsCategory = CommentsCategory::byId($postInfo->getCommentsCategoryId());
} else {
$commentsCategoryName = $postInfo->getCommentsCategoryName();
try {
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$commentsCategory = new CommentsCategory($commentsCategoryName);
$commentsCategory->save();
$news->updatePostCommentCategory($postInfo, $commentsCategory);
}
}
$userInfo = null;
if($postInfo->hasUserId())
try {
$userInfo = User::byId($postInfo->getUserId());
} catch(UserNotFoundException $ex) {}
$response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo,
'comments_info' => $commentsInfo,
'comments_user' => User::getCurrent(),
'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo,
'comments_info' => $commentsCategory,
'comments_user' => User::getCurrent(),
]));
}
private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed {
$hasCategory = !empty($categoryInfo);
$pagination = new Pagination(10);
$posts = $hasCategory ? $categoryInfo->posts($pagination) : NewsPost::all($pagination, true);
private function createFeed(string $feedMode, ?NewsCategoryInfo $categoryInfo, array $posts): Feed {
$hasCategory = $categoryInfo !== null;
$feed = (new Feed)
->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
@ -92,19 +166,29 @@ final class NewsHandler extends Handler {
->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedMode}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedMode}")));
foreach($posts as $post) {
$postUrl = url_prefix(false) . url('news-post', ['post' => $post->getId()]);
$commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $post->getId()]);
$authorUrl = url_prefix(false) . url('user-profile', ['user' => $post->getUser()->getId()]);
$postInfo = $post['post'];
$userInfo = $post['user'];
$userId = 0;
$userName = 'Author';
if($userInfo !== null) {
$userId = $userInfo->getId();
$userName = $userInfo->getUsername();
}
$postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]);
$commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $postInfo->getId()]);
$authorUrl = url_prefix(false) . url('user-profile', ['user' => $userId]);
$feedItem = (new FeedItem)
->setTitle($post->getTitle())
->setSummary($post->getFirstParagraph())
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post->getText()))
->setCreationDate($post->getCreatedTime())
->setTitle($postInfo->getTitle())
->setSummary($postInfo->getFirstParagraph())
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()))
->setCreationDate($postInfo->getCreatedTime())
->setUniqueId($postUrl)
->setContentUrl($postUrl)
->setCommentsUrl($commentsUrl)
->setAuthorName($post->getUser()->getUsername())
->setAuthorName($userName)
->setAuthorUrl($authorUrl);
if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate())
@ -116,31 +200,75 @@ final class NewsHandler extends Handler {
return $feed;
}
private function fetchPostInfoForFeed(array $postInfos): array {
$news = $this->context->getNews();
$posts = [];
$userInfos = [];
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
$posts[] = [
'post' => $postInfo,
'user' => $userInfo,
];
}
return $posts;
}
private function getFeaturedPostsForFeed(): array {
return $this->fetchPostInfoForFeed(
$this->context->getNews()->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(10)
)
);
}
public function feedIndexAtom($response, $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', null, NewsPost::all(new Pagination(10), true))
self::createFeed('atom', null, $this->getFeaturedPostsForFeed())
);
}
public function feedIndexRss($response, $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', null, NewsPost::all(new Pagination(10), true))
self::createFeed('rss', null, $this->getFeaturedPostsForFeed())
);
}
public function feedCategoryAtom($response, $request, NewsCategory $categoryInfo) {
private function getCategoryPostsForFeed(NewsCategoryInfo $categoryInfo): array {
return $this->fetchPostInfoForFeed(
$this->context->getNews()->getPostsByCategory($categoryInfo, pagination: new Pagination(10))
);
}
public function feedCategoryAtom($response, $request, NewsCategoryInfo $categoryInfo) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
self::createFeed('atom', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo))
);
}
public function feedCategoryRss($response, $request, NewsCategory $categoryInfo) {
public function feedCategoryRss($response, $request, NewsCategoryInfo $categoryInfo) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
self::createFeed('rss', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo))
);
}
}

View file

@ -5,6 +5,7 @@ use Misuzu\Template;
use Misuzu\Changelog\Changelog;
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;
use Misuzu\News\News;
use Misuzu\SharpChat\SharpChatRoutes;
use Misuzu\Users\Users;
use Index\Data\IDbConnection;
@ -25,6 +26,7 @@ class MisuzuContext {
private HttpFx $router;
private Emotes $emotes;
private Changelog $changelog;
private News $news;
public function __construct(IDbConnection $dbConn, IConfig $config) {
$this->dbConn = $dbConn;
@ -32,6 +34,7 @@ class MisuzuContext {
$this->users = new Users($this->dbConn);
$this->emotes = new Emotes($this->dbConn);
$this->changelog = new Changelog($this->dbConn);
$this->news = new News($this->dbConn);
}
public function getDbConn(): IDbConnection {
@ -71,6 +74,10 @@ class MisuzuContext {
return $this->changelog;
}
public function getNews(): News {
return $this->news;
}
public function setUpHttp(bool $legacy = false): void {
$this->router = new HttpFx;
$this->router->use('/', function($response) {

481
src/News/News.php Normal file
View file

@ -0,0 +1,481 @@
<?php
namespace Misuzu\News;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Users\User;
class News {
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
private function readCategories(IDbResult $result): array {
$categories = [];
while($result->next())
$categories[] = new NewsCategoryInfo($result);
return $categories;
}
private function readPosts(IDbResult $result): array {
$posts = [];
while($result->next())
$posts[] = new NewsPostInfo($result);
return $posts;
}
public function countAllCategories(bool $includeHidden = false): int {
$query = 'SELECT COUNT(*) FROM msz_news_categories';
if($includeHidden)
$query .= ' WHERE category_is_hidden = 0';
$result = $this->dbConn->query($query);
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getAllCategories(
bool $includeHidden = false,
?Pagination $pagination = null
): array {
$hasPagination = $pagination !== null;
$query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created), (SELECT COUNT(*) FROM msz_news_posts AS np WHERE np.category_id = nc.category_id) AS category_posts_count FROM msz_news_categories AS nc';
if(!$includeHidden)
$query .= ' WHERE category_is_hidden = 0';
$query .= ' ORDER BY category_created ASC';
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$stmt = $this->cache->get($query);
$args = 0;
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return self::readCategories($stmt->getResult());
}
public function getCategoryByPost(NewsPostInfo|string $postInfo): NewsCategoryInfo {
$query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created) FROM msz_news_categories WHERE category_id = ';
if($postInfo instanceof NewsPostInfo) {
$query .= '?';
$param = $postInfo->getCategoryId();
} else {
$query .= '(SELECT category_id FROM msz_news_posts WHERE post_id = ?)';
$param = $postInfo;
}
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $param);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No news category associated with that ID exists.');
return new NewsCategoryInfo($result);
}
public function getCategoryById(string $categoryId): NewsCategoryInfo {
$stmt = $this->cache->get('SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created) FROM msz_news_categories WHERE category_id = ?');
$stmt->addParameter(1, $categoryId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No news category with that ID exists.');
return new NewsCategoryInfo($result);
}
public function createCategory(
string $name,
string $description,
bool $hidden
): NewsCategoryInfo {
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty');
$description = trim($description);
if(empty($description))
throw new InvalidArgumentException('$description may not be empty');
$stmt = $this->cache->get('INSERT INTO msz_news_categories (category_name, category_description, category_is_hidden) VALUES (?, ?, ?)');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $description);
$stmt->addParameter(3, $hidden ? 1 : 0);
$stmt->execute();
return $this->getCategoryById((string)$this->dbConn->getLastInsertId());
}
public function deleteCategory(NewsCategoryInfo|string $infoOrId): void {
if($infoOrId instanceof NewsCategoryInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('DELETE FROM msz_news_categories WHERE category_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function updateCategory(
NewsCategoryInfo|string $infoOrId,
?string $name = null,
?string $description = null,
?bool $hidden = null
): void {
if($infoOrId instanceof NewsCategoryInfo)
$infoOrId = $infoOrId->getId();
if($name !== null) {
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty');
}
if($description !== null) {
$description = trim($description);
if(empty($description))
throw new InvalidArgumentException('$description may not be empty');
}
$hasHidden = $hidden !== null;
$stmt = $this->cache->get('UPDATE msz_news_categories SET category_name = COALESCE(?, category_name), category_description = COALESCE(?, category_description), category_is_hidden = IF(?, ?, category_is_hidden) WHERE category_id = ?');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $description);
$stmt->addParameter(3, $hasHidden ? 1 : 0);
$stmt->addParameter(4, $hidden ? 1 : 0);
$stmt->addParameter(5, $infoOrId);
$stmt->execute();
}
public function countAllPosts(
bool $onlyFeatured = false,
bool $includeScheduled = false,
bool $includeDeleted = false
): int {
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_news_posts';
if($onlyFeatured) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_is_featured = 1';
}
if(!$includeScheduled) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_scheduled <= NOW()';
}
if(!$includeDeleted) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_deleted IS NULL';
}
$result = $this->dbConn->query($query);
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function countPostsByCategory(
NewsCategoryInfo|string $categoryInfo,
bool $onlyFeatured = false,
bool $includeScheduled = false,
bool $includeDeleted = false
): int {
if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId();
$query = 'SELECT COUNT(*) FROM msz_news_posts WHERE category_id = ?';
if($onlyFeatured)
$query .= ' AND post_is_featured = 1';
if(!$includeScheduled)
$query .= ' AND post_scheduled <= NOW()';
if(!$includeDeleted)
$query .= ' AND post_deleted IS NULL';
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $categoryInfo);
$stmt->execute();
$result = $stmt->getResult();
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
private const POSTS_SELECT_QUERY = 'SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts';
private const POSTS_SELECT_ORDER = ' ORDER BY post_scheduled DESC';
public function getAllPosts(
bool $onlyFeatured = false,
bool $includeScheduled = false,
bool $includeDeleted = false,
?Pagination $pagination = null
): array {
$args = 0;
$hasPagination = $pagination !== null;
$query = self::POSTS_SELECT_QUERY;
if($onlyFeatured) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_is_featured = 1';
}
if(!$includeScheduled) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_scheduled <= NOW()';
}
if(!$includeDeleted) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' post_deleted IS NULL';
}
$query .= self::POSTS_SELECT_ORDER;
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$stmt = $this->cache->get($query);
$args = 0;
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return self::readPosts($stmt->getResult());
}
public function getPostsByCategory(
NewsCategoryInfo|string $categoryInfo,
bool $onlyFeatured = false,
bool $includeScheduled = false,
bool $includeDeleted = false,
?Pagination $pagination = null
): array {
if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId();
$hasPagination = $pagination !== null;
$query = self::POSTS_SELECT_QUERY;
$query .= ' WHERE category_id = ?';
if($onlyFeatured)
$query .= ' AND post_is_featured = 1';
if(!$includeScheduled)
$query .= ' AND post_scheduled <= NOW()';
if(!$includeDeleted)
$query .= ' AND post_deleted IS NULL';
$query .= self::POSTS_SELECT_ORDER;
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$stmt = $this->cache->get($query);
$args = 0;
$stmt->addParameter(++$args, $categoryInfo);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return self::readPosts($stmt->getResult());
}
public function getPostsBySearchQuery(
string $searchQuery,
bool $includeScheduled = false,
bool $includeDeleted = false,
?Pagination $pagination = null
): array {
$hasPagination = $pagination !== null;
$query = self::POSTS_SELECT_QUERY;
$query .= ' WHERE MATCH(post_title, post_text) AGAINST (? IN NATURAL LANGUAGE MODE)';
if(!$includeScheduled)
$query .= ' AND post_scheduled <= NOW()';
if(!$includeDeleted)
$query .= ' AND post_deleted IS NULL';
$query .= self::POSTS_SELECT_ORDER;
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$stmt = $this->cache->get($query);
$args = 0;
$stmt->addParameter(++$args, $searchQuery);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return self::readPosts($stmt->getResult());
}
public function getPostById(string $postId): NewsPostInfo {
$stmt = $this->cache->get('SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts WHERE post_id = ?');
$stmt->addParameter(1, $postId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No news post with that ID exists.');
return new NewsPostInfo($result);
}
public function createPost(
NewsCategoryInfo|string $categoryInfo,
string $title,
string $body,
bool $featured = false,
User|string|null $userInfo = null,
DateTime|int|null $schedule = null
): NewsPostInfo {
if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId();
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($schedule instanceof DateTime)
$schedule = $schedule->getUnixTimeSeconds();
$title = trim($title);
if(empty($title))
throw new InvalidArgumentException('$title may not be empty');
$body = trim($body);
if(empty($body))
throw new InvalidArgumentException('$body may not be empty');
$stmt = $this->cache->get('INSERT INTO msz_news_posts (category_id, user_id, post_is_featured, post_title, post_text, post_scheduled) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->addParameter(1, $categoryInfo);
$stmt->addParameter(2, $userInfo);
$stmt->addParameter(3, $featured ? 1 : 0);
$stmt->addParameter(4, $title);
$stmt->addParameter(5, $body);
$stmt->addParameter(6, $schedule);
$stmt->execute();
return $this->getPostById((string)$this->dbConn->getLastInsertId());
}
public function deletePost(NewsPostInfo|string $postInfo): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
$stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?');
$stmt->addParameter(1, $postInfo);
$stmt->execute();
}
public function restorePost(NewsPostInfo|string $postInfo): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
$stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = NULL WHERE post_id = ?');
$stmt->addParameter(1, $postInfo);
$stmt->execute();
}
public function nukePost(NewsPostInfo|string $postInfo): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
// should this enforce a soft delete first? (AND post_deleted IS NOT NULL)
$stmt = $this->cache->get('DELETE FROM msz_news_posts WHERE post_id = ?');
$stmt->addParameter(1, $postInfo);
$stmt->execute();
}
public function updatePost(
NewsPostInfo|string $postInfo,
NewsCategoryInfo|string|null $categoryInfo = null,
?string $title = null,
?string $body = null,
?bool $featured = null,
bool $updateUserInfo = false,
User|string|null $userInfo = null,
DateTime|int|null $schedule = null
): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId();
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($schedule instanceof DateTime)
$schedule = $schedule->getUnixTimeSeconds();
if($title !== null) {
$title = trim($title);
if(empty($title))
throw new InvalidArgumentException('$title may not be empty');
}
if($body !== null) {
$body = trim($body);
if(empty($body))
throw new InvalidArgumentException('$body may not be empty');
}
$hasFeatured = $featured !== null;
$stmt = $this->cache->get('UPDATE msz_news_posts SET category_id = COALESCE(?, category_id), user_id = IF(?, ?, user_id), post_is_featured = IF(?, ?, post_is_featured), post_title = COALESCE(?, post_title), post_text = COALESCE(?, post_text), post_scheduled = COALESCE(?, post_scheduled) WHERE post_id = ?');
$stmt->addParameter(1, $categoryInfo);
$stmt->addParameter(2, $updateUserInfo ? 1 : 0);
$stmt->addParameter(3, $userInfo);
$stmt->addParameter(4, $hasFeatured ? 1 : 0);
$stmt->addParameter(5, $featured ? 1 : 0);
$stmt->addParameter(6, $title);
$stmt->addParameter(7, $body);
$stmt->addParameter(8, $schedule);
$stmt->addParameter(9, $postInfo);
$stmt->execute();
}
public function updatePostCommentCategory(
NewsPostInfo|string $postInfo,
CommentsCategory|string $commentsCategory
): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
if($commentsCategory instanceof CommentsCategory)
$commentsCategory = (string)$commentsCategory->getId();
// "post_updated = post_updated" is an Attempt at making this not bump post_updated ON UPDATE
$stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ?, post_updated = post_updated WHERE post_id = ?');
$stmt->addParameter(1, $postInfo);
$stmt->addParameter(2, $commentsCategory);
$stmt->execute();
}
}

View file

@ -1,148 +0,0 @@
<?php
namespace Misuzu\News;
use ArrayAccess;
use Misuzu\DB;
use Misuzu\Pagination;
class NewsCategoryException extends NewsException {};
class NewsCategoryNotFoundException extends NewsCategoryException {};
class NewsCategory implements ArrayAccess {
// Database fields
private $category_id = -1;
private $category_name = '';
private $category_description = '';
private $category_is_hidden = false;
private $category_created = null;
private $postCount = -1;
public const TABLE = 'news_categories';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`, %1$s.`category_description`, %1$s.`category_is_hidden`'
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`';
public function __construct() {}
public function getId(): int {
return $this->category_id < 1 ? -1 : $this->category_id;
}
public function getName(): string {
return $this->category_name ?? '';
}
public function setName(string $name): self {
$this->category_name = $name;
return $this;
}
public function getDescription(): string {
return $this->category_description ?? '';
}
public function setDescription(string $description): self {
$this->category_description = $description;
return $this;
}
public function isHidden(): bool {
return $this->category_is_hidden !== 0;
}
public function setHidden(bool $hide): self {
$this->category_is_hidden = $hide ? 1 : 0;
return $this;
}
public function getCreatedTime(): int {
return $this->category_created === null ? -1 : $this->category_created;
}
// Purely cosmetic, use ::countAll for pagination
public function getPostCount(): int {
if($this->postCount < 0)
$this->postCount = (int)DB::prepare('
SELECT COUNT(`post_id`)
FROM `msz_news_posts`
WHERE `category_id` = :cat_id
AND `post_scheduled` <= NOW()
AND `post_deleted` IS NULL
')->bind('cat_id', $this->getId())->fetchColumn();
return $this->postCount;
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`category_name`, `category_description`, `category_is_hidden`) VALUES'
. ' (:name, :description, :hidden)';
} else {
$query = 'UPDATE `%1$s%2$s` SET `category_name` = :name, `category_description` = :description, `category_is_hidden` = :hidden'
. ' WHERE `category_id` = :category';
}
$savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('name', $this->category_name)
->bind('description', $this->category_description)
->bind('hidden', $this->category_is_hidden);
if($isInsert) {
$this->category_id = $savePost->executeGetId();
$this->category_created = time();
} else {
$savePost->bind('category', $this->getId())
->execute();
}
}
public function posts(?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array {
return NewsPost::byCategory($this, $pagination, $includeScheduled, $includeDeleted);
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`category_id`)', self::TABLE));
}
public static function countAll(bool $showHidden = false): int {
return (int)DB::prepare(self::countQueryBase()
. ($showHidden ? '' : ' WHERE `category_is_hidden` = 0'))
->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $categoryId): self {
$getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id');
$getCat->bind('cat_id', $categoryId);
$cat = $getCat->fetchObject(self::class);
if(!$cat)
throw new NewsCategoryNotFoundException;
return $cat;
}
public static function all(?Pagination $pagination = null, bool $showHidden = false): array {
$catsQuery = self::byQueryBase()
. ($showHidden ? '' : ' WHERE `category_is_hidden` = 0')
. ' ORDER BY `category_id` ASC';
if($pagination !== null)
$catsQuery .= ' LIMIT :range OFFSET :offset';
$getCats = DB::prepare($catsQuery);
if($pagination !== null)
$getCats->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getCats->fetchObjects(self::class);
}
// Twig shim for the news category list in manage, don't use this class as an array normally.
public function offsetExists($offset): bool {
return $offset === 'name' || $offset === 'id';
}
public function offsetGet($offset): mixed {
return $this->{'get' . ucfirst($offset)}();
}
public function offsetSet($offset, $value): void {}
public function offsetUnset($offset): void {}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Misuzu\News;
use Index\DateTime;
use Index\Data\IDbResult;
class NewsCategoryInfo {
private string $id;
private string $name;
private string $description;
private bool $hidden;
private int $created;
private int $posts;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->name = $result->getString(1);
$this->description = $result->getString(2);
$this->hidden = $result->getInteger(3) !== 0;
$this->created = $result->getInteger(4);
$this->posts = $result->getInteger(5);
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getDescription(): string {
return $this->description;
}
public function isHidden(): bool {
return $this->hidden;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getPostsCount(): int {
return $this->posts;
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace Misuzu\News;
use RuntimeException;
class NewsException extends RuntimeException {}

View file

@ -1,299 +0,0 @@
<?php
namespace Misuzu\News;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {};
class NewsPost {
// Database fields
private $post_id = -1;
private $category_id = -1;
private $user_id = null;
private $comment_section_id = null;
private $post_is_featured = false;
private $post_title = '';
private $post_text = '';
private $post_scheduled = null;
private $post_created = null;
private $post_updated = null;
private $post_deleted = null;
private $category = null;
private $user = null;
private $userLookedUp = false;
private $comments = null;
public const TABLE = 'news_posts';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`post_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_section_id`'
. ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
. ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
. ', UNIX_TIMESTAMP(%1$s.`post_created`) AS `post_created`'
. ', UNIX_TIMESTAMP(%1$s.`post_updated`) AS `post_updated`'
. ', UNIX_TIMESTAMP(%1$s.`post_deleted`) AS `post_deleted`';
public function getId(): int {
return $this->post_id < 1 ? -1 : $this->post_id;
}
public function getCategoryId(): int {
return $this->category_id < 1 ? -1 : $this->category_id;
}
public function setCategoryId(int $categoryId): self {
$this->category_id = max(1, $categoryId);
$this->category = null;
return $this;
}
public function getCategory(): NewsCategory {
if($this->category === null)
$this->category = NewsCategory::byId($this->getCategoryId());
return $this->category;
}
public function setCategory(NewsCategory $category): self {
$this->category_id = $category->getId();
$this->category = $category;
return $this;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getCommentsCategoryId(): int {
return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
}
public function hasCommentsCategory(): bool {
return $this->getCommentsCategoryId() > 0;
}
public function getCommentsCategory(): CommentsCategory {
if($this->comments === null)
$this->comments = CommentsCategory::byId($this->getCommentsCategoryId());
return $this->comments;
}
public function isFeatured(): bool {
return $this->post_is_featured !== 0;
}
public function setFeatured(bool $featured): self {
$this->post_is_featured = $featured ? 1 : 0;
return $this;
}
public function getTitle(): string {
return $this->post_title;
}
public function setTitle(string $title): self {
$this->post_title = $title;
return $this;
}
public function getText(): string {
return $this->post_text;
}
public function setText(string $text): self {
$this->post_text = $text;
return $this;
}
public function getParsedText(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getText());
}
public function getFirstParagraph(): string {
$text = $this->getText();
$index = mb_strpos($text, "\n");
return $index === false ? $text : mb_substr($text, 0, $index);
}
public function getParsedFirstParagraph(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getFirstParagraph());
}
public function getScheduledTime(): int {
return $this->post_scheduled === null ? -1 : $this->post_scheduled;
}
public function setScheduledTime(int $scheduled): self {
$time = ($time = $this->getCreatedTime()) < 0 ? time() : $time;
$this->post_scheduled = $scheduled < $time ? $time : $scheduled;
return $this;
}
public function isPublished(): bool {
return $this->getScheduledTime() < time();
}
public function getCreatedTime(): int {
return $this->post_created === null ? -1 : $this->post_created;
}
public function getUpdatedTime(): int {
return $this->post_updated === null ? -1 : $this->post_updated;
}
public function isEdited(): bool {
return $this->getUpdatedTime() >= 0;
}
public function getDeletedTime(): int {
return $this->post_deleted === null ? -1 : $this->post_deleted;
}
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $isDeleted): self {
if($this->isDeleted() !== $isDeleted)
$this->post_deleted = $isDeleted ? time() : null;
return $this;
}
public function ensureCommentsCategory(): void {
if($this->hasCommentsCategory())
return;
$this->comments = new CommentsCategory("news-{$this->getId()}");
$this->comments->save();
$this->comment_section_id = $this->comments->getId();
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
->execute([
'comment_section_id' => $this->getCommentsCategoryId(),
'post_id' => $this->getId(),
]);
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `post_is_featured`, `post_title`'
. ', `post_text`, `post_scheduled`, `post_deleted`) VALUES'
. ' (:category, :user, :featured, :title, :text, FROM_UNIXTIME(:scheduled), FROM_UNIXTIME(:deleted))';
} else {
$query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `post_is_featured` = :featured'
. ', `post_title` = :title, `post_text` = :text, `post_scheduled` = FROM_UNIXTIME(:scheduled)'
. ', `post_deleted` = FROM_UNIXTIME(:deleted)'
. ' WHERE `post_id` = :post';
}
$savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('category', $this->category_id)
->bind('user', $this->user_id)
->bind('featured', $this->post_is_featured)
->bind('title', $this->post_title)
->bind('text', $this->post_text)
->bind('scheduled', $this->post_scheduled)
->bind('deleted', $this->post_deleted);
if($isInsert) {
$this->post_id = $savePost->executeGetId();
$this->post_created = time();
} else {
$this->post_updated = time();
$savePost->bind('post', $this->getId())
->execute();
}
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`post_id`)', self::TABLE));
}
public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int {
return (int)DB::prepare(self::countQueryBase()
. ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)'
. ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL'))
->bind('only_featured', $onlyFeatured ? 1 : 0)
->fetchColumn();
}
public static function countByCategory(NewsCategory $category, bool $includeScheduled = false, bool $includeDeleted = false): int {
return (int)DB::prepare(self::countQueryBase()
. ' WHERE `category_id` = :cat_id'
. ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL'))
->bind('cat_id', $category->getId())
->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $postId): self {
$post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id')
->bind('post_id', $postId)
->fetchObject(self::class);
if(!$post)
throw new NewsPostNotFoundException;
return $post;
}
public static function bySearchQuery(string $query, bool $includeScheduled = false, bool $includeDeleted = false): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE MATCH(`post_title`, `post_text`) AGAINST (:query IN NATURAL LANGUAGE MODE)'
. ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
. ' ORDER BY `post_id` DESC'
) ->bind('query', $query)
->fetchObjects(self::class);
}
public static function byCategory(NewsCategory $category, ?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array {
$postsQuery = self::byQueryBase()
. ' WHERE `category_id` = :cat_id'
. ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
. ' ORDER BY `post_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('cat_id', $category->getId());
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
public static function all(?Pagination $pagination = null, bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): array {
$postsQuery = self::byQueryBase()
. ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)'
. ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
. ' ORDER BY `post_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('only_featured', $onlyFeatured ? 1 : 0);
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
}

122
src/News/NewsPostInfo.php Normal file
View file

@ -0,0 +1,122 @@
<?php
namespace Misuzu\News;
use Index\DateTime;
use Index\Data\IDbResult;
class NewsPostInfo {
private string $id;
private string $categoryId;
private ?string $userId;
private ?string $commentsSectionId;
private bool $featured;
private string $title;
private string $body;
private int $scheduled;
private int $created;
private int $updated;
private ?int $deleted;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->categoryId = (string)$result->getInteger(1);
$this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2);
$this->commentsSectionId = $result->isNull(3) ? null : (string)$result->getInteger(3);
$this->featured = $result->getInteger(4) !== 0;
$this->title = $result->getString(5);
$this->body = $result->getString(6);
$this->scheduled = $result->getInteger(7);
$this->created = $result->getInteger(8);
$this->updated = $result->getInteger(9);
$this->deleted = $result->isNull(10) ? null : $result->getInteger(10);
}
public function getId(): string {
return $this->id;
}
public function getCategoryId(): string {
return $this->categoryId;
}
public function hasUserId(): bool {
return $this->userId !== null;
}
public function getUserId(): ?string {
return $this->userId;
}
public function hasCommentsCategoryId(): bool {
return $this->commentsSectionId !== null;
}
public function getCommentsCategoryId(): string {
return $this->commentsSectionId;
}
public function getCommentsCategoryName(): string {
return sprintf('news-%s', $this->id);
}
public function isFeatured(): bool {
return $this->featured;
}
public function getTitle(): string {
return $this->title;
}
public function getBody(): string {
return $this->body;
}
public function getFirstParagraph(): string {
$index = mb_strpos($this->body, "\n");
return $index === false ? $this->body : mb_substr($this->body, 0, $index);
}
public function getScheduledTime(): int {
return $this->scheduled;
}
public function getScheduledAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->scheduled);
}
public function isPublished(): bool {
return $this->scheduled <= time();
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getUpdatedTime(): int {
return $this->updated;
}
public function getUpdatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->updated);
}
public function isEdited(): bool {
return $this->updated > $this->created;
}
public function isDeleted(): bool {
return $this->deleted !== null;
}
public function getDeletedTime(): ?int {
return $this->deleted;
}
public function getDeletedAt(): ?DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
}
}

View file

@ -117,8 +117,10 @@ define('MSZ_URLS', [
'manage-news-categories' => ['/manage/news/categories.php'],
'manage-news-category' => ['/manage/news/category.php', ['c' => '<category>']],
'manage-news-category-delete' => ['/manage/news/category.php', ['c' => '<category>', 'delete' => '1', 'csrf' => '{token}']],
'manage-news-posts' => ['/manage/news/posts.php'],
'manage-news-post' => ['/manage/news/post.php', ['p' => '<post>']],
'manage-news-post-delete' => ['/manage/news/post.php', ['p' => '<post>', 'delete' => '1', 'csrf' => '{token}']],
'manage-users' => ['/manage/users'],
'manage-user' => ['/manage/users/user.php', ['u' => '<user>']],

View file

@ -171,11 +171,9 @@
{% for post in featured_news %}
<div class="landingv2-news-post markdown">
<h1>{{ post.title }}</h1>
<p>{{ post.parsedFirstParagraph|raw }}</p>
<p>{{ post.firstParagraph|parse_text(2)|raw }}</p>
<div class="landingv2-news-post-options">
<a href="{{ url('news-post', {'post': post.id}) }}" class="landingv2-news-post-option">Continue reading</a>
| <a href="{{ url('news-post-comments', {'post': post.id}) }}" class="landingv2-news-post-option">
{{ not post.hasCommentsCategory or post.commentsCategory.postCount < 1 ? 'No' : post.commentsCategory.postCount|number_format }} comment{{ not post.hasCommentsCategory or post.commentsCategory.postCount != 1 ? 's' : '' }}</a>
| {{ post.createdTime|time_diff }}
</div>
</div>

View file

@ -9,10 +9,10 @@
{% for cat in news_categories %}
<p>
<a href="{{ url('manage-news-category', {'category': cat.id}) }}" class="input__button">{{ cat.id }}</a>
{{ cat.name }},
{{ cat.isHidden }},
{{ cat.createdTime|date('r') }}
<a href="{{ url('manage-news-category', {'category': cat.id}) }}" class="input__button">#{{ cat.id }}</a>
{{ cat.name }} |
{{ cat.isHidden ? 'Unlisted' : 'Public' }} |
{{ cat.createdAt }}
</p>
{% endfor %}

View file

@ -2,32 +2,33 @@
{% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox %}
{% set is_new = category is not defined %}
{% block manage_content %}
<form method="post" action="{{ url('manage-news-category', {'category': category_info.id|default(0)}) }}" class="container">
{{ container_title(is_new ? 'New Category' : 'Editing ' ~ category_info.name) }}
{{ container_title(category_new ? 'New Category' : 'Editing ' ~ category_info.name) }}
{{ input_csrf() }}
{{ input_hidden('category[id]', category_info.id|default(0)) }}
<table style="color:inherit">
<tr>
<td>Name</td>
<td>{{ input_text('category[name]', '', category_info.name|default(), 'text', '', true) }}</td>
<td>{{ input_text('nc_name', '', category_info.name|default(), 'text', '', true) }}</td>
</tr>
<tr>
<td>Description</td>
<td><textarea name="category[description]" required class="input__textarea">{{ category_info.description|default() }}</textarea></td>
<td><textarea name="nc_desc" required class="input__textarea">{{ category_info.description|default() }}</textarea></td>
</tr>
<tr>
<td>Is Hidden</td>
<td>{{ input_checkbox('category[hidden]', '', category_info.isHidden|default(false)) }}</td>
<td>{{ input_checkbox('nc_hidden', '', category_info.isHidden|default(false)) }}</td>
</tr>
</table>
<button class="input__button">Save</button>
<div>
<button class="input__button">Save</button>
{% if not category_new %}
<a href="{{ url('manage-news-category-delete', {'category': category_info.id}) }}" class="input__button input__button--destroy" onclick="return confirm('Are you sure?');">Delete</a>
{% endif %}
</div>
</form>
{% endblock %}

View file

@ -2,36 +2,37 @@
{% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_select %}
{% set is_new = post_info is not defined %}
{% block manage_content %}
<form method="post" action="{{ url('manage-news-post', {'post': post_info.id|default(0)}) }}" class="container">
{{ container_title(is_new ? 'New Post' : 'Editing ' ~ post_info.title) }}
{{ container_title(post_new ? 'New Post' : 'Editing ' ~ post_info.title) }}
{{ input_csrf() }}
{{ input_hidden('post[id]', post_info.id|default(0)) }}
<table style="color:inherit">
<tr>
<td>Name</td>
<td>{{ input_text('post[title]', '', post_info.title|default(), 'text', '', true) }}</td>
<td>{{ input_text('np_title', '', post_info.title|default(), 'text', '', true) }}</td>
</tr>
<tr>
<td>Category</td>
<td>{{ input_select('post[category]', categories, post_info.categoryId|default(0), 'name', 'id') }}</td>
<td>{{ input_select('np_category', categories, post_info.categoryId|default(0)) }}</td>
</tr>
<tr>
<td>Is Featured</td>
<td>{{ input_checkbox('post[featured]', '', post_info.isFeatured|default(false)) }}</td>
<td>{{ input_checkbox('np_featured', '', post_info.isFeatured|default(false)) }}</td>
</tr>
<tr>
<td colspan="2"><textarea name="post[text]" required class="input__textarea">{{ post_info.text|default() }}</textarea></td>
<td colspan="2"><textarea name="np_body" required class="input__textarea">{{ post_info.body|default() }}</textarea></td>
</tr>
</table>
<button class="input__button">Save</button>
<div>
<button class="input__button">Save</button>
{% if not post_new %}
<a href="{{ url('manage-news-post-delete', {'post': post_info.id}) }}" class="input__button input__button--destroy" onclick="return confirm('Are you sure?');">Delete</a>
{% endif %}
</div>
</form>
{% endblock %}

View file

@ -9,16 +9,16 @@
{% for post in news_posts %}
<p>
<a href="{{ url('manage-news-post', {'post': post.id}) }}" class="input__button">{{ post.id }}</a>
<a href="{{ url('manage-news-category', {'category': post.categoryId}) }}" class="input__button">Cat: {{ post.categoryId }}</a>
{{ post.isFeatured }},
{{ post.user.id }},
{{ post.title }},
{{ post.scheduledTime|date('r') }},
{{ post.createdTime|date('r') }},
{{ post.updatedTime|date('r') }},
{{ post.deletedTime|date('r') }},
{{ post.commentsCategoryId }}
<a href="{{ url('manage-news-post', {'post': post.id}) }}" class="input__button">#{{ post.id }}</a>
<a href="{{ url('manage-news-category', {'category': post.categoryId}) }}" class="input__button">Category #{{ post.categoryId }}</a>
{{ post.title }} |
{{ post.isFeatured ? 'Featured' : 'Normal' }} |
User #{{ post.userId }} |
{% if post.hasCommentsCategoryId %}Comments category #{{ post.commentsCategoryId }}{% else %}No comments category{% endif %} |
Created {{ post.createdAt }} |
{{ post.isPublished ? 'published' : 'Published ' ~ post.scheduledAt }} |
{{ post.isEdited ? 'Edited ' ~ post.updatedAt : 'not edited' }} |
{{ post.isDeleted ? 'Deleted ' ~ post.deletedAt : 'not deleted' }}
</p>
{% endfor %}

View file

@ -2,10 +2,10 @@
{% from 'macros.twig' import pagination, container_title %}
{% from 'news/macros.twig' import news_preview %}
{% set title = category_info.name ~ ' :: News' %}
{% set manage_link = url('manage-news-category', {'category': category_info.id}) %}
{% set title = news_category.name ~ ' :: News' %}
{% set manage_link = url('manage-news-category', {'category': news_category.id}) %}
{% set canonical_url = url('news-category', {
'category': category_info.id,
'category': news_category.id,
'page': news_pagination.page > 2 ? news_pagination.page : 0,
}) %}
@ -13,33 +13,33 @@
{
'type': 'rss',
'title': '',
'url': url('news-category-feed-rss', {'category': category_info.id}),
'url': url('news-category-feed-rss', {'category': news_category.id}),
},
{
'type': 'atom',
'title': '',
'url': url('news-category-feed-atom', {'category': category_info.id}),
'url': url('news-category-feed-atom', {'category': news_category.id}),
},
] %}
{% block content %}
<div class="news__container">
<div class="news__preview__listing">
{% for post in posts %}
{% for post in news_posts %}
{{ news_preview(post) }}
{% endfor %}
<div class="container" style="padding: 4px; display: {{ news_pagination.pages > 1 ? 'block' : 'none' }}">
{{ pagination(news_pagination, url('news-category', {'category':category_info.id})) }}
{{ pagination(news_pagination, url('news-category', {'category': news_category.id})) }}
</div>
</div>
<div class="news__sidebar">
<div class="container news__list">
{{ container_title('News » ' ~ category_info.name) }}
{{ container_title('News » ' ~ news_category.name) }}
<div class="container__content">
{{ category_info.description }}
{{ news_category.description }}
</div>
</div>
@ -47,7 +47,7 @@
{{ container_title('Feeds') }}
<div class="news__feeds">
<a href="{{ url('news-category-feed-atom', {'category': category_info.id}) }}" class="news__feed">
<a href="{{ url('news-category-feed-atom', {'category': news_category.id}) }}" class="news__feed">
<div class="news__feed__icon">
<i class="fas fa-rss"></i>
</div>
@ -55,7 +55,7 @@
Atom
</div>
</a>
<a href="{{ url('news-category-feed-rss', {'category': category_info.id}) }}" class="news__feed">
<a href="{{ url('news-category-feed-rss', {'category': news_category.id}) }}" class="news__feed">
<div class="news__feed__icon">
<i class="fas fa-rss"></i>
</div>

View file

@ -24,7 +24,7 @@
{% block content %}
<div class="news__container">
<div class="news__preview__listing">
{% for post in posts %}
{% for post in news_posts %}
{{ news_preview(post) }}
{% endfor %}
@ -38,13 +38,13 @@
{{ container_title('Categories') }}
<div class="container__content">
{% for category in categories %}
{% for category in news_categories %}
<a class="news__list__item news__list__item--kvp" href="{{ url('news-category', {'category': category.id}) }}">
<div class="news__list__name">
{{ category.name }}
</div>
<div class="news__list__value">
{{ category.postCount }} post{{ category.postCount == 1 ? '' : 's' }}
{{ category.postsCount }} post{{ category.postsCount == 1 ? '' : 's' }}
</div>
</a>
{% endfor %}

View file

@ -23,49 +23,49 @@
<div class="news__preview__date">
Posted
<time datetime="{{ post.createdTime|date('c') }}" title="{{ post.createdTime|date('r') }}">
{{ post.createdTime|time_diff }}
<time datetime="{{ post.post.createdTime|date('c') }}" title="{{ post.post.createdTime|date('r') }}">
{{ post.post.createdTime|time_diff }}
</time>
</div>
</div>
</div>
<div class="news__preview__content markdown">
<h1>{{ post.title }}</h1>
<h1>{{ post.post.title }}</h1>
<div class="news__preview__text">
{{ post.parsedFirstParagraph|raw }}
{{ post.post.firstParagraph|parse_text(2)|raw }}
</div>
<div class="news__preview__links">
<a href="{{ url('news-post', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link">
{{ not post.hasCommentsCategory or post.commentsCategory.postCount < 1 ? 'No' : post.commentsCategory.postCount|number_format }} comment{{ not post.hasCommentsCategory or post.commentsCategory.postCount != 1 ? 's' : '' }}
<a href="{{ url('news-post', {'post': post.post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.post.id}) }}" class="news__preview__link">
{{ post.comments_count < 1 ? 'No' : post.comments_count|number_format }} comment{{ post.comments_count != 1 ? 's' : '' }}
</a>
</div>
</div>
</div>
{% endmacro %}
{% macro news_post(post) %}
{% macro news_post(post, category, user) %}
{% from 'macros.twig' import avatar %}
<div class="container news__post" style="{% if post.user is not null %}--accent-colour: {{ post.user.colour }}{% endif %}">
<div class="container news__post" style="{% if user is not null %}--accent-colour: {{ user.colour }}{% endif %}">
<div class="news__post__info">
<div class="news__post__info__background"></div>
<div class="news__post__info__content">
{% if post.user is not null %}
{% if user is not null %}
<div class="news__post__user">
<a class="news__post__avatar" href="{{ url('user-profile', {'user': post.user.id}) }}">
{{ avatar(post.user.id, 60, post.user.username) }}
<a class="news__post__avatar" href="{{ url('user-profile', {'user': user.id}) }}">
{{ avatar(user.id, 60, user.username) }}
</a>
<div class="news__post__user__details">
<a class="news__post__username" href="{{ url('user-profile', {'user': post.user.id}) }}">{{ post.user.username }}</a>
<a class="news__post__username" href="{{ url('user-profile', {'user': user.id}) }}">{{ user.username }}</a>
</div>
</div>
{% endif %}
<a class="news__post__category" href="{{ url('news-category', {'category': post.category.id}) }}">
{{ post.category.name }}
<a class="news__post__category" href="{{ url('news-category', {'category': category.id}) }}">
{{ category.name }}
</a>
<div class="news__post__date">
@ -88,7 +88,7 @@
<div class="news__post__text markdown">
<h1>{{ post.title }}</h1>
{{ post.parsedText|raw }}
{{ post.body|parse_text(2)|raw }}
</div>
</div>
{% endmacro %}

View file

@ -8,7 +8,7 @@
{% set manage_link = url('manage-news-post', {'post': post_info.id}) %}
{% block content %}
{{ news_post(post_info) }}
{{ news_post(post_info, post_category_info, post_user_info) }}
{% if comments_info is defined %}
<div class="container">