diff --git a/app/components/link-blog.vue b/app/components/link-blog.vue index 67f5a1d..a6bd948 100644 --- a/app/components/link-blog.vue +++ b/app/components/link-blog.vue @@ -6,7 +6,10 @@ @@ -26,7 +29,8 @@ data: function () { return { - posts: [] + posts: [], + tags: [] } }, @@ -35,6 +39,9 @@ this.$http.get('api/blog/post', {filter: {limit: 1000}}).then(function (res) { this.$set('posts', res.data.posts); }); + this.$http.get('api/blog/tag', {filter: {limit: 1000}}).then(function (res) { + this.$set('tags', res.data.tags); + }); }, ready: function() { @@ -43,8 +50,11 @@ filters: { - link: function (post) { + postlink: function (post) { return '@blog/id?id='+post.id; + }, + taglink: function (tag) { + return '@blog/tag?id='+tag.id; } } diff --git a/app/components/post-settings.vue b/app/components/post-settings.vue index c00e07d..4947f11 100644 --- a/app/components/post-settings.vue +++ b/app/components/post-settings.vue @@ -58,6 +58,17 @@ +
+ {{ 'Tags' | trans }} +
+ +
+
+
{{ 'Restrict Access' | trans }}
diff --git a/app/views/admin/post-edit.js b/app/views/admin/post-edit.js index cbbdca1..6981d09 100644 --- a/app/views/admin/post-edit.js +++ b/app/views/admin/post-edit.js @@ -7,7 +7,7 @@ window.Post = { data: window.$data, post: window.$data.post, sections: [] - } + }; }, created: function () { diff --git a/index.php b/index.php index 41b681e..91b00e7 100644 --- a/index.php +++ b/index.php @@ -36,6 +36,7 @@ 'name' => '@blog/api', 'controller' => [ 'Pagekit\\Blog\\Controller\\PostApiController', + 'Pagekit\\Blog\\Controller\\TagApiController', 'Pagekit\\Blog\\Controller\\CommentApiController' ] ] @@ -159,6 +160,26 @@ new PostListener(), new ReadmorePlugin ); + //temp add db in dev environment + //todo remove here, add to update/install scripts + $util = $app['db']->getUtility(); + if ($util->tableExists('@blog_tag') === false) { + $util->createTable('@blog_tag', function ($table) { + $table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]); + $table->addColumn('name', 'string', ['length' => 255]); + $table->addColumn('slug', 'string', ['length' => 255]); + $table->setPrimaryKey(['id']); + }); + } + + if ($util->tableExists('@blog_post_tags') === false) { + $util->createTable('@blog_post_tags', function ($table) { + $table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]); + $table->addColumn('post_id', 'integer', ['unsigned' => true, 'length' => 10]); + $table->addColumn('tag_id', 'integer', ['unsigned' => true, 'length' => 10]); + $table->setPrimaryKey(['id']); + }); + } }, 'view.scripts' => function ($event, $scripts) { diff --git a/scripts.php b/scripts.php index d1f86d9..45bbf5d 100644 --- a/scripts.php +++ b/scripts.php @@ -50,6 +50,24 @@ $table->addIndex(['post_id', 'status'], '@BLOG_COMMENT_POST_ID_STATUS'); }); } + + if ($util->tableExists('@blog_tag') === false) { + $util->createTable('@blog_tag', function ($table) { + $table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]); + $table->addColumn('name', 'string', ['length' => 255]); + $table->addColumn('slug', 'string', ['length' => 255]); + $table->setPrimaryKey(['id']); + }); + } + + if ($util->tableExists('@blog_post_tags') === false) { + $util->createTable('@blog_post_tags', function ($table) { + $table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]); + $table->addColumn('post_id', 'integer', ['unsigned' => true, 'length' => 10]); + $table->addColumn('tag_id', 'integer', ['unsigned' => true, 'length' => 10]); + $table->setPrimaryKey(['id']); + }); + } }, @@ -64,6 +82,14 @@ if ($util->tableExists('@blog_comment')) { $util->dropTable('@blog_comment'); } + + if ($util->tableExists('@blog_tag')) { + $util->dropTable('@blog_tag'); + } + + if ($util->tableExists('@blog_post_tags')) { + $util->dropTable('@blog_post_tags'); + } }, 'updates' => [ diff --git a/src/Controller/BlogController.php b/src/Controller/BlogController.php index d4312ef..c1506a5 100644 --- a/src/Controller/BlogController.php +++ b/src/Controller/BlogController.php @@ -5,6 +5,7 @@ use Pagekit\Application as App; use Pagekit\Blog\Model\Comment; use Pagekit\Blog\Model\Post; +use Pagekit\Blog\Model\Tag; use Pagekit\User\Model\Role; /** @@ -44,7 +45,7 @@ public function editAction($id = 0) { try { - if (!$post = Post::where(compact('id'))->related('user')->first()) { + if (!$post = Post::where(compact('id'))->related('user', 'tags')->first()) { if ($id) { App::abort(404, __('Invalid post id.')); @@ -88,6 +89,8 @@ public function editAction($id = 0) ], '$data' => [ 'post' => $post, + 'post_tags' => array_values($post->tags ? : []), + 'tags' => array_values(Tag::findAll()), 'statuses' => Post::getStatuses(), 'roles' => array_values(Role::findAll()), 'canEditAll' => $user->hasAccess('blog: manage all posts'), diff --git a/src/Controller/PostApiController.php b/src/Controller/PostApiController.php index 329a57b..ed1c23a 100644 --- a/src/Controller/PostApiController.php +++ b/src/Controller/PostApiController.php @@ -51,7 +51,7 @@ public function indexAction($filter = [], $page = 0) $pages = ceil($count / $limit); $page = max(0, min($pages - 1, $page)); - $posts = array_values($query->offset($page * $limit)->related('user', 'comments')->limit($limit)->orderBy($order[1], $order[2])->get()); + $posts = array_values($query->offset($page * $limit)->related('user', 'comments', 'tags')->limit($limit)->orderBy($order[1], $order[2])->get()); return compact('posts', 'pages', 'count'); } diff --git a/src/Controller/SiteController.php b/src/Controller/SiteController.php index 1324f56..69af60e 100644 --- a/src/Controller/SiteController.php +++ b/src/Controller/SiteController.php @@ -25,21 +25,26 @@ public function __construct() * @Route("/") * @Route("/page/{page}", name="page", requirements={"page" = "\d+"}) */ - public function indexAction($page = 1) + public function indexAction($page = 1, $tag_id = 0) { if (!App::node()->hasAccess(App::user())) { App::abort(403, __('Insufficient User Rights.')); } - $query = Post::where(['status = ?', 'date < ?'], [Post::STATUS_PUBLISHED, new \DateTime])->where(function ($query) { + $query = Post::where(['status = ?', 'date < ?'], [Post::STATUS_PUBLISHED, new \DateTime])->from('@blog_post i')->where(function ($query) { return $query->where('roles IS NULL')->whereInSet('roles', App::user()->roles, false, 'OR'); - })->related('user'); + })->related('user', 'tags'); + + if ($tag_id) { + //taxonomy trick? + $query->join('@blog_post_tags t', 'post_id = i.id')->where('tag_id = ?', [$tag_id]); + } if (!$limit = $this->blog->config('posts.posts_per_page')) { $limit = 10; } - $count = $query->count('id'); + $count = $query->count('i.id'); $total = ceil($count / $limit); $page = max(1, min($total, $page)); @@ -97,7 +102,7 @@ public function feedAction($type = '') foreach (Post::where(['status = ?', 'date < ?'], [Post::STATUS_PUBLISHED, new \DateTime])->where(function ($query) { return $query->where('roles IS NULL')->whereInSet('roles', App::user()->roles, false, 'OR'); - })->related('user')->limit($this->blog->config('feed.limit'))->orderBy('date', 'DESC')->get() as $post) { + })->related('user', 'tags')->limit($this->blog->config('feed.limit'))->orderBy('date', 'DESC')->get() as $post) { $url = App::url('@blog/id', ['id' => $post->id], 0); $feed->addItem( $feed->createItem([ @@ -119,7 +124,7 @@ public function feedAction($type = '') */ public function postAction($id = 0) { - if (!$post = Post::where(['id = ?', 'status = ?', 'date < ?'], [$id, Post::STATUS_PUBLISHED, new \DateTime])->related('user')->first()) { + if (!$post = Post::where(['id = ?', 'status = ?', 'date < ?'], [$id, Post::STATUS_PUBLISHED, new \DateTime])->related('user', 'tags')->first()) { App::abort(404, __('Post not found!')); } @@ -168,4 +173,13 @@ public function postAction($id = 0) 'post' => $post ]; } + + /** + * @Route("/tag/{id}", name="tag") + * @Route("/tag/{id}/page/{page}", name="tag/page", requirements={"page" = "\d+", "id" = "\d+"}) + */ + public function tagAction($page = 1, $id = '') + { + return $this->indexAction($page, $id); + } } diff --git a/src/Controller/TagApiController.php b/src/Controller/TagApiController.php new file mode 100644 index 0000000..d008d29 --- /dev/null +++ b/src/Controller/TagApiController.php @@ -0,0 +1,113 @@ +where(function ($query) use ($search) { + $query->orWhere(['name LIKE :search', 'slug LIKE :search'], ['search' => "%{$search}%"]); + }); + } + + if (!preg_match('/^(name|slug)\s(asc|desc)$/i', $order, $order)) { + $order = [1 => 'name', 2 => 'asc']; + } + + $limit = (int) $limit ?: App::module('blog')->config('posts.posts_per_page'); + $count = $query->count(); + $pages = ceil($count / $limit); + $page = max(0, min($pages - 1, $page)); + + $tags = array_values($query->offset($page * $limit)->limit($limit)->orderBy($order[1], $order[2])->get()); + + return compact('tags', 'pages', 'count'); + } + + /** + * @Route("/", methods="POST") + * @Route("/{id}", methods="POST", requirements={"id"="\d+"}) + * @Request({"tag": "array", "id": "int"}, csrf=true) + */ + public function saveAction($data, $id = 0) + { + if (!$id || !$tag = Tag::find($id)) { + + if ($id) { + App::abort(404, __('Post not found.')); + } + + $tag = Tag::create(); + } + + if (!$data['slug'] = App::filter($data['slug'] ?: $data['name'], 'slugify')) { + App::abort(400, __('Invalid slug.')); + } + + $tag->save($data); + + return ['message' => 'success', 'tag' => $tag]; + } + + /** + * @Route("/{id}", methods="DELETE", requirements={"id"="\d+"}) + * @Request({"id": "int"}, csrf=true) + */ + public function deleteAction($id) + { + if ($tag = Tag::find($id)) { + + if(!App::user()->hasAccess('blog: manage all posts')) { + App::abort(400, __('Access denied.')); + } + + $tag->delete(); + } + + return ['message' => 'success']; + } + + /** + * @Route("/bulk", methods="POST") + * @Request({"tags": "array"}, csrf=true) + */ + public function bulkSaveAction($tags = []) + { + foreach ($tags as $data) { + $this->saveAction($data, isset($data['id']) ? $data['id'] : 0); + } + + return ['message' => 'success']; + } + + /** + * @Route("/bulk", methods="DELETE") + * @Request({"ids": "array"}, csrf=true) + */ + public function bulkDeleteAction($ids = []) + { + foreach (array_filter($ids) as $id) { + $this->deleteAction($id); + } + + return ['message' => 'success']; + } +} diff --git a/src/Event/RouteListener.php b/src/Event/RouteListener.php index 3840293..90ac0b5 100644 --- a/src/Event/RouteListener.php +++ b/src/Event/RouteListener.php @@ -24,6 +24,9 @@ public function onConfigureRoute($event, $route) if ($route->getName() == '@blog/id' && UrlResolver::getPermalink()) { App::routes()->alias(dirname($route->getPath()).'/'.ltrim(UrlResolver::getPermalink(), '/'), '@blog/id', ['_resolver' => 'Pagekit\Blog\UrlResolver']); } + if ($route->getName() == '@blog/tag') { + App::routes()->alias(dirname($route->getPath()).'/{tag}/', '@blog/tag', ['_resolver' => 'Pagekit\Blog\TagUrlResolver']); + } } /** diff --git a/src/Model/Post.php b/src/Model/Post.php index a94f9c4..d871f66 100755 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -70,11 +70,22 @@ class Post implements \JsonSerializable */ public $comments; + /** + * @ManyToMany( + * targetEntity="Tag", + * tableThrough="@blog_post_tags", + * keyThroughFrom="post_id", + * keyThroughTo="tag_id" + * ) + */ + public $tags; + /** @var array */ protected static $properties = [ 'author' => 'getAuthor', 'published' => 'isPublished', - 'accessible' => 'isAccessible' + 'accessible' => 'isAccessible', + 'tag_names' => 'getTagNames' ]; public static function getStatuses() @@ -117,6 +128,18 @@ public function isAccessible(User $user = null) return $this->isPublished() && $this->hasAccess($user ?: App::user()); } + /** + * @return array + */ + public function getTagNames () { + if ($this->tags) { + return array_values(array_map(function ($tag) { + return $tag->name; + }, $this->tags)); + } + return []; + } + /** * {@inheritdoc} */ diff --git a/src/Model/PostTags.php b/src/Model/PostTags.php new file mode 100644 index 0000000..7511f6f --- /dev/null +++ b/src/Model/PostTags.php @@ -0,0 +1,31 @@ +toArray(); + } +} diff --git a/src/Model/Tag.php b/src/Model/Tag.php new file mode 100644 index 0000000..ebc08e4 --- /dev/null +++ b/src/Model/Tag.php @@ -0,0 +1,40 @@ +id; + + if (!$tag->slug) { + $tag->slug = $tag->title; + } + + while (self::where(['slug = ?'], [$tag->slug])->where(function ($query) use ($id) { + if ($id) $query->where('id <> ?', [$id]); + })->first()) { + $tag->slug = preg_replace('/-\d+$/', '', $tag->slug).'-'.$i++; + } + + } + +} diff --git a/src/TagUrlResolver.php b/src/TagUrlResolver.php new file mode 100644 index 0000000..6fb7b53 --- /dev/null +++ b/src/TagUrlResolver.php @@ -0,0 +1,109 @@ +cacheEntries = App::cache()->fetch(self::CACHE_KEY) ?: []; + } + + /** + * {@inheritdoc} + */ + public function match(array $parameters = []) + { + if (isset($parameters['id'])) { + return $parameters; + } + + if (!isset($parameters['tag'])) { + App::abort(404, 'Tag not found.'); + } + + $slug = $parameters['tag']; + + $id = false; + foreach ($this->cacheEntries as $entry) { + if ($entry['slug'] === $slug) { + $id = $entry['id']; + } + } + + if (!$id) { + + if (!$tag = Tag::where(compact('slug'))->first()) { + App::abort(404, 'Tag not found.'); + } + + $this->addCache($tag); + $id = $tag->id; + } + + $parameters['id'] = $id; + return $parameters; + } + + /** + * {@inheritdoc} + */ + public function generate(array $parameters = []) + { + $id = $parameters['id']; + + if (!isset($this->cacheEntries[$id])) { + + if (!$tag = Tag::where(compact('id'))->first()) { + throw new RouteNotFoundException('Tag not found!'); + } + + $this->addCache($tag); + } + + $meta = $this->cacheEntries[$id]; + + $parameters['tag'] = $meta['slug']; + + unset($parameters['id']); + return $parameters; + } + + public function __destruct() + { + if ($this->cacheDirty) { + App::cache()->save(self::CACHE_KEY, $this->cacheEntries); + } + } + + protected function addCache($tag) + { + $this->cacheEntries[$tag->id] = [ + 'id' => $tag->id, + 'slug' => $tag->slug + ]; + + $this->cacheDirty = true; + } +} diff --git a/views/admin/post-index.php b/views/admin/post-index.php index cc9aa80..acedc15 100644 --- a/views/admin/post-index.php +++ b/views/admin/post-index.php @@ -48,6 +48,7 @@ {{ 'Comments' | trans }} + {{ 'Tags' | trans }} {{ 'Date' | trans }} {{ 'URL' | trans }} @@ -73,6 +74,9 @@ {{ post.comment_count }} + + {{ post.tag_names }} + {{ post.date | date }} diff --git a/views/post.php b/views/post.php index d892997..11c2a91 100644 --- a/views/post.php +++ b/views/post.php @@ -10,6 +10,10 @@
content ?>
diff --git a/views/posts.php b/views/posts.php index d03b231..83dae68 100755 --- a/views/posts.php +++ b/views/posts.php @@ -11,6 +11,10 @@
excerpt ?: $post->content ?>