Skip to content

Commit 963c260

Browse files
committed
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
2 parents ebca80b + 46a54dd commit 963c260

28 files changed

+708
-41
lines changed

app/Resources/translations/messages.en.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@
202202
<source>label.published_at</source>
203203
<target>Published at</target>
204204
</trans-unit>
205+
<trans-unit id="label.tags">
206+
<source>label.tags</source>
207+
<target>Tags</target>
208+
</trans-unit>
205209
<trans-unit id="label.actions">
206210
<source>label.actions</source>
207211
<target>Actions</target>

app/Resources/translations/messages.es.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@
206206
<source>label.published_at</source>
207207
<target>Publicado el</target>
208208
</trans-unit>
209+
<trans-unit id="label.tags">
210+
<source>label.tags</source>
211+
<target>Etiquetas</target>
212+
</trans-unit>
209213
<trans-unit id="label.actions">
210214
<source>label.actions</source>
211215
<target>Acciones</target>

app/Resources/translations/validators.en.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
<source>post.too_short_content</source>
1515
<target>Post content is too short ({{ limit }} characters minimum)</target>
1616
</trans-unit>
17+
<trans-unit id="post.too_much_tags">
18+
<source>post.too_much_tags</source>
19+
<target>Too much tags ({{ limit }} maximum)</target>
20+
</trans-unit>
1721
<trans-unit id="comment.blank">
1822
<source>comment.blank</source>
1923
<target>Please don't leave your comment blank!</target>

app/Resources/translations/validators.es.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
<source>post.too_short_content</source>
1515
<target>El contenido del artículo es demasiado corto ({{ limit }} caracteres como mínimo)</target>
1616
</trans-unit>
17+
<trans-unit id="post.too_much_tags">
18+
<source>post.too_much_tags</source>
19+
<target>Demasiadas etiquetas ({{ limit }} como máximo)</target>
20+
</trans-unit>
1721
<trans-unit id="comment.blank">
1822
<source>comment.blank</source>
1923
<target>No es posible dejar el contenido del comentario vacío.</target>

app/Resources/views/admin/blog/new.html.twig

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
{{ form_row(form.summary) }}
1111
{{ form_row(form.content) }}
1212
{{ form_row(form.publishedAt) }}
13+
{{ form_row(form.tags) }}
1314

1415
<input type="submit" value="{{ 'label.create_post'|trans }}" class="btn btn-primary" />
1516
{{ form_widget(form.saveAndCreateNew, { label: 'label.save_and_create_new', attr: { class: 'btn btn-primary' } }) }}

app/Resources/views/admin/blog/show.html.twig

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
{% block main %}
66
<h1>{{ post.title }}</h1>
7+
78
<p class="post-metadata">
89
<span class="metadata"><i class="fa fa-calendar"></i> {{ post.publishedAt|localizeddate('long', 'medium', null, 'UTC') }}</span>
910
<span class="metadata"><i class="fa fa-user"></i> {{ post.author.email }}</span>
@@ -14,6 +15,8 @@
1415
</div>
1516

1617
{{ post.content|md2html }}
18+
19+
{{ include('blog/_post_tags.html.twig') }}
1720
{% endblock %}
1821

1922
{% block sidebar %}

app/Resources/views/base.html.twig

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<link rel="stylesheet" href="{{ asset('css/font-lato.css') }}">
1717
<link rel="stylesheet" href="{{ asset('css/bootstrap-datetimepicker.min.css') }}">
1818
<link rel="stylesheet" href="{{ asset('css/highlight-solarized-light.css') }}">
19+
<link rel="stylesheet" href="{{ asset('css/bootstrap-tagsinput.css') }}">
1920
<link rel="stylesheet" href="{{ asset('css/main.css') }}">
2021
{% endblock %}
2122
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
@@ -141,6 +142,7 @@
141142
<script src="{{ asset('js/bootstrap-3.3.7.min.js') }}"></script>
142143
<script src="{{ asset('js/highlight.pack.js') }}"></script>
143144
<script src="{{ asset('js/bootstrap-datetimepicker.min.js') }}"></script>
145+
<script src="{{ asset('js/bootstrap-tagsinput.min.js') }}"></script>
144146
<script src="{{ asset('js/main.js') }}"></script>
145147
{% endblock %}
146148

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% if not post.tags.empty %}
2+
<p class="post-tags">
3+
{% for tag in post.tags %}
4+
<span class="label label-success">
5+
<i class="fa fa-tag"></i> {{ tag.name }}
6+
</span>
7+
{% endfor %}
8+
</p>
9+
{% endif %}
10+

app/Resources/views/blog/index.html.twig

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
</a>
1212
</h2>
1313

