Permalink
Browse files

feature #447 Add tags to Post (yceruto)

This PR was squashed before being merged into the master branch (closes #447).

Discussion
----------

Add tags to Post

This implements #44 and it's a complete variant of #192 (On hold since Ago 2016).

| Blog Post Index | Post Show |
| ---- | --- |
| ![tags-index](https://cloud.githubusercontent.com/assets/2028198/22331272/8f0522f8-e398-11e6-8efb-41b8ebb119f4.png)  |  ![tags-show](https://cloud.githubusercontent.com/assets/2028198/22331381/29aa391a-e399-11e6-9f5a-b09b25a86090.png) |

| Post New/Edit |
| --- |
| ![tagsinput](https://cloud.githubusercontent.com/assets/2028198/22348577/3263128e-e3da-11e6-8b62-25f9720f4275.png) |

Also this splits the only one fixture class into three classes (`UserFixtures`, `PostFixtures` and `TagFixtures`):
 - [x] To prevent mess and complexity.
 - [x] To avoid post title duplicated (slug issue).
 - [x] To fix example and explanation of: Sharing objects between fixtures (#192 (comment)).
 - [x] To add a new example about: Fixture ordering and `DependentFixtureInterface` (dependence between fixtures).

Add Translation:
 - [x] English
 - [x] Español

Updated `blog.sqlite` and `blog_test.sqlite` DBs.

---

### Handling Tags (Bootstrap-tagsinput):

There is many options to do that:

 * Form collection to add new tags?
 * [`select2`](https://select2.github.io/examples.html) or [`chosen`](https://github.com/harvesthq/chosen) js plugins?
 * [Bootstrap tagsinput](http://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/) js plugin? <-- 👍
* [Tokenfield for Bootstrap](http://sliptree.github.io/bootstrap-tokenfield/) js plugin?

### TODO:
- [x] Create TagsInputType to handle the post tags collection (Added DataTransformer example)
- [x] Add typeaheadjs option to show tags hint.

Commits
-------

46a54dd Add tags to Post
  • Loading branch information...
2 parents ebca80b + 46a54dd commit 963c260b7ba149b4b215069ee8f23555fb26d494 @javiereguiluz javiereguiluz committed Feb 6, 2017
@@ -202,6 +202,10 @@
<source>label.published_at</source>
<target>Published at</target>
</trans-unit>
+ <trans-unit id="label.tags">
+ <source>label.tags</source>
+ <target>Tags</target>
+ </trans-unit>
<trans-unit id="label.actions">
<source>label.actions</source>
<target>Actions</target>
@@ -206,6 +206,10 @@
<source>label.published_at</source>
<target>Publicado el</target>
</trans-unit>
+ <trans-unit id="label.tags">
+ <source>label.tags</source>
+ <target>Etiquetas</target>
+ </trans-unit>
<trans-unit id="label.actions">
<source>label.actions</source>
<target>Acciones</target>
@@ -14,6 +14,10 @@
<source>post.too_short_content</source>
<target>Post content is too short ({{ limit }} characters minimum)</target>
</trans-unit>
+ <trans-unit id="post.too_much_tags">
+ <source>post.too_much_tags</source>
+ <target>Too much tags ({{ limit }} maximum)</target>
+ </trans-unit>
<trans-unit id="comment.blank">
<source>comment.blank</source>
<target>Please don't leave your comment blank!</target>
@@ -14,6 +14,10 @@
<source>post.too_short_content</source>
<target>El contenido del artículo es demasiado corto ({{ limit }} caracteres como mínimo)</target>
</trans-unit>
+ <trans-unit id="post.too_much_tags">
+ <source>post.too_much_tags</source>
+ <target>Demasiadas etiquetas ({{ limit }} como máximo)</target>
+ </trans-unit>
<trans-unit id="comment.blank">
<source>comment.blank</source>
<target>No es posible dejar el contenido del comentario vacío.</target>
@@ -10,6 +10,7 @@
{{ form_row(form.summary) }}
{{ form_row(form.content) }}
{{ form_row(form.publishedAt) }}
+ {{ form_row(form.tags) }}
<input type="submit" value="{{ 'label.create_post'|trans }}" class="btn btn-primary" />
{{ form_widget(form.saveAndCreateNew, { label: 'label.save_and_create_new', attr: { class: 'btn btn-primary' } }) }}
@@ -4,6 +4,7 @@
{% block main %}
<h1>{{ post.title }}</h1>
+
<p class="post-metadata">
<span class="metadata"><i class="fa fa-calendar"></i> {{ post.publishedAt|localizeddate('long', 'medium', null, 'UTC') }}</span>
<span class="metadata"><i class="fa fa-user"></i> {{ post.author.email }}</span>
@@ -14,6 +15,8 @@
</div>
{{ post.content|md2html }}
+
+ {{ include('blog/_post_tags.html.twig') }}
{% endblock %}
{% block sidebar %}
@@ -16,6 +16,7 @@
<link rel="stylesheet" href="{{ asset('css/font-lato.css') }}">
<link rel="stylesheet" href="{{ asset('css/bootstrap-datetimepicker.min.css') }}">
<link rel="stylesheet" href="{{ asset('css/highlight-solarized-light.css') }}">
+ <link rel="stylesheet" href="{{ asset('css/bootstrap-tagsinput.css') }}">
<link rel="stylesheet" href="{{ asset('css/main.css') }}">
{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
@@ -141,6 +142,7 @@
<script src="{{ asset('js/bootstrap-3.3.7.min.js') }}"></script>
<script src="{{ asset('js/highlight.pack.js') }}"></script>
<script src="{{ asset('js/bootstrap-datetimepicker.min.js') }}"></script>
+ <script src="{{ asset('js/bootstrap-tagsinput.min.js') }}"></script>
<script src="{{ asset('js/main.js') }}"></script>
{% endblock %}
@@ -0,0 +1,10 @@
+{% if not post.tags.empty %}
+ <p class="post-tags">
+ {% for tag in post.tags %}
+ <span class="label label-success">
+ <i class="fa fa-tag"></i> {{ tag.name }}
+ </span>
+ {% endfor %}
+ </p>
+{% endif %}
+
@@ -11,6 +11,8 @@
</a>
</h2>
+ {{ include('blog/_post_tags.html.twig') }}
+
<p class="post-metadata">
<span class="metadata"><i class="fa fa-calendar"></i> {{ post.publishedAt|localizeddate('long', 'medium', null, 'UTC') }}</span>
<span class="metadata"><i class="fa fa-user"></i> {{ post.author.email }}</span>
@@ -16,6 +16,9 @@
<guid>{{ url('blog_post', {'slug': post.slug}) }}</guid>
<pubDate>{{ post.publishedAt|date(format='r', timezone='GMT') }}</pubDate>
<author>{{ post.author.email }}</author>
+ {% for tag in post.tags %}
+ <category>{{ tag.name }}</category>
+ {% endfor %}
</item>
{% endfor %}
</channel>
@@ -12,6 +12,8 @@
{{ post.content|md2html }}
+ {{ include('blog/_post_tags.html.twig') }}
+
<div id="post-add-comment" class="well">
{# The 'IS_AUTHENTICATED_FULLY' role ensures that the user has entered
his/her credentials (login + password) during this session. If he/she
@@ -15,3 +15,12 @@
</span>
</div>
{% endblock %}
+
+{% block tags_input_widget %}
+ <div class="input-group">
+ {{ form_widget(form, {'attr': {'data-toggle': 'tagsinput', 'data-tags': tags|json_encode}}) }}
+ <span class="input-group-addon">
+ <span class="fa fa-tags" aria-hidden="true"></span>
+ </span>
+ </div>
+{% endblock %}
@@ -22,6 +22,16 @@ services:
tags:
- { name: twig.extension }
+ # This is only needed if your form type requires some dependencies to be injected
+ # by the container, otherwise it is unnecessary overhead and therefore not recommended
+ # to do this for all form type classes.
+ # See http://symfony.com/doc/current/best_practices/forms.html
+ app.form.type.tagsinput:
+ class: AppBundle\Form\Type\TagsInputType
+ arguments: ['@doctrine.orm.entity_manager']
+ tags:
+ - { name: form.type }
+
# Event Listeners are classes that listen to one or more specific events.
# Those events are defined in the tags added to the service definition.
# See http://symfony.com/doc/current/event_dispatcher.html#creating-an-event-listener
@@ -13,8 +13,9 @@
use AppBundle\Entity\Comment;
use AppBundle\Entity\Post;
-use AppBundle\Entity\User;
+use AppBundle\Entity\Tag;
use Doctrine\Common\DataFixtures\AbstractFixture;
+use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -31,8 +32,9 @@
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
+ * @author Yonel Ceruto <yonelceruto@gmail.com>
*/
-class LoadFixtures extends AbstractFixture implements ContainerAwareInterface
+class PostFixtures extends AbstractFixture implements DependentFixtureInterface, ContainerAwareInterface
{
use ContainerAwareTrait;
@@ -41,46 +43,23 @@ class LoadFixtures extends AbstractFixture implements ContainerAwareInterface
*/
public function load(ObjectManager $manager)
{
- $this->loadUsers($manager);
- $this->loadPosts($manager);
- }
-
- private function loadUsers(ObjectManager $manager)
- {
- $passwordEncoder = $this->container->get('security.password_encoder');
-
- $johnUser = new User();
- $johnUser->setUsername('john_user');
- $johnUser->setEmail('john_user@symfony.com');
- $encodedPassword = $passwordEncoder->encodePassword($johnUser, 'kitten');
- $johnUser->setPassword($encodedPassword);
- $manager->persist($johnUser);
- $this->addReference('john-user', $johnUser);
-
- $annaAdmin = new User();
- $annaAdmin->setUsername('anna_admin');
- $annaAdmin->setEmail('anna_admin@symfony.com');
- $annaAdmin->setRoles(['ROLE_ADMIN']);
- $encodedPassword = $passwordEncoder->encodePassword($annaAdmin, 'kitten');
- $annaAdmin->setPassword($encodedPassword);
- $manager->persist($annaAdmin);
- $this->addReference('anna-admin', $annaAdmin);
-
- $manager->flush();
- }
+ $phrases = $this->getPhrases();
+ shuffle($phrases);
- private function loadPosts(ObjectManager $manager)
- {
- foreach (range(1, 30) as $i) {
+ foreach ($phrases as $i => $title) {
$post = new Post();
- $post->setTitle($this->getRandomPostTitle());
+ $post->setTitle($title);
$post->setSummary($this->getRandomPostSummary());
$post->setSlug($this->container->get('slugger')->slugify($post->getTitle()));
$post->setContent($this->getPostContent());
+ // This reference has been added in UserFixtures class and contains
+ // an instance of User entity.
$post->setAuthor($this->getReference('anna-admin'));
$post->setPublishedAt(new \DateTime('now - '.$i.'days'));
+ $this->addRandomTags($post);
+
foreach (range(1, 5) as $j) {
$comment = new Comment();
@@ -99,6 +78,34 @@ private function loadPosts(ObjectManager $manager)
$manager->flush();
}
+ /**
+ * This method must return an array of fixtures classes
+ * on which the implementing class depends on.
+ *
+ * @return array
+ */
+ public function getDependencies()
+ {
+ return [
+ TagFixtures::class,
+ UserFixtures::class,
+ ];
+ }
+
+ private function addRandomTags(Post $post)
+ {
+ if (0 === $count = mt_rand(0, 3)) {
+ return;
+ }
+
+ $indexes = (array) array_rand(TagFixtures::$names, $count);
+ foreach ($indexes as $index) {
+ /** @var Tag $tag */
+ $tag = $this->getReference('tag-'.$index);
+ $post->addTag($tag);
+ }
+ }
+
private function getPostContent()
{
return <<<'MARKDOWN'
@@ -157,16 +164,24 @@ private function getPhrases()
'Sed varius a risus eget aliquam',
'Nunc viverra elit ac laoreet suscipit',
'Pellentesque et sapien pulvinar consectetur',
+ 'Ubi est barbatus nix',
+ 'Abnobas sunt hilotaes de placidus vita',
+ 'Ubi est audax amicitia',
+ 'Eposs sunt solems de superbus fortis',
+ 'Vae humani generis',
+ 'Diatrias tolerare tanquam noster caesium',
+ 'Teres talis orgias saepe tractare de camerarius flavum sensorem',
+ 'Silva de secundus galatae demitto quadra',
+ 'Sunt accentores vitare salvus flavum parses',
+ 'Potus sensim ducunt ad ferox abnoba',
+ 'Sunt seculaes transferre talis camerarius fluctuies',
+ 'Era brevis ratione est',
+ 'Sunt torquises imitari velox mirabilis medicinaes',
+ 'Cum mineralis persuadere omnes finises desiderium bi-color',
+ 'Bassus fatalis classiss virtualiter transferre de flavum',
];
}
- private function getRandomPostTitle()
- {
- $titles = $this->getPhrases();
-
- return $titles[array_rand($titles)];
- }
-
private function getRandomPostSummary($maxLength = 255)
{
$phrases = $this->getPhrases();
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace AppBundle\DataFixtures\ORM;
+
+use AppBundle\Entity\Tag;
+use Doctrine\Common\DataFixtures\AbstractFixture;
+use Doctrine\Common\Persistence\ObjectManager;
+
+/**
+ * Defines the sample data to load in the database when running the unit and
+ * functional tests.
+ *
+ * Execute this command to load the data:
+ *
+ * $ php bin/console doctrine:fixtures:load
+ *
+ * See http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
+ *
+ * @author Yonel Ceruto <yonelceruto@gmail.com>
+ */
+class TagFixtures extends AbstractFixture
+{
+ public static $names = [
+ 'Lorem',
+ 'ipsum',
+ 'consectetur',
+ 'adipiscing',
+ 'incididunt',
+ 'labore',
+ 'voluptate',
+ 'dolore',
+ 'pariatur',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load(ObjectManager $manager)
+ {
+ foreach (self::$names as $index => $name) {
+ $tag = new Tag();
+ $tag->setName($name);
+
+ $manager->persist($tag);
+ $this->addReference('tag-'.$index, $tag);
+ }
+
+ $manager->flush();
+ }
+}
Oops, something went wrong.

0 comments on commit 963c260

Please sign in to comment.