From d05df7cda295de23c91810aaa8cf1707d4cd1d5b Mon Sep 17 00:00:00 2001 From: Vladimir Jimenez Date: Fri, 2 Feb 2018 20:48:38 -0800 Subject: [PATCH] Port News model to new QueryBuilder - Update News model to use new `is_deleted` and `is_draft` columns with the respective database migration - Update News controller to use new query builder - Change News editor to only allow drafts for items that have not been published yet --- controllers/NewsController.php | 45 ++++++--- ...02213952_news_status_column_conversion.php | 67 ++++++++++++++ models/News.php | 91 +++++++++++-------- models/NewsCategory.php | 23 +++-- src/Form/Creator/NewsFormCreator.php | 70 +++++++++----- views/News/article.html.twig | 2 +- views/News/form.html.twig | 17 +++- 7 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 migrations/20180202213952_news_status_column_conversion.php diff --git a/controllers/NewsController.php b/controllers/NewsController.php index 92d15507..fd7a17d1 100644 --- a/controllers/NewsController.php +++ b/controllers/NewsController.php @@ -4,29 +4,44 @@ class NewsController extends CRUDController { - public function showAction(News $article) + public function showAction(Player $me, News $article) { - return array("article" => $article, "categories" => $this->getCategories()); + if ($article->isDraft() && (!$me->isValid() || !$me->hasPermission(News::EDIT_PERMISSION))) { + throw new ForbiddenException('You do not have permission to view draft posts.'); + } + + return [ + 'article' => $article, + 'categories' => $this->getCategories(), + ]; } - public function listAction(Request $request, NewsCategory $category = null) + public function listAction(Request $request, Player $me, NewsCategory $category = null) { + $currentPage = $this->getCurrentPage(); $qb = $this->getQueryBuilder(); - $currentPage = $this->getCurrentPage(); + $news = $qb + ->orderBy('created', 'DESC') + ->limit(5) + ->fromPage($currentPage) + ; - $news = $qb->sortBy('created')->reverse() - ->where('category')->is($category) - ->limit(5)->fromPage($currentPage) - ->getModels(); + if ($category !== null) { + $news->where('category', '=', $category->getId()); + } + + if (!$me->isValid() || !$me->hasPermission(News::CREATE_PERMISSION)) { + $news->whereNot('is_draft', '=', true); + } - return array( - "news" => $news, - "categories" => $this->getCategories(), - "category" => $category, - "currentPage" => $currentPage, - "totalPages" => $qb->countPages() - ); + return [ + 'news' => $news->getModels(true), + 'categories' => $this->getCategories(), + 'category' => $category, + 'currentPage' => $currentPage, + 'totalPages' => $qb->countPages(), + ]; } public function createAction(Player $me) diff --git a/migrations/20180202213952_news_status_column_conversion.php b/migrations/20180202213952_news_status_column_conversion.php new file mode 100644 index 00000000..7c967d15 --- /dev/null +++ b/migrations/20180202213952_news_status_column_conversion.php @@ -0,0 +1,67 @@ +table('news'); + $newsTable + ->addColumn('is_draft', 'boolean', [ + 'after' => 'editor', + 'null' => false, + 'default' => false, + 'comment' => 'Whether or not the news article is a draft', + ]) + ->addColumn('is_deleted', 'boolean', [ + 'after' => 'is_draft', + 'null' => false, + 'default' => false, + 'comment' => 'Whether or not the news article has been soft deleted', + ]) + ->update() + ; + + $this->query("UPDATE news SET is_draft = 1 WHERE status = 'revision' OR status ='draft';"); + $this->query("UPDATE news SET is_deleted = 1 WHERE status = 'deleted' OR status = 'disabled';"); + + $newsTable + ->removeColumn('parent_id') + ->removeColumn('status') + ->update() + ; + } + + public function down() + { + $newsTable = $this->table('news'); + $newsTable + ->addColumn('parent_id', 'integer', [ + 'after' => 'id', + 'null' => true, + 'default' => null, + 'length' => 11, + 'comment' => 'The ID of the original news post. If this column is set, then it is a revision', + ]) + ->addColumn('status', 'set', [ + 'values' => ['published', 'revision', 'draft', 'disabled', 'deleted'], + 'after' => 'editor', + 'null' => false, + 'default' => 'published', + 'comment' => 'The status of the news element', + ]) + ->update() + ; + + $this->query("UPDATE news SET status = 'draft' WHERE is_draft = 1;"); + $this->query("UPDATE news SET status = 'deleted' WHERE is_deleted = 1;"); + + $newsTable + ->removeColumn('is_draft') + ->removeColumn('is_deleted') + ->update() + ; + } +} diff --git a/models/News.php b/models/News.php index 2a8b44ae..2e17bf2d 100644 --- a/models/News.php +++ b/models/News.php @@ -54,11 +54,10 @@ class News extends UrlModel implements NamedModel */ protected $editor; - const DEFAULT_STATUS = 'published'; + /** @var bool Whether or not the News item is a draft */ + protected $is_draft; - /** - * The name of the database table used for queries - */ + const DELETED_COLUMN = 'is_deleted'; const TABLE = "news"; const CREATE_PERMISSION = Permission::CREATE_NEWS; @@ -78,7 +77,8 @@ protected function assignResult($news) $this->updated = TimeDate::fromMysql($news['updated']); $this->author = $news['author']; $this->editor = $news['editor']; - $this->status = $news['status']; + $this->is_draft = $news['is_draft']; + $this->is_deleted = $news['is_deleted']; } /** @@ -181,6 +181,16 @@ public function getName() return $this->getSubject(); } + /** + * Get whether or not this news article is + * + * @return bool + */ + public function isDraft() + { + return $this->is_draft; + } + /** * {@inheritdoc} */ @@ -197,6 +207,18 @@ public static function getRouteName($action = 'show') return "news_$action"; } + /** + * Update the "draft" status of a post + * + * @param bool $draft + * + * @return static + */ + public function setDraft($draft) + { + return $this->updateProperty($this->is_draft, 'is_draft', $draft); + } + /** * Update the content of a post * @@ -248,6 +270,8 @@ public function updateCategory($categoryID) */ public function updateStatus($status = 'published') { + @trigger_error('The `status` column of the News article has been deprecated. Use `is_draft` or `is_deleted`', E_USER_DEPRECATED); + return $this->updateProperty($this->status, 'status', $status); } @@ -262,14 +286,6 @@ public function updateSubject($subject) return $this->updateProperty($this->subject, 'subject', $subject); } - /** - * {@inheritdoc} - */ - public static function getActiveStatuses() - { - return array('published', 'revision'); - } - /** * Add a new news article * @@ -277,20 +293,20 @@ public static function getActiveStatuses() * @param string $content The content of the article * @param int $authorID The ID of the author * @param int $categoryId The ID of the category this article will be published under - * @param string $status The status of the article: 'published', 'disabled', or 'deleted' + * @param bool $is_draft Whether or not the added news item should be stored as a draft * * @return News An object representing the article that was just created or false if the article was not created */ - public static function addNews($subject, $content, $authorID, $categoryId = 1, $status = 'published') + public static function addNews($subject, $content, $authorID, $categoryId = 1, $is_draft = false) { - return self::create(array( + return self::create([ 'category' => $categoryId, 'subject' => $subject, 'content' => $content, 'author' => $authorID, 'editor' => $authorID, - 'status' => $status, - ), array('created', 'updated')); + 'is_draft' => $is_draft, + ], ['created', 'updated']); } /** @@ -300,38 +316,37 @@ public static function addNews($subject, $content, $authorID, $categoryId = 1, $ * @param int $limit The amount of matches to be retrieved * @param bool $getDrafts Whether or not to fetch drafts * + * @throws Exception When a database is not configured in BZiON + * * @return News[] An array of news objects */ public static function getNews($start = 0, $limit = 5, $getDrafts = false) { - $ignoredStatuses[] = "deleted"; - - if (!$getDrafts) { - $ignoredStatuses[] = "draft"; + $qb = self::getQueryBuilder() + ->limit($limit) + ->offset($start) + ->orderBy('created', 'DESC') + ->active() + ; + + if ($getDrafts) { + $qb->orWhere('is_draft', '=', true); } - return self::arrayIdToModel( - self::fetchIdsFrom( - "status", $ignoredStatuses, true, - "ORDER BY created DESC LIMIT $limit OFFSET $start" - ) - ); + return $qb->getModels(true); } /** * Get a query builder for news - * @return QueryBuilder + * + * @throws Exception + * + * @return QueryBuilderFlex */ public static function getQueryBuilder() { - return new QueryBuilder('News', array( - 'columns' => array( - 'subject' => 'subject', - 'category' => 'category', - 'created' => 'created', - 'status' => 'status' - ), - 'name' => 'subject' - )); + return QueryBuilderFlex::createForModel(News::class) + ->setNameColumn('subject') + ; } } diff --git a/models/NewsCategory.php b/models/NewsCategory.php index 0386bb22..979470a0 100644 --- a/models/NewsCategory.php +++ b/models/NewsCategory.php @@ -97,22 +97,25 @@ public function getStatus() * @param int $limit The amount of matches to be retrieved * @param bool $getDrafts Whether or not to fetch drafts * + * @throws \Pixie\Exception + * @throws Exception + * * @return News[] An array of news objects */ public function getNews($start = 0, $limit = 5, $getDrafts = false) { - $ignoredStatuses = ""; - - if (!$getDrafts) { - $ignoredStatuses = "'draft', "; + $qb = News::getQueryBuilder() + ->limit($limit) + ->offset($start) + ->active() + ->where('category', '=', $this->getId()) + ; + + if ($getDrafts) { + $qb->whereNot('is_draft', '=', true); } - $ignoredStatuses .= "'deleted'"; - - $query = "WHERE status NOT IN ($ignoredStatuses) AND category = ? "; - $query .= "ORDER BY created DESC LIMIT $limit OFFSET $start"; - - return News::arrayIdToModel(News::fetchIds($query, array($this->getId()))); + return $qb->getModels(true); } /** diff --git a/src/Form/Creator/NewsFormCreator.php b/src/Form/Creator/NewsFormCreator.php index e3c42aec..24ac1465 100644 --- a/src/Form/Creator/NewsFormCreator.php +++ b/src/Form/Creator/NewsFormCreator.php @@ -8,11 +8,17 @@ namespace BZIon\Form\Creator; use BZIon\Form\Type\ModelType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; /** * Form creator for news + * + * @property \News|null $editing */ class NewsFormCreator extends ModelFormCreator { @@ -21,32 +27,41 @@ class NewsFormCreator extends ModelFormCreator */ protected function build($builder) { - return $builder + $builder ->add('category', new ModelType('NewsCategory'), array( 'constraints' => new NotBlank() )) - ->add('subject', 'text', array( - 'constraints' => array( - new NotBlank(), new Length(array( + ->add('subject', TextType::class, [ + 'constraints' => [ + new NotBlank(), + new Length([ 'max' => 100, - )), - ), - )) - ->add('content', 'textarea', array( + ]), + ], + ]) + ->add('content', TextareaType::class, array( 'constraints' => new NotBlank() )) - ->add('status', 'choice', array( - 'choices' => array( - 'published' => 'Public', - 'revision' => 'Revision', - 'draft' => 'Draft', - ), - )) - ->add('enter', 'submit', [ + ->add('publish', SubmitType::class, [ 'attr' => [ 'class' => 'c-button--blue pattern pattern--downward-stripes', ], - ]); + 'label' => 'Publish', + ]) + ; + + if ($this->editing === null || $this->editing->isDraft()) { + $builder + ->add('save_draft', SubmitType::class, [ + 'attr' => [ + 'class' => 'c-button--green pattern pattern--upward-stripes', + ], + 'label' => 'Save Draft' + ]) + ; + } + + return $builder; } /** @@ -59,7 +74,6 @@ public function fill($form, $article) $form->get('category')->setData($article->getCategory()); $form->get('subject')->setData($article->getSubject()); $form->get('content')->setData($article->getContent()); - $form->get('status')->setData($article->getStatus()); } /** @@ -69,12 +83,16 @@ public function fill($form, $article) */ public function update($form, $article) { - $article->updateCategory($form->get('category')->getData()->getId()) - ->updateSubject($form->get('subject')->getData()) - ->updateContent($form->get('content')->getData()) - ->updateStatus($form->get('status')->getData()) - ->updateLastEditor($this->me->getId()) - ->updateEditTimestamp(); + $saveDraft = $form->get('save_draft'); + + $article + ->updateCategory($form->get('category')->getData()->getId()) + ->updateSubject($form->get('subject')->getData()) + ->updateContent($form->get('content')->getData()) + ->setDraft($saveDraft && $saveDraft->isClicked()) + ->updateLastEditor($this->me->getId()) + ->updateEditTimestamp() + ; } /** @@ -82,12 +100,14 @@ public function update($form, $article) */ public function enter($form) { + $saveDraft = $form->get('save_draft'); + return \News::addNews( $form->get('subject')->getData(), $form->get('content')->getData(), $this->me->getId(), $form->get('category')->getData()->getId(), - $form->get('status')->getData() + ($saveDraft && $saveDraft->isClicked()) ); } } diff --git a/views/News/article.html.twig b/views/News/article.html.twig index 64d12081..5de7c222 100644 --- a/views/News/article.html.twig +++ b/views/News/article.html.twig @@ -1,7 +1,7 @@

- {% if (article.status == "draft") %} + {% if article.draft %} [Draft] {% endif %} {{ link_to(article) }} diff --git a/views/News/form.html.twig b/views/News/form.html.twig index ff60af4f..3237c2b3 100644 --- a/views/News/form.html.twig +++ b/views/News/form.html.twig @@ -15,10 +15,25 @@ +
+
+
+
+ {{ form_row(form.publish) }} +
+ + {% if form.save_draft is defined %} +
+ {{ form_row(form.save_draft) }} +
+ {% endif %} +
+
+
+
{{ form_rest(form) }}