14+
{{ include('blog/_post_tags.html.twig') }}
15+
1416
<p class="post-metadata">
1517
<span class="metadata"><i class="fa fa-calendar"></i> {{ post.publishedAt|localizeddate('long', 'medium', null, 'UTC') }}</span>
1618
<span class="metadata"><i class="fa fa-user"></i> {{ post.author.email }}</span>

app/Resources/views/blog/index.xml.twig

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
<guid>{{ url('blog_post', {'slug': post.slug}) }}</guid>
1717
<pubDate>{{ post.publishedAt|date(format='r', timezone='GMT') }}</pubDate>
1818
<author>{{ post.author.email }}</author>
19+
{% for tag in post.tags %}
20+
<category>{{ tag.name }}</category>
21+
{% endfor %}
1922
</item>
2023
{% endfor %}
2124
</channel>

app/Resources/views/blog/post_show.html.twig

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
{{ post.content|md2html }}
1414

15+
{{ include('blog/_post_tags.html.twig') }}
16+
1517
<div id="post-add-comment" class="well">
1618
{# The 'IS_AUTHENTICATED_FULLY' role ensures that the user has entered
1719
his/her credentials (login + password) during this session. If he/she

app/Resources/views/form/fields.html.twig

+9
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@
1515
</span>
1616
</div>
1717
{% endblock %}
18+
19+
{% block tags_input_widget %}
20+
<div class="input-group">
21+
{{ form_widget(form, {'attr': {'data-toggle': 'tagsinput', 'data-tags': tags|json_encode}}) }}
22+
<span class="input-group-addon">
23+
<span class="fa fa-tags" aria-hidden="true"></span>
24+
</span>
25+
</div>
26+
{% endblock %}

app/config/services.yml

+10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ services:
2222
tags:
2323
- { name: twig.extension }
2424

25+
# This is only needed if your form type requires some dependencies to be injected
26+
# by the container, otherwise it is unnecessary overhead and therefore not recommended
27+
# to do this for all form type classes.
28+
# See http://symfony.com/doc/current/best_practices/forms.html
29+
app.form.type.tagsinput:
30+
class: AppBundle\Form\Type\TagsInputType
31+
arguments: ['@doctrine.orm.entity_manager']
32+
tags:
33+
- { name: form.type }
34+
2535
# Event Listeners are classes that listen to one or more specific events.
2636
# Those events are defined in the tags added to the service definition.
2737
# See http://symfony.com/doc/current/event_dispatcher.html#creating-an-event-listener

src/AppBundle/DataFixtures/ORM/LoadFixtures.php renamed to src/AppBundle/DataFixtures/ORM/PostFixtures.php

+55-40
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
use AppBundle\Entity\Comment;
1515
use AppBundle\Entity\Post;
16-
use AppBundle\Entity\User;
16+
use AppBundle\Entity\Tag;
1717
use Doctrine\Common\DataFixtures\AbstractFixture;
18+
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
1819
use Doctrine\Common\Persistence\ObjectManager;
1920
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
2021
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -31,8 +32,9 @@
3132
*
3233
* @author Ryan Weaver <[email protected]>
3334
* @author Javier Eguiluz <[email protected]>
35+
* @author Yonel Ceruto <[email protected]>
3436
*/
35-
class LoadFixtures extends AbstractFixture implements ContainerAwareInterface
37+
class PostFixtures extends AbstractFixture implements DependentFixtureInterface, ContainerAwareInterface
3638
{
3739
use ContainerAwareTrait;
3840

@@ -41,46 +43,23 @@ class LoadFixtures extends AbstractFixture implements ContainerAwareInterface
4143
*/
4244
public function load(ObjectManager $manager)
4345
{
44-
$this->loadUsers($manager);
45-
$this->loadPosts($manager);
46-
}
47-
48-
private function loadUsers(ObjectManager $manager)
49-
{
50-
$passwordEncoder = $this->container->get('security.password_encoder');
51-
52-
$johnUser = new User();
53-
$johnUser->setUsername('john_user');
54-
$johnUser->setEmail('[email protected]');
55-
$encodedPassword = $passwordEncoder->encodePassword($johnUser, 'kitten');
56-
$johnUser->setPassword($encodedPassword);
57-
$manager->persist($johnUser);
58-
$this->addReference('john-user', $johnUser);
59-
60-
$annaAdmin = new User();
61-
$annaAdmin->setUsername('anna_admin');
62-
$annaAdmin->setEmail('[email protected]');
63-
$annaAdmin->setRoles(['ROLE_ADMIN']);
64-
$encodedPassword = $passwordEncoder->encodePassword($annaAdmin, 'kitten');
65-
$annaAdmin->setPassword($encodedPassword);
66-
$manager->persist($annaAdmin);
67-
$this->addReference('anna-admin', $annaAdmin);
68-
69-
$manager->flush();
70-
}
46+
$phrases = $this->getPhrases();
47+
shuffle($phrases);
7148

72-
private function loadPosts(ObjectManager $manager)
73-
{
74-
foreach (range(1, 30) as $i) {
49+
foreach ($phrases as $i => $title) {
7550
$post = new Post();
7651

77-
$post->setTitle($this->getRandomPostTitle());
52+
$post->setTitle($title);
7853
$post->setSummary($this->getRandomPostSummary());
7954
$post->setSlug($this->container->get('slugger')->slugify($post->getTitle()));
8055
$post->setContent($this->getPostContent());
56+
// This reference has been added in UserFixtures class and contains
57+
// an instance of User entity.
8158
$post->setAuthor($this->getReference('anna-admin'));
8259
$post->setPublishedAt(new \DateTime('now - '.$i.'days'));
8360

61+
$this->addRandomTags($post);
62+
8463
foreach (range(1, 5) as $j) {
8564
$comment = new Comment();
8665

@@ -99,6 +78,34 @@ private function loadPosts(ObjectManager $manager)
9978
$manager->flush();
10079
}
10180

81+
/**
82+
* This method must return an array of fixtures classes
83+
* on which the implementing class depends on.
84+
*
85+
* @return array
86+
*/
87+
public function getDependencies()
88+
{
89+
return [
90+
TagFixtures::class,
91+
UserFixtures::class,
92+
];
93+
}
94+
95+
private function addRandomTags(Post $post)
96+
{
97+
if (0 === $count = mt_rand(0, 3)) {
98+
return;
99+
}
100+
101+
$indexes = (array) array_rand(TagFixtures::$names, $count);
102+
foreach ($indexes as $index) {
103+
/** @var Tag $tag */
104+
$tag = $this->getReference('tag-'.$index);
105+
$post->addTag($tag);
106+
}
107+
}
108+
102109
private function getPostContent()
103110
{
104111
return <<<'MARKDOWN'
@@ -157,16 +164,24 @@ private function getPhrases()
157164
'Sed varius a risus eget aliquam',
158165
'Nunc viverra elit ac laoreet suscipit',
159166
'Pellentesque et sapien pulvinar consectetur',
167+
'Ubi est barbatus nix',
168+
'Abnobas sunt hilotaes de placidus vita',
169+
'Ubi est audax amicitia',
170+
'Eposs sunt solems de superbus fortis',
171+
'Vae humani generis',
172+
'Diatrias tolerare tanquam noster caesium',
173+
'Teres talis orgias saepe tractare de camerarius flavum sensorem',
174+
'Silva de secundus galatae demitto quadra',
175+
'Sunt accentores vitare salvus flavum parses',
176+
'Potus sensim ducunt ad ferox abnoba',
177+
'Sunt seculaes transferre talis camerarius fluctuies',
178+
'Era brevis ratione est',
179+
'Sunt torquises imitari velox mirabilis medicinaes',
180+
'Cum mineralis persuadere omnes finises desiderium bi-color',
181+
'Bassus fatalis classiss virtualiter transferre de flavum',
160182
];
161183
}
162184

163-
private function getRandomPostTitle()
164-
{
165-
$titles = $this->getPhrases();
166-
167-
return $titles[array_rand($titles)];
168-
}
169-
170185
private function getRandomPostSummary($maxLength = 255)
171186
{
172187
$phrases = $this->getPhrases();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace AppBundle\DataFixtures\ORM;
13+
14+
use AppBundle\Entity\Tag;
15+
use Doctrine\Common\DataFixtures\AbstractFixture;
16+
use Doctrine\Common\Persistence\ObjectManager;
17+
18+
/**
19+
* Defines the sample data to load in the database when running the unit and
20+
* functional tests.
21+
*
22+
* Execute this command to load the data:
23+
*
24+
* $ php bin/console doctrine:fixtures:load
25+
*
26+
* See http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
27+
*
28+
* @author Yonel Ceruto <[email protected]>
29+
*/
30+
class TagFixtures extends AbstractFixture
31+
{
32+
public static $names = [
33+
'Lorem',
34+
'ipsum',
35+
'consectetur',
36+
'adipiscing',
37+
'incididunt',
38+
'labore',
39+
'voluptate',
40+
'dolore',
41+
'pariatur',
42+
];
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
public function load(ObjectManager $manager)
48+
{
49+
foreach (self::$names as $index => $name) {
50+
$tag = new Tag();
51+
$tag->setName($name);
52+
53+
$manager->persist($tag);
54+
$this->addReference('tag-'.$index, $tag);
55+
}
56+
57+
$manager->flush();
58+
}
59+
}

0 commit comments

Comments
 (0)