diff options
-rw-r--r-- | inc/poche/Database.class.php | 71 | ||||
-rw-r--r-- | inc/poche/Poche.class.php | 79 | ||||
-rw-r--r-- | inc/poche/Tools.class.php | 41 | ||||
-rw-r--r-- | index.php | 3 | ||||
-rw-r--r-- | install/mysql.sql | 15 | ||||
-rwxr-xr-x | install/poche.sqlite | bin | 360448 -> 393216 bytes | |||
-rw-r--r-- | install/postgres.sql | 13 | ||||
-rw-r--r-- | themes/default/_menu.twig | 1 | ||||
-rw-r--r-- | themes/default/css/style.css | 6 | ||||
-rw-r--r-- | themes/default/edit-tags.twig | 20 | ||||
-rw-r--r-- | themes/default/img/default/rss.png | bin | 0 -> 288 bytes | |||
-rw-r--r-- | themes/default/tag.twig | 33 | ||||
-rw-r--r-- | themes/default/tags.twig | 8 | ||||
-rw-r--r-- | themes/default/view.twig | 3 |
14 files changed, 252 insertions, 41 deletions
diff --git a/inc/poche/Database.class.php b/inc/poche/Database.class.php index c233eda1..d95b9b81 100644 --- a/inc/poche/Database.class.php +++ b/inc/poche/Database.class.php | |||
@@ -249,4 +249,75 @@ class Database { | |||
249 | public function getLastId($column = '') { | 249 | public function getLastId($column = '') { |
250 | return $this->getHandle()->lastInsertId($column); | 250 | return $this->getHandle()->lastInsertId($column); |
251 | } | 251 | } |
252 | |||
253 | public function retrieveAllTags() { | ||
254 | $sql = "SELECT * FROM tags"; | ||
255 | $query = $this->executeQuery($sql, array()); | ||
256 | $tags = $query->fetchAll(); | ||
257 | |||
258 | return $tags; | ||
259 | } | ||
260 | |||
261 | public function retrieveTag($id) { | ||
262 | $tag = NULL; | ||
263 | $sql = "SELECT * FROM tags WHERE id=?"; | ||
264 | $params = array(intval($id)); | ||
265 | $query = $this->executeQuery($sql, $params); | ||
266 | $tag = $query->fetchAll(); | ||
267 | |||
268 | return isset($tag[0]) ? $tag[0] : null; | ||
269 | } | ||
270 | |||
271 | public function retrieveEntriesByTag($tag_id) { | ||
272 | $sql = | ||
273 | "SELECT * FROM entries | ||
274 | LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id | ||
275 | WHERE tags_entries.tag_id = ?"; | ||
276 | $query = $this->executeQuery($sql, array($tag_id)); | ||
277 | $entries = $query->fetchAll(); | ||
278 | |||
279 | return $entries; | ||
280 | } | ||
281 | |||
282 | public function retrieveTagsByEntry($entry_id) { | ||
283 | $sql = | ||
284 | "SELECT * FROM tags | ||
285 | LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id | ||
286 | WHERE tags_entries.entry_id = ?"; | ||
287 | $query = $this->executeQuery($sql, array($entry_id)); | ||
288 | $tags = $query->fetchAll(); | ||
289 | |||
290 | return $tags; | ||
291 | } | ||
292 | |||
293 | public function removeTagForEntry($entry_id, $tag_id) { | ||
294 | $sql_action = "DELETE FROM tags_entries WHERE tag_id=? AND entry_id=?"; | ||
295 | $params_action = array($tag_id, $entry_id); | ||
296 | $query = $this->executeQuery($sql_action, $params_action); | ||
297 | return $query; | ||
298 | } | ||
299 | |||
300 | public function retrieveTagByValue($value) { | ||
301 | $tag = NULL; | ||
302 | $sql = "SELECT * FROM tags WHERE value=?"; | ||
303 | $params = array($value); | ||
304 | $query = $this->executeQuery($sql, $params); | ||
305 | $tag = $query->fetchAll(); | ||
306 | |||
307 | return isset($tag[0]) ? $tag[0] : null; | ||
308 | } | ||
309 | |||
310 | public function createTag($value) { | ||
311 | $sql_action = 'INSERT INTO tags ( value ) VALUES (?)'; | ||
312 | $params_action = array($value); | ||
313 | $query = $this->executeQuery($sql_action, $params_action); | ||
314 | return $query; | ||
315 | } | ||
316 | |||
317 | public function setTagToEntry($tag_id, $entry_id) { | ||
318 | $sql_action = 'INSERT INTO tags_entries ( tag_id, entry_id ) VALUES (?, ?)'; | ||
319 | $params_action = array($tag_id, $entry_id); | ||
320 | $query = $this->executeQuery($sql_action, $params_action); | ||
321 | return $query; | ||
322 | } | ||
252 | } | 323 | } |
diff --git a/inc/poche/Poche.class.php b/inc/poche/Poche.class.php index d45d0c40..d415dd03 100644 --- a/inc/poche/Poche.class.php +++ b/inc/poche/Poche.class.php | |||
@@ -397,6 +397,36 @@ class Poche | |||
397 | Tools::redirect(); | 397 | Tools::redirect(); |
398 | } | 398 | } |
399 | break; | 399 | break; |
400 | case 'add_tag' : | ||
401 | $tags = explode(',', $_POST['value']); | ||
402 | $entry_id = $_POST['entry_id']; | ||
403 | foreach($tags as $key => $tag_value) { | ||
404 | $value = trim($tag_value); | ||
405 | $tag = $this->store->retrieveTagByValue($value); | ||
406 | |||
407 | if (is_null($tag)) { | ||
408 | # we create the tag | ||
409 | $tag = $this->store->createTag($value); | ||
410 | $sequence = ''; | ||
411 | if (STORAGE == 'postgres') { | ||
412 | $sequence = 'tags_id_seq'; | ||
413 | } | ||
414 | $tag_id = $this->store->getLastId($sequence); | ||
415 | } | ||
416 | else { | ||
417 | $tag_id = $tag['id']; | ||
418 | } | ||
419 | |||
420 | # we assign the tag to the article | ||
421 | $this->store->setTagToEntry($tag_id, $entry_id); | ||
422 | } | ||
423 | Tools::redirect(); | ||
424 | break; | ||
425 | case 'remove_tag' : | ||
426 | $tag_id = $_GET['tag_id']; | ||
427 | $this->store->removeTagForEntry($id, $tag_id); | ||
428 | Tools::redirect(); | ||
429 | break; | ||
400 | default: | 430 | default: |
401 | break; | 431 | break; |
402 | } | 432 | } |
@@ -430,6 +460,31 @@ class Poche | |||
430 | ); | 460 | ); |
431 | Tools::logm('config view'); | 461 | Tools::logm('config view'); |
432 | break; | 462 | break; |
463 | case 'edit-tags': | ||
464 | # tags | ||
465 | $tags = $this->store->retrieveTagsByEntry($id); | ||
466 | $tpl_vars = array( | ||
467 | 'entry_id' => $id, | ||
468 | 'tags' => $tags, | ||
469 | ); | ||
470 | break; | ||
471 | case 'tag': | ||
472 | $entries = $this->store->retrieveEntriesByTag($id); | ||
473 | $tag = $this->store->retrieveTag($id); | ||
474 | $tpl_vars = array( | ||
475 | 'tag' => $tag, | ||
476 | 'entries' => $entries, | ||
477 | ); | ||
478 | break; | ||
479 | case 'tags': | ||
480 | $token = $this->user->getConfigValue('token'); | ||
481 | $tags = $this->store->retrieveAllTags(); | ||
482 | $tpl_vars = array( | ||
483 | 'token' => $token, | ||
484 | 'user_id' => $this->user->getId(), | ||
485 | 'tags' => $tags, | ||
486 | ); | ||
487 | break; | ||
433 | case 'view': | 488 | case 'view': |
434 | $entry = $this->store->retrieveOneById($id, $this->user->getId()); | 489 | $entry = $this->store->retrieveOneById($id, $this->user->getId()); |
435 | if ($entry != NULL) { | 490 | if ($entry != NULL) { |
@@ -443,12 +498,16 @@ class Poche | |||
443 | 498 | ||
444 | # flattr checking | 499 | # flattr checking |
445 | $flattr = new FlattrItem(); | 500 | $flattr = new FlattrItem(); |
446 | $flattr->checkItem($entry['url'],$entry['id']); | 501 | $flattr->checkItem($entry['url'], $entry['id']); |
502 | |||
503 | # tags | ||
504 | $tags = $this->store->retrieveTagsByEntry($entry['id']); | ||
447 | 505 | ||
448 | $tpl_vars = array( | 506 | $tpl_vars = array( |
449 | 'entry' => $entry, | 507 | 'entry' => $entry, |
450 | 'content' => $content, | 508 | 'content' => $content, |
451 | 'flattr' => $flattr | 509 | 'flattr' => $flattr, |
510 | 'tags' => $tags | ||
452 | ); | 511 | ); |
453 | } | 512 | } |
454 | else { | 513 | else { |
@@ -859,9 +918,9 @@ class Poche | |||
859 | $_SESSION['poche_user']->setConfig($currentConfig); | 918 | $_SESSION['poche_user']->setConfig($currentConfig); |
860 | } | 919 | } |
861 | 920 | ||
862 | public function generateFeeds($token, $user_id, $type = 'home') | 921 | public function generateFeeds($token, $user_id, $tag_id, $type = 'home') |
863 | { | 922 | { |
864 | $allowed_types = array('home', 'fav', 'archive'); | 923 | $allowed_types = array('home', 'fav', 'archive', 'tag'); |
865 | $config = $this->store->getConfigUser($user_id); | 924 | $config = $this->store->getConfigUser($user_id); |
866 | 925 | ||
867 | if (!in_array($type, $allowed_types) || | 926 | if (!in_array($type, $allowed_types) || |
@@ -876,7 +935,13 @@ class Poche | |||
876 | $feed->setChannelElement('updated', date(DATE_RSS , time())); | 935 | $feed->setChannelElement('updated', date(DATE_RSS , time())); |
877 | $feed->setChannelElement('author', 'poche'); | 936 | $feed->setChannelElement('author', 'poche'); |
878 | 937 | ||
879 | $entries = $this->store->getEntriesByView($type, $user_id); | 938 | if ($type == 'tag') { |
939 | $entries = $this->store->retrieveEntriesByTag($tag_id); | ||
940 | } | ||
941 | else { | ||
942 | $entries = $this->store->getEntriesByView($type, $user_id); | ||
943 | } | ||
944 | |||
880 | if (count($entries) > 0) { | 945 | if (count($entries) > 0) { |
881 | foreach ($entries as $entry) { | 946 | foreach ($entries as $entry) { |
882 | $newItem = $feed->createNewItem(); | 947 | $newItem = $feed->createNewItem(); |
diff --git a/inc/poche/Tools.class.php b/inc/poche/Tools.class.php index 9d8e1fd6..63916582 100644 --- a/inc/poche/Tools.class.php +++ b/inc/poche/Tools.class.php | |||
@@ -88,39 +88,16 @@ class Tools | |||
88 | 88 | ||
89 | public static function getTplFile($view) | 89 | public static function getTplFile($view) |
90 | { | 90 | { |
91 | $default_tpl = 'home.twig'; | 91 | $views = array( |
92 | 92 | 'install', 'import', 'export', 'config', 'tags', | |
93 | switch ($view) { | 93 | 'edit-tags', 'view', 'login', 'error', 'tag' |
94 | case 'install': | 94 | ); |
95 | $tpl_file = 'install.twig'; | 95 | |
96 | break; | 96 | if (in_array($view, $views)) { |
97 | case 'import'; | 97 | return $view . '.twig'; |
98 | $tpl_file = 'import.twig'; | ||
99 | break; | ||
100 | case 'export': | ||
101 | $tpl_file = 'export.twig'; | ||
102 | break; | ||
103 | case 'config': | ||
104 | $tpl_file = 'config.twig'; | ||
105 | break; | ||
106 | case 'view': | ||
107 | $tpl_file = 'view.twig'; | ||
108 | break; | ||
109 | |||
110 | case 'login': | ||
111 | $tpl_file = 'login.twig'; | ||
112 | break; | ||
113 | |||
114 | case 'error': | ||
115 | $tpl_file = 'error.twig'; | ||
116 | break; | ||
117 | |||
118 | default: | ||
119 | $tpl_file = $default_tpl; | ||
120 | break; | ||
121 | } | 98 | } |
122 | 99 | ||
123 | return $tpl_file; | 100 | return 'home.twig'; |
124 | } | 101 | } |
125 | 102 | ||
126 | public static function getFile($url) | 103 | public static function getFile($url) |
@@ -77,7 +77,8 @@ if (isset($_GET['login'])) { | |||
77 | $poche->generateToken(); | 77 | $poche->generateToken(); |
78 | } | 78 | } |
79 | else { | 79 | else { |
80 | $poche->generateFeeds($_GET['token'], $_GET['user_id'], $_GET['type']); | 80 | $tag_id = (isset($_GET['tag_id']) ? intval($_GET['tag_id']) : 0); |
81 | $poche->generateFeeds($_GET['token'], $_GET['user_id'], $tag_id, $_GET['type']); | ||
81 | } | 82 | } |
82 | } | 83 | } |
83 | 84 | ||
diff --git a/install/mysql.sql b/install/mysql.sql index 9b01e32c..66c4bb31 100644 --- a/install/mysql.sql +++ b/install/mysql.sql | |||
@@ -31,4 +31,19 @@ CREATE TABLE IF NOT EXISTS `users_config` ( | |||
31 | `name` varchar(255) NOT NULL, | 31 | `name` varchar(255) NOT NULL, |
32 | `value` varchar(255) NOT NULL, | 32 | `value` varchar(255) NOT NULL, |
33 | PRIMARY KEY (`id`) | 33 | PRIMARY KEY (`id`) |
34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | ||
35 | |||
36 | CREATE TABLE tags ( | ||
37 | `id` int(11) NOT NULL AUTO_INCREMENT, | ||
38 | `value` varchar(255) NOT NULL, | ||
39 | PRIMARY KEY (`id`) | ||
40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | ||
41 | |||
42 | CREATE TABLE tags_entries ( | ||
43 | `id` int(11) NOT NULL AUTO_INCREMENT, | ||
44 | `entry_id` int(11) NOT NULL, | ||
45 | `tag_id` int(11) NOT NULL, | ||
46 | FOREIGN KEY(entry_id) REFERENCES entries(id) ON DELETE CASCADE, | ||
47 | FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, | ||
48 | PRIMARY KEY (`id`) | ||
34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file | 49 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file |
diff --git a/install/poche.sqlite b/install/poche.sqlite index 7abf1f62..b911f272 100755 --- a/install/poche.sqlite +++ b/install/poche.sqlite | |||
Binary files differ | |||
diff --git a/install/postgres.sql b/install/postgres.sql index 9e0e8276..fe8f559c 100644 --- a/install/postgres.sql +++ b/install/postgres.sql | |||
@@ -27,4 +27,15 @@ CREATE TABLE users_config ( | |||
27 | user_id integer NOT NULL, | 27 | user_id integer NOT NULL, |
28 | name varchar(255) NOT NULL, | 28 | name varchar(255) NOT NULL, |
29 | value varchar(255) NOT NULL | 29 | value varchar(255) NOT NULL |
30 | ); \ No newline at end of file | 30 | ); |
31 | |||
32 | CREATE TABLE tags ( | ||
33 | id bigserial primary key, | ||
34 | value varchar(255) NOT NULL | ||
35 | ); | ||
36 | |||
37 | CREATE TABLE tags_entries ( | ||
38 | id bigserial primary key, | ||
39 | entry_id integer NOT NULL, | ||
40 | tag_id integer NOT NULL | ||
41 | ) \ No newline at end of file | ||
diff --git a/themes/default/_menu.twig b/themes/default/_menu.twig index 699d6a0c..02bec1dc 100644 --- a/themes/default/_menu.twig +++ b/themes/default/_menu.twig | |||
@@ -2,6 +2,7 @@ | |||
2 | <li><a href="./" {% if view == 'home' %}class="current"{% endif %}>{% trans "home" %}</a></li> | 2 | <li><a href="./" {% if view == 'home' %}class="current"{% endif %}>{% trans "home" %}</a></li> |
3 | <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li> | 3 | <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li> |
4 | <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li> | 4 | <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li> |
5 | <li><a href="./?view=tags" {% if view == 'tags' %}class="current"{% endif %}>{% trans "tags" %}</a></li> | ||
5 | <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li> | 6 | <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li> |
6 | <li><a href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li> | 7 | <li><a href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li> |
7 | </ul> \ No newline at end of file | 8 | </ul> \ No newline at end of file |
diff --git a/themes/default/css/style.css b/themes/default/css/style.css index 670eb50f..2088ee2e 100644 --- a/themes/default/css/style.css +++ b/themes/default/css/style.css | |||
@@ -176,6 +176,12 @@ a:visited { | |||
176 | text-decoration: none; | 176 | text-decoration: none; |
177 | } | 177 | } |
178 | 178 | ||
179 | #article .tags { | ||
180 | font-size: 0.8em; | ||
181 | color: #888; | ||
182 | padding-bottom: 5px; | ||
183 | } | ||
184 | |||
179 | .backhome { | 185 | .backhome { |
180 | display: inline; | 186 | display: inline; |
181 | } | 187 | } |
diff --git a/themes/default/edit-tags.twig b/themes/default/edit-tags.twig new file mode 100644 index 00000000..7116bba9 --- /dev/null +++ b/themes/default/edit-tags.twig | |||
@@ -0,0 +1,20 @@ | |||
1 | {% extends "layout.twig" %} | ||
2 | {% block title %}edit tags{% endblock %} | ||
3 | {% block menu %} | ||
4 | {% include '_menu.twig' %} | ||
5 | {% endblock %} | ||
6 | {% block content %} | ||
7 | {% if tags is empty %} | ||
8 | no tags | ||
9 | {% endif %} | ||
10 | <ul> | ||
11 | {% for tag in tags %}<li>{{ tag.value }} <a href="./?action=remove_tag&tag_id={{ tag.id }}&id={{ entry_id }}">✘</a></li>{% endfor %} | ||
12 | </ul> | ||
13 | <form method="post" action="./?action=add_tag"> | ||
14 | <label for="value">New tags: </label><input type="text" id="value" name="value" required="required" /> | ||
15 | <p>{% trans "you can type several tags, separated by comma" %}</p> | ||
16 | <input type="hidden" name="entry_id" value="{{ entry_id }}" /> | ||
17 | <input type="submit" value="add tags" /> | ||
18 | </form> | ||
19 | <a href="./?view=view&id={{ entry_id }}">{% trans "back to the article" %}</a> | ||
20 | {% endblock %} \ No newline at end of file | ||
diff --git a/themes/default/img/default/rss.png b/themes/default/img/default/rss.png new file mode 100644 index 00000000..21bad1a1 --- /dev/null +++ b/themes/default/img/default/rss.png | |||
Binary files differ | |||
diff --git a/themes/default/tag.twig b/themes/default/tag.twig new file mode 100644 index 00000000..364c7cd4 --- /dev/null +++ b/themes/default/tag.twig | |||
@@ -0,0 +1,33 @@ | |||
1 | {% extends "layout.twig" %} | ||
2 | {% block title %}tag {% endblock %} | ||
3 | {% block menu %} | ||
4 | {% include '_menu.twig' %} | ||
5 | {% endblock %} | ||
6 | {% block content %} | ||
7 | <h3>{% trans "Tag" %} {{ tag.value }}</h3> | ||
8 | {% if entries is empty %} | ||
9 | <div class="messages warning"><p>{% trans "No link available here!" %}</p></div> | ||
10 | {% else %} | ||
11 | {% block pager %} | ||
12 | {% if nb_results > 1 %} | ||
13 | <div class="results"> | ||
14 | <div class="nb-results">{{ nb_results }} {% trans "results" %}</div> | ||
15 | {{ page_links | raw }} | ||
16 | </div> | ||
17 | {% endif %} | ||
18 | {% endblock %} | ||
19 | {% for entry in entries %} | ||
20 | <div id="entry-{{ entry.id|e }}" class="entrie"> | ||
21 | <h2><a href="index.php?view=view&id={{ entry.id|e }}">{{ entry.title|raw }}</a></h2> | ||
22 | <ul class="tools"> | ||
23 | <li><a title="{% trans "toggle mark as read" %}" class="tool {% if entry.is_read == 0 %}archive-off{% else %}archive{% endif %}" href="./?action=toggle_archive&id={{ entry.id|e }}"><span>{% trans "toggle mark as read" %}</span></a></li> | ||
24 | <li><a title="{% trans "toggle favorite" %}" class="tool {% if entry.is_fav == 0 %}fav-off{% else %}fav{% endif %}" href="./?action=toggle_fav&id={{ entry.id|e }}"><span>{% trans "toggle favorite" %}</span></a></li> | ||
25 | <li><a title="{% trans "delete" %}" class="tool delete" href="./?action=delete&id={{ entry.id|e }}"><span>{% trans "delete" %}</span></a></li> | ||
26 | <li><a href="{{ entry.url|e }}" target="_blank" title="{% trans "original" %} : {{ entry.title|e }}" class="tool link"><span>{{ entry.url | e | getDomain }}</span></a></li> | ||
27 | <li><a target="_blank" title="{% trans "estimated reading time:" %} {{ entry.content| getReadingTime }} min" class="reading-time"><span>{{ entry.content| getReadingTime }} min</span></a></li> | ||
28 | </ul> | ||
29 | <p>{{ entry.content|striptags|slice(0, 300) }}...</p> | ||
30 | </div> | ||
31 | {% endfor %} | ||
32 | {% endif %} | ||
33 | {% endblock %} \ No newline at end of file | ||
diff --git a/themes/default/tags.twig b/themes/default/tags.twig new file mode 100644 index 00000000..cff6b1d7 --- /dev/null +++ b/themes/default/tags.twig | |||
@@ -0,0 +1,8 @@ | |||
1 | {% extends "layout.twig" %} | ||
2 | {% block title %}tags{% endblock %} | ||
3 | {% block menu %} | ||
4 | {% include '_menu.twig' %} | ||
5 | {% endblock %} | ||
6 | {% block content %} | ||
7 | {% for tag in tags %}<a href="./?view=tag&id={{ tag.id }}">{{ tag.value }}</a> {% if token != '' %}<a href="?feed&type=tag&user_id={{ user_id }}&tag_id={{ tag.id }}&token={{ token }}" target="_blank"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/rss.png" /></a>{% endif %} {% endfor %} | ||
8 | {% endblock %} \ No newline at end of file | ||
diff --git a/themes/default/view.twig b/themes/default/view.twig index 1e54ae38..64672b61 100644 --- a/themes/default/view.twig +++ b/themes/default/view.twig | |||
@@ -20,6 +20,9 @@ | |||
20 | <header class="mbm"> | 20 | <header class="mbm"> |
21 | <h1>{{ entry.title|raw }}</h1> | 21 | <h1>{{ entry.title|raw }}</h1> |
22 | </header> | 22 | </header> |
23 | <aside class="tags"> | ||
24 | tags: {% for tag in tags %}<a href="./?view=tag&id={{ tag.id }}">{{ tag.value }}</a> {% endfor %}<a href="./?view=edit-tags&id={{ entry.id|e }}" title="{% trans "edit tags" %}">✎</a> | ||
25 | </aside> | ||
23 | <article> | 26 | <article> |
24 | {{ content | raw }} | 27 | {{ content | raw }} |
25 | </article> | 28 | </article> |