diff --git a/TODO b/TODO
index 9a36ba57..2590c2db 100644
--- a/TODO
+++ b/TODO
@@ -26,7 +26,6 @@ everything related to tags:
- basic tags
- merging
- tag editing
- - name
- category (from config.ini)
- description
- related tags
diff --git a/data/config.ini b/data/config.ini
index be888d38..62016ba5 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -57,6 +57,7 @@ changePostFlags = regularUser, powerUser, moderator, administrator
listTags = anonymous, regularUser, powerUser, moderator, administrator
massTag = powerUser, moderator, administrator
+changeTagName = moderator, administrator
listComments = anonymous, regularUser, powerUser, moderator, administrator
addComments = regularUser, powerUser, moderator, administrator
diff --git a/public_html/css/tags.css b/public_html/css/tags.css
index 3906c0b1..f7fdd5a0 100644
--- a/public_html/css/tags.css
+++ b/public_html/css/tags.css
@@ -17,3 +17,9 @@
#tag-view h3 {
margin-bottom: 0.5em;
}
+
+#tag-view form {
+ text-align: left;
+ max-width: 20em;
+ margin: 0 auto;
+}
diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js
index 152de6b8..04535268 100644
--- a/public_html/js/Auth.js
+++ b/public_html/js/Auth.js
@@ -43,6 +43,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
listTags: 'listTags',
massTag: 'massTag',
+ changeTagName: 'changeTagName',
viewHistory: 'viewHistory',
};
diff --git a/public_html/js/Presenters/TagPresenter.js b/public_html/js/Presenters/TagPresenter.js
index 6c6a2eaa..a5a19cef 100644
--- a/public_html/js/Presenters/TagPresenter.js
+++ b/public_html/js/Presenters/TagPresenter.js
@@ -6,19 +6,28 @@ App.Presenters.TagPresenter = function(
jQuery,
util,
promise,
+ auth,
api,
+ router,
keyboard,
- topNavigationPresenter) {
+ topNavigationPresenter,
+ messagePresenter) {
var $el = jQuery('#content');
+ var $messages = $el;
var templates = {};
+ var tag;
var tagName;
+ var privileges = {};
+
function init(params, loaded) {
topNavigationPresenter.select('tags');
topNavigationPresenter.changeTitle('Tags');
+ privileges.canChangeName = auth.hasPrivilege(auth.privileges.changeTagName);
+
promise.wait(
util.promiseTemplate('tag'),
util.promiseTemplate('post-list-item'))
@@ -33,22 +42,50 @@ App.Presenters.TagPresenter = function(
function reinit(params, loaded) {
tagName = params.tagName;
- render();
- loaded();
+ messagePresenter.hideMessages($messages);
- promise.wait(api.get('posts', {query: tagName}))
- .then(function(response) {
- var posts = response.json.data;
+ promise.wait(
+ api.get('tags/' + tagName),
+ api.get('posts', {query: tagName}))
+ .then(function(tagResponse, postsResponse) {
+ tag = tagResponse.json;
+ var posts = postsResponse.json.data;
posts = posts.slice(0, 8);
+
+ render();
+ loaded();
+
renderPosts(posts);
- }).fail(function(response) {
- console.log(new Error(response));
+ }).fail(function(tagResponse, postsResponse) {
+ messagePresenter.showError($messages, tagResponse.json.error || postsResponse.json.error);
+ loaded();
});
}
function render() {
- $el.html(templates.tag({tagName: tagName}));
+ $el.html(templates.tag({privileges: privileges, tag: tag, tagName: tagName}));
$el.find('.post-list').hide();
+ $el.find('form').submit(editFormSubmitted);
+ }
+
+ function editFormSubmitted(e) {
+ e.preventDefault();
+ var $form = $el.find('form');
+ var formData = {};
+
+ if (privileges.canChangeName) {
+ formData.name = $form.find('[name=name]').val();
+ }
+
+ promise.wait(api.put('/tags/' + tag.name, formData))
+ .then(function(response) {
+ tag = response.json;
+ render();
+ router.navigate('#/tag/' + tag.name);
+ }).fail(function(response) {
+ console.log(response);
+ window.alert('An error occurred');
+ });
}
function renderPosts(posts) {
@@ -76,4 +113,4 @@ App.Presenters.TagPresenter = function(
};
-App.DI.register('tagPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'keyboard', 'topNavigationPresenter'], App.Presenters.TagPresenter);
+App.DI.register('tagPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'api', 'router', 'keyboard', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.TagPresenter);
diff --git a/public_html/templates/tag.tpl b/public_html/templates/tag.tpl
index 2440825b..48ccc569 100644
--- a/public_html/templates/tag.tpl
+++ b/public_html/templates/tag.tpl
@@ -3,6 +3,26 @@
<%= tagName %>
+ <% if (_.any(privileges)) { %>
+
+ <% } %>
+
Example usages
diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php
index bb3b30d2..6c8df736 100644
--- a/src/Controllers/TagController.php
+++ b/src/Controllers/TagController.php
@@ -1,6 +1,7 @@
get('/api/tags', [$this, 'getTags']);
+ $router->get('/api/tags/:tagName', [$this, 'getTag']);
+ $router->put('/api/tags/:tagName', [$this, 'updateTag']);
+ }
+
+ public function getTag($tagName)
+ {
+ $this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
+
+ $tag = $this->tagService->getByName($tagName);
+ return $this->tagViewProxy->fromEntity($tag);
}
public function getTags()
@@ -49,4 +60,16 @@ final class TagController extends AbstractController
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
+
+ public function updateTag($tagName)
+ {
+ $tag = $this->tagService->getByName($tagName);
+ $formData = new TagEditFormData($this->inputReader);
+
+ if ($formData->name !== null)
+ $this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_NAME);
+
+ $tag = $this->tagService->updateTag($tag, $formData);
+ return $this->tagViewProxy->fromEntity($tag);
+ }
}
diff --git a/src/Dao/AbstractDao.php b/src/Dao/AbstractDao.php
index be87b67b..3ea71b66 100644
--- a/src/Dao/AbstractDao.php
+++ b/src/Dao/AbstractDao.php
@@ -242,12 +242,15 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
$query->where($sql, ...$bindings);
}
- protected function arrayToEntities(array $arrayEntities)
+ protected function arrayToEntities(array $arrayEntities, $entityConverter = null)
{
+ if ($entityConverter === null)
+ $entityConverter = $this->entityConverter;
+
$entities = [];
foreach ($arrayEntities as $arrayEntity)
{
- $entity = $this->entityConverter->toEntity($arrayEntity);
+ $entity = $entityConverter->toEntity($arrayEntity);
$entities[$entity->getId()] = $entity;
}
return $entities;
diff --git a/src/Dao/PostDao.php b/src/Dao/PostDao.php
index 42254a83..4d6dbfb5 100644
--- a/src/Dao/PostDao.php
+++ b/src/Dao/PostDao.php
@@ -52,6 +52,17 @@ class PostDao extends AbstractDao implements ICrudDao
return $this->findOneBy('name', $name);
}
+ public function findByTagName($tagName)
+ {
+ $query = $this->fpdo->from('posts')
+ ->disableSmartJoin()
+ ->innerJoin('postTags ON postTags.postId = posts.id')
+ ->innerJoin('tags ON postTags.tagId = tags.id')
+ ->where('tags.name', $tagName);
+ $arrayEntities = iterator_to_array($query);
+ return $this->arrayToEntities($arrayEntities);
+ }
+
public function findByContentChecksum($checksum)
{
return $this->findOneBy('contentChecksum', $checksum);
diff --git a/src/Dao/TagDao.php b/src/Dao/TagDao.php
index e69324d3..13bece48 100644
--- a/src/Dao/TagDao.php
+++ b/src/Dao/TagDao.php
@@ -1,5 +1,6 @@
findOneBy('name', $tagName);
+ }
+
public function findByNames($tagNames)
{
return $this->findBy('name', $tagNames);
diff --git a/src/FormData/TagEditFormData.php b/src/FormData/TagEditFormData.php
new file mode 100644
index 00000000..fb5cbd91
--- /dev/null
+++ b/src/FormData/TagEditFormData.php
@@ -0,0 +1,24 @@
+name = $inputReader->name;
+ }
+ }
+
+ public function validate(Validator $validator)
+ {
+ if ($this->name !== null)
+ $validator->validatePostTags([$this->name]);
+ }
+}
+
diff --git a/src/Privilege.php b/src/Privilege.php
index 1d8308cd..4a76505b 100644
--- a/src/Privilege.php
+++ b/src/Privilege.php
@@ -36,6 +36,7 @@ class Privilege
const LIST_TAGS = 'listTags';
const MASS_TAG = 'massTag';
+ const CHANGE_TAG_NAME = 'changeTagName';
const LIST_COMMENTS = 'listComments';
const ADD_COMMENTS = 'addComments';
diff --git a/src/Services/TagService.php b/src/Services/TagService.php
index 72009443..aa53e357 100644
--- a/src/Services/TagService.php
+++ b/src/Services/TagService.php
@@ -1,31 +1,55 @@
validator = $validator;
$this->transactionManager = $transactionManager;
+ $this->postDao = $postDao;
$this->tagDao = $tagDao;
$this->fileDao = $fileDao;
+ $this->historyService = $historyService;
$this->timeService = $timeService;
}
+ public function getByName($tagName)
+ {
+ $transactionFunc = function() use ($tagName)
+ {
+ $tag = $this->tagDao->findByName($tagName);
+ if (!$tag)
+ throw new \InvalidArgumentException('Tag with name "' . $tagName . '" was not found.');
+ return $tag;
+ };
+ return $this->transactionManager->rollback($transactionFunc);
+ }
+
public function getFiltered(TagFilter $filter)
{
$transactionFunc = function() use ($filter)
@@ -96,4 +120,34 @@ class TagService
};
return $this->transactionManager->commit($transactionFunc);
}
+
+ public function updateTag(Tag $tag, TagEditFormData $formData)
+ {
+ $transactionFunc = function() use ($tag, $formData)
+ {
+ $this->validator->validate($formData);
+
+ if ($formData->name !== null)
+ $this->updateTagName($tag, $formData->name);
+
+ return $this->tagDao->save($tag);
+ };
+ $ret = $this->transactionManager->commit($transactionFunc);
+
+ $transactionFunc = function() use ($tag)
+ {
+ $posts = $this->postDao->findByTagName($tag->getName());
+ foreach ($posts as $post)
+ $this->historyService->saveSnapshot($this->historyService->getPostChangeSnapshot($post));
+ };
+ $this->transactionManager->commit($transactionFunc);
+
+ $this->exportJson();
+ return $ret;
+ }
+
+ private function updateTagName(Tag $tag, $newName)
+ {
+ $tag->setName($newName);
+ }
}