From a042f04fa35e17e0e6bc4769400ad7debd67e725 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 4 Aug 2015 13:04:22 +0930 Subject: [PATCH] Enforce min/max tag count settings --- .../src/components/TagDiscussionModal.js | 65 +++++++++++++++---- extensions/tags/js/lib/models/Tag.js | 5 +- .../tags/less/forum/TagDiscussionModal.less | 6 +- extensions/tags/locale/en.yml | 7 +- .../tags/src/Listeners/AddApiAttributes.php | 9 +++ .../tags/src/Listeners/AddClientAssets.php | 3 +- extensions/tags/src/Listeners/PersistData.php | 43 +++++++++++- extensions/tags/src/TagCountException.php | 7 ++ 8 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 extensions/tags/src/TagCountException.php diff --git a/extensions/tags/js/forum/src/components/TagDiscussionModal.js b/extensions/tags/js/forum/src/components/TagDiscussionModal.js index 090811a1f..ddd6ddadf 100644 --- a/extensions/tags/js/forum/src/components/TagDiscussionModal.js +++ b/extensions/tags/js/forum/src/components/TagDiscussionModal.js @@ -3,6 +3,7 @@ import DiscussionPage from 'flarum/components/DiscussionPage'; import Button from 'flarum/components/Button'; import highlight from 'flarum/helpers/highlight'; import classList from 'flarum/utils/classList'; +import extractText from 'flarum/utils/extractText'; import tagLabel from 'tags/helpers/tagLabel'; import tagIcon from 'tags/helpers/tagIcon'; @@ -24,6 +25,19 @@ export default class TagDiscussionModal extends Modal { } else if (this.props.discussion) { this.props.discussion.tags().map(this.addTag.bind(this)); } + + this.minPrimary = app.forum.attribute('minPrimaryTags'); + this.maxPrimary = app.forum.attribute('maxPrimaryTags'); + this.minSecondary = app.forum.attribute('minSecondaryTags'); + this.maxSecondary = app.forum.attribute('maxSecondaryTags'); + } + + primaryCount() { + return this.selected.filter(tag => tag.isPrimary()).length; + } + + secondaryCount() { + return this.selected.filter(tag => !tag.isPrimary()).length; } /** @@ -75,17 +89,46 @@ export default class TagDiscussionModal extends Modal { : app.trans('tags.tag_new_discussion_title'); } + getInstruction(primaryCount, secondaryCount) { + if (primaryCount < this.minPrimary) { + return app.trans('tags.choose_primary_tags', {count: this.minPrimary - primaryCount}); + } else if (secondaryCount < this.minSecondary) { + return app.trans('tags.choose_secondary_tags', {count: this.minSecondary - secondaryCount}); + } + + return ''; + } + content() { let tags = this.tags; const filter = this.filter().toLowerCase(); + const primaryCount = this.primaryCount(); + const secondaryCount = this.secondaryCount(); + // Filter out all child tags whose parents have not been selected. This + // makes it impossible to select a child if its parent hasn't been selected. + tags = tags.filter(tag => { + const parent = tag.parent(); + return parent === false || this.selected.indexOf(parent) !== -1; + }); + + // If the number of selected primary/secondary tags is at the maximum, then + // we'll filter out all other tags of that type. + if (primaryCount >= app.forum.attribute('maxPrimaryTags')) { + tags = tags.filter(tag => !tag.isPrimary() || this.selected.indexOf(tag) !== -1); + } + + if (secondaryCount >= app.forum.attribute('maxSecondaryTags')) { + tags = tags.filter(tag => tag.isPrimary() || this.selected.indexOf(tag) !== -1); + } + + // If the user has entered text in the filter input, then filter by tags + // whose name matches what they've entered. if (filter) { tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter); } - if (tags.indexOf(this.index) === -1) { - this.index = tags[0]; - } + if (tags.indexOf(this.index) === -1) this.index = tags[0]; return [
@@ -103,7 +146,7 @@ export default class TagDiscussionModal extends Modal { )}
]; diff --git a/extensions/tags/js/lib/models/Tag.js b/extensions/tags/js/lib/models/Tag.js index 0cfcb74a7..391a4a7bc 100644 --- a/extensions/tags/js/lib/models/Tag.js +++ b/extensions/tags/js/lib/models/Tag.js @@ -1,5 +1,6 @@ import Model from 'flarum/Model'; import mixin from 'flarum/utils/mixin'; +import computed from 'flarum/utils/computed'; export default class Tag extends mixin(Model, { name: Model.attribute('name'), @@ -21,5 +22,7 @@ export default class Tag extends mixin(Model, { lastDiscussion: Model.hasOne('lastDiscussion'), isRestricted: Model.attribute('isRestricted'), - canStartDiscussion: Model.attribute('canStartDiscussion') + canStartDiscussion: Model.attribute('canStartDiscussion'), + + isPrimary: computed('position', 'parent', (position, parent) => position !== null && parent === false) }) {} diff --git a/extensions/tags/less/forum/TagDiscussionModal.less b/extensions/tags/less/forum/TagDiscussionModal.less index e5ba8bd0f..2cdb63cbe 100644 --- a/extensions/tags/less/forum/TagDiscussionModal.less +++ b/extensions/tags/less/forum/TagDiscussionModal.less @@ -58,8 +58,12 @@ cursor: not-allowed; } .TagsInput-selected { - .TagLabel { + .TagsInput-tag { margin-right: 5px; + + &:last-child { + margin-right: 10px; + } } } diff --git a/extensions/tags/locale/en.yml b/extensions/tags/locale/en.yml index d536396d3..66846de51 100644 --- a/extensions/tags/locale/en.yml +++ b/extensions/tags/locale/en.yml @@ -7,7 +7,12 @@ tags: tag_new_discussion_title: Choose Tags for Your Discussion edit_discussion_tags_title: "Edit Tags for {title}" edit_discussion_tags_link: Edit Tags - discussion_tags_placeholder: Choose one or more topics + choose_primary_tags: + one: Choose a primary tag + other: "Choose {count} primary tags" + choose_secondary_tags: + one: Choose 1 more tag + other: "Choose {count} more tags" confirm: Confirm more: More... tag_cloud_title: Tags diff --git a/extensions/tags/src/Listeners/AddApiAttributes.php b/extensions/tags/src/Listeners/AddApiAttributes.php index 33ddaf64e..8668b50bd 100755 --- a/extensions/tags/src/Listeners/AddApiAttributes.php +++ b/extensions/tags/src/Listeners/AddApiAttributes.php @@ -70,6 +70,15 @@ class AddApiAttributes if ($event->serializer instanceof DiscussionSerializer) { $event->attributes['canTag'] = $event->model->can($event->actor, 'tag'); } + + if ($event->serializer instanceof ForumSerializer) { + $settings = app('Flarum\Core\Settings\SettingsRepository'); + + $event->attributes['minPrimaryTags'] = $settings->get('tags.min_primary_tags'); + $event->attributes['maxPrimaryTags'] = $settings->get('tags.max_primary_tags'); + $event->attributes['minSecondaryTags'] = $settings->get('tags.min_secondary_tags'); + $event->attributes['maxSecondaryTags'] = $settings->get('tags.max_secondary_tags'); + } } public function addRoutes(RegisterApiRoutes $event) diff --git a/extensions/tags/src/Listeners/AddClientAssets.php b/extensions/tags/src/Listeners/AddClientAssets.php index 242c28fb4..9d311ad82 100755 --- a/extensions/tags/src/Listeners/AddClientAssets.php +++ b/extensions/tags/src/Listeners/AddClientAssets.php @@ -35,7 +35,8 @@ class AddClientAssets 'tags.tag_new_discussion_title', 'tags.edit_discussion_tags_title', 'tags.edit_discussion_tags_link', - 'tags.discussion_tags_placeholder', + 'tags.choose_primary_tags', + 'tags.choose_secondary_tags', 'tags.confirm', 'tags.more', 'tags.tag_cloud_title', diff --git a/extensions/tags/src/Listeners/PersistData.php b/extensions/tags/src/Listeners/PersistData.php index 6099b1f19..cacf96b9c 100755 --- a/extensions/tags/src/Listeners/PersistData.php +++ b/extensions/tags/src/Listeners/PersistData.php @@ -5,9 +5,18 @@ use Flarum\Tags\Events\DiscussionWasTagged; use Flarum\Events\DiscussionWillBeSaved; use Flarum\Core\Discussions\Discussion; use Flarum\Core\Exceptions\PermissionDeniedException; +use Flarum\Core\Settings\SettingsRepository; +use Flarum\Tags\TagCountException; class PersistData { + protected $settings; + + public function __construct(SettingsRepository $settings) + { + $this->settings = $settings; + } + public function subscribe($events) { $events->listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']); @@ -25,13 +34,25 @@ class PersistData $newTagIds[] = (int) $link['id']; } - $newTags = Tag::whereIn('id', $newTagIds); + $newTags = Tag::whereIn('id', $newTagIds)->get(); + $primaryCount = 0; + $secondaryCount = 0; + foreach ($newTags as $tag) { if (! $tag->can($actor, 'startDiscussion')) { throw new PermissionDeniedException; } + + if ($tag->position !== null && $tag->parent_id === null) { + $primaryCount++; + } else { + $secondaryCount++; + } } + $this->validatePrimaryTagCount($primaryCount); + $this->validateSecondaryTagCount($secondaryCount); + $oldTags = []; if ($discussion->exists) { @@ -50,4 +71,24 @@ class PersistData }); } } + + protected function validatePrimaryTagCount($count) + { + $min = $this->settings->get('tags.min_primary_tags'); + $max = $this->settings->get('tags.max_primary_tags'); + + if ($count < $min || $count > $max) { + throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d primary tags.', $min, $max)]); + } + } + + protected function validateSecondaryTagCount($count) + { + $min = $this->settings->get('tags.min_secondary_tags'); + $max = $this->settings->get('tags.max_secondary_tags'); + + if ($count < $min || $count > $max) { + throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d secondary tags.', $min, $max)]); + } + } } diff --git a/extensions/tags/src/TagCountException.php b/extensions/tags/src/TagCountException.php new file mode 100644 index 000000000..b59614d2b --- /dev/null +++ b/extensions/tags/src/TagCountException.php @@ -0,0 +1,7 @@ +