mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-05-01 16:34:14 -04:00
FEATURE: tag groups
This commit is contained in:
parent
a3f1daca5d
commit
f3f6c2f98f
30 changed files with 434 additions and 67 deletions
app
assets
javascripts/discourse
components
controllers
models
routes
templates
stylesheets
controllers
models
serializers
config
db/migrate
lib
spec/components
|
@ -0,0 +1,29 @@
|
||||||
|
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||||
|
import DropdownButton from 'discourse/components/dropdown-button';
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default DropdownButton.extend({
|
||||||
|
buttonExtraClasses: 'no-text',
|
||||||
|
title: '',
|
||||||
|
text: iconHTML('bars') + ' ' + iconHTML('caret-down'),
|
||||||
|
classNames: ['tags-admin-menu'],
|
||||||
|
|
||||||
|
@computed()
|
||||||
|
dropDownContent() {
|
||||||
|
const items = [
|
||||||
|
{ id: 'manageGroups',
|
||||||
|
title: I18n.t('tagging.manage_groups'),
|
||||||
|
description: I18n.t('tagging.manage_groups_description'),
|
||||||
|
styleClasses: 'fa fa-wrench' }
|
||||||
|
];
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
actionNames: {
|
||||||
|
manageGroups: 'showTagGroups'
|
||||||
|
},
|
||||||
|
|
||||||
|
clicked(id) {
|
||||||
|
this.sendAction('actionNames.' + id);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this.get('model').save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default Ember.ArrayController.extend({
|
||||||
|
actions: {
|
||||||
|
selectTagGroup: function(tagGroup) {
|
||||||
|
if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); }
|
||||||
|
this.set('selectedItem', tagGroup);
|
||||||
|
tagGroup.set('selected', true);
|
||||||
|
tagGroup.set('savingStatus', null);
|
||||||
|
this.transitionToRoute('tagGroups.show', tagGroup);
|
||||||
|
},
|
||||||
|
|
||||||
|
newTagGroup: function() {
|
||||||
|
const newTagGroup = this.store.createRecord('tag-group');
|
||||||
|
newTagGroup.set('name', I18n.t('tagging.groups.new_name'));
|
||||||
|
this.pushObject(newTagGroup);
|
||||||
|
this.send('selectTagGroup', newTagGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
34
app/assets/javascripts/discourse/models/tag-group.js.es6
Normal file
34
app/assets/javascripts/discourse/models/tag-group.js.es6
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import RestModel from 'discourse/models/rest';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
const TagGroup = RestModel.extend({
|
||||||
|
@computed('name', 'tag_names')
|
||||||
|
disableSave() {
|
||||||
|
return Ember.isEmpty(this.get('name')) || Ember.isEmpty(this.get('tag_names')) || this.get('saving');
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
var url = "/tag_groups",
|
||||||
|
self = this;
|
||||||
|
if (this.get('id')) {
|
||||||
|
url = "/tag_groups/" + this.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('savingStatus', I18n.t('saving'));
|
||||||
|
this.set('saving', true);
|
||||||
|
|
||||||
|
return Discourse.ajax(url, {
|
||||||
|
data: {
|
||||||
|
name: this.get('name'),
|
||||||
|
tag_names: this.get('tag_names')
|
||||||
|
},
|
||||||
|
type: this.get('id') ? 'PUT' : 'POST'
|
||||||
|
}).then(function(result) {
|
||||||
|
if(result.id) { self.set('id', result.id); }
|
||||||
|
self.set('savingStatus', I18n.t('saved'));
|
||||||
|
self.set('saving', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TagGroup;
|
|
@ -131,4 +131,8 @@ export default function() {
|
||||||
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resource('tagGroups', {path: '/tag_groups'}, function() {
|
||||||
|
this.route('show', {path: '/:id'});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
return this.store.find('tagGroup', params.id);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return this.store.findAll('tagGroup');
|
||||||
|
},
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return I18n.t("tagging.groups.title");
|
||||||
|
},
|
||||||
|
});
|
|
@ -18,6 +18,11 @@ export default Discourse.Route.extend({
|
||||||
didTransition() {
|
didTransition() {
|
||||||
this.controllerFor("application").set("showFooter", true);
|
this.controllerFor("application").set("showFooter", true);
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
showTagGroups() {
|
||||||
|
this.transitionTo('tagGroups');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="tag-group-content">
|
||||||
|
<p class="about">{{i18n 'tagging.groups.about'}}</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="tag-group-content">
|
||||||
|
<h1>{{text-field value=model.name}}</h1>
|
||||||
|
<br/>
|
||||||
|
<div class="group-tags-list">
|
||||||
|
<label>{{i18n 'tagging.groups.tags_label'}}</label>
|
||||||
|
<br/>
|
||||||
|
{{tag-chooser tags=model.tag_names everyTag="false" unlimitedTagCount="true"}}
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'tagging.groups.save'}}</button>
|
||||||
|
<span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span>
|
||||||
|
</div>
|
16
app/assets/javascripts/discourse/templates/tag-groups.hbs
Normal file
16
app/assets/javascripts/discourse/templates/tag-groups.hbs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="container tag-groups-container">
|
||||||
|
<h2>{{i18n "tagging.groups.title"}}</h2>
|
||||||
|
|
||||||
|
<div class='content-list'>
|
||||||
|
<ul>
|
||||||
|
{{#each model as |tagGroup|}}
|
||||||
|
<li><a {{action "selectTagGroup" tagGroup}} class="{{if tagGroup.selected 'active'}}">{{tagGroup.name}}</a></li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<button {{action "newTagGroup"}} class='btn'><i class="fa fa-plus"></i>{{i18n 'tagging.groups.new'}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{outlet}}
|
||||||
|
|
||||||
|
<div class="clearfix" />
|
||||||
|
</div>
|
|
@ -2,14 +2,20 @@
|
||||||
{{discourse-banner user=currentUser banner=site.banner}}
|
{{discourse-banner user=currentUser banner=site.banner}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{i18n "tagging.tags"}}</h2>
|
<div class="list-controls">
|
||||||
|
<div class="container">
|
||||||
|
{{tags-admin-dropdown}}
|
||||||
|
<h2>{{i18n "tagging.tags"}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='tag-sort-options'>
|
<div class='tag-sort-options'>
|
||||||
{{i18n "tagging.sort_by"}}
|
{{i18n "tagging.sort_by"}}
|
||||||
<a {{action "sortByCount"}}>{{i18n "tagging.sort_by_count"}}</a>
|
<a {{action "sortByCount"}}>{{i18n "tagging.sort_by_count"}}</a>
|
||||||
<a {{action "sortById"}}>{{i18n "tagging.sort_by_name"}}</a>
|
<a {{action "sortById"}}>{{i18n "tagging.sort_by_name"}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix" />
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
{{#each model.extras.categories as |category|}}
|
{{#each model.extras.categories as |category|}}
|
||||||
{{tag-list tags=category.tags sortProperties=sortProperties categoryId=category.id}}
|
{{tag-list tags=category.tags sortProperties=sortProperties categoryId=category.id}}
|
||||||
|
|
|
@ -1200,44 +1200,6 @@ table.api-keys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-list {
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: dark-light-diff($primary, $secondary, 50%, -20%);
|
|
||||||
font-size: 1.071em;
|
|
||||||
padding-left: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
li:first-of-type {
|
|
||||||
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
li a {
|
|
||||||
display: block;
|
|
||||||
padding: 10px;
|
|
||||||
color: $primary;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: bold;
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-editor {
|
.content-editor {
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
|
|
||||||
|
|
|
@ -216,6 +216,44 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-list {
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: dark-light-diff($primary, $secondary, 50%, -20%);
|
||||||
|
font-size: 1.071em;
|
||||||
|
padding-left: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li:first-of-type {
|
||||||
|
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
color: $primary;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// don't wrap relative dates, we want
|
// don't wrap relative dates, we want
|
||||||
//
|
//
|
||||||
// Jul 26, '15
|
// Jul 26, '15
|
||||||
|
|
|
@ -201,3 +201,35 @@ header .discourse-tag {color: $tag-color !important; }
|
||||||
color: $tag-color;
|
color: $tag-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-admin-menu {
|
||||||
|
margin-top: 20px;
|
||||||
|
ul {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-groups-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
.content-list {
|
||||||
|
width: 20%;
|
||||||
|
float: left;
|
||||||
|
margin: 20px 0;
|
||||||
|
ul {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag-group-content {
|
||||||
|
width: 75%;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.group-tags-list .tag-chooser {
|
||||||
|
height: 150px !important;
|
||||||
|
.select2-choices {
|
||||||
|
height: 150px !important; // to fight with select2.scss's important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.saving {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -272,11 +272,13 @@ button.dismiss-read {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-notification-menu .dropdown-menu {
|
.category-notification-menu, .tags-admin-menu {
|
||||||
right: 0;
|
.dropdown-menu {
|
||||||
top: 30px;
|
right: 0;
|
||||||
bottom: auto;
|
top: 30px;
|
||||||
left: auto;
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-heading {
|
.category-heading {
|
||||||
|
|
|
@ -379,7 +379,7 @@ ol.category-breadcrumb {
|
||||||
.btn-default.pull-right { margin-right: 10px; }
|
.btn-default.pull-right { margin-right: 10px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-notification-menu {
|
.category-notification-menu, .tags-admin-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
62
app/controllers/tag_groups_controller.rb
Normal file
62
app/controllers/tag_groups_controller.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
class TagGroupsController < ApplicationController
|
||||||
|
skip_before_filter :check_xhr, only: [:index, :show]
|
||||||
|
before_filter :ensure_logged_in, except: [:index, :show]
|
||||||
|
before_filter :fetch_tag_group, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
tag_groups = TagGroup.order('name ASC').preload(:tags).all
|
||||||
|
serializer = ActiveModel::ArraySerializer.new(tag_groups, each_serializer: TagGroupSerializer, root: 'tag_groups')
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
store_preloaded "tagGroups", MultiJson.dump(serializer)
|
||||||
|
render "default/empty"
|
||||||
|
end
|
||||||
|
format.json { render_json_dump(serializer) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
serializer = TagGroupSerializer.new(@tag_group)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
store_preloaded "tagGroup", MultiJson.dump(serializer)
|
||||||
|
render "default/empty"
|
||||||
|
end
|
||||||
|
format.json { render_json_dump(serializer) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
guardian.ensure_can_admin_tag_groups!
|
||||||
|
@tag_group = TagGroup.new(tag_groups_params)
|
||||||
|
if @tag_group.save
|
||||||
|
render_serialized(@tag_group, TagGroupSerializer)
|
||||||
|
else
|
||||||
|
return render_json_error(@tag_group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
guardian.ensure_can_admin_tag_groups!
|
||||||
|
json_result(@tag_group, serializer: TagGroupSerializer) do |tag_group|
|
||||||
|
@tag_group.update(tag_groups_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
guardian.ensure_can_admin_tag_groups!
|
||||||
|
@tag_group.destroy
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_tag_group
|
||||||
|
@tag_group = TagGroup.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_groups_params
|
||||||
|
params[:tag_names] ||= []
|
||||||
|
params.permit(:id, :name, :tag_names => [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -316,16 +316,7 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_tags=(tag_names_arg)
|
def allowed_tags=(tag_names_arg)
|
||||||
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user)) || []
|
DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg)
|
||||||
if self.tags.pluck(:name).sort != tag_names.sort
|
|
||||||
self.tags = Tag.where(name: tag_names).all
|
|
||||||
if self.tags.size < tag_names.size
|
|
||||||
new_tag_names = tag_names - self.tags.map(&:name)
|
|
||||||
new_tag_names.each do |name|
|
|
||||||
self.tags << Tag.create(name: name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def downcase_email
|
def downcase_email
|
||||||
|
|
|
@ -9,6 +9,9 @@ class Tag < ActiveRecord::Base
|
||||||
has_many :category_tags, dependent: :destroy
|
has_many :category_tags, dependent: :destroy
|
||||||
has_many :categories, through: :category_tags
|
has_many :categories, through: :category_tags
|
||||||
|
|
||||||
|
has_many :tag_group_memberships
|
||||||
|
has_many :tag_groups, through: :tag_group_memberships
|
||||||
|
|
||||||
def self.tags_by_count_query(opts={})
|
def self.tags_by_count_query(opts={})
|
||||||
q = TopicTag.joins(:tag, :topic).group("topic_tags.tag_id, tags.name").order('count_all DESC')
|
q = TopicTag.joins(:tag, :topic).group("topic_tags.tag_id, tags.name").order('count_all DESC')
|
||||||
q = q.limit(opts[:limit]) if opts[:limit]
|
q = q.limit(opts[:limit]) if opts[:limit]
|
||||||
|
|
8
app/models/tag_group.rb
Normal file
8
app/models/tag_group.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class TagGroup < ActiveRecord::Base
|
||||||
|
has_many :tag_group_memberships, dependent: :destroy
|
||||||
|
has_many :tags, through: :tag_group_memberships
|
||||||
|
|
||||||
|
def tag_names=(tag_names_arg)
|
||||||
|
DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg)
|
||||||
|
end
|
||||||
|
end
|
4
app/models/tag_group_membership.rb
Normal file
4
app/models/tag_group_membership.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class TagGroupMembership < ActiveRecord::Base
|
||||||
|
belongs_to :tag
|
||||||
|
belongs_to :tag_group, counter_cache: "tag_count"
|
||||||
|
end
|
7
app/serializers/tag_group_serializer.rb
Normal file
7
app/serializers/tag_group_serializer.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class TagGroupSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :tag_names
|
||||||
|
|
||||||
|
def tag_names
|
||||||
|
object.tags.pluck(:name).sort
|
||||||
|
end
|
||||||
|
end
|
|
@ -2986,6 +2986,8 @@ en:
|
||||||
sort_by: "Sort by:"
|
sort_by: "Sort by:"
|
||||||
sort_by_count: "count"
|
sort_by_count: "count"
|
||||||
sort_by_name: "name"
|
sort_by_name: "name"
|
||||||
|
manage_groups: "Manage Tag Groups"
|
||||||
|
manage_groups_description: "Define groups to organize tags"
|
||||||
|
|
||||||
filters:
|
filters:
|
||||||
without_category: "%{filter} %{tag} topics"
|
without_category: "%{filter} %{tag} topics"
|
||||||
|
@ -3005,6 +3007,14 @@ en:
|
||||||
title: "Muted"
|
title: "Muted"
|
||||||
description: "You will not be notified of anything about new topics in this tag, and they will not appear on your unread tab."
|
description: "You will not be notified of anything about new topics in this tag, and they will not appear on your unread tab."
|
||||||
|
|
||||||
|
groups:
|
||||||
|
title: "Tag Groups"
|
||||||
|
about: "Add tags to groups to manage them more easily."
|
||||||
|
new: "New Group"
|
||||||
|
tags_label: "Tags in this group:"
|
||||||
|
new_name: "New Tag Group"
|
||||||
|
save: "Save"
|
||||||
|
|
||||||
topics:
|
topics:
|
||||||
none:
|
none:
|
||||||
unread: "You have no unread topics."
|
unread: "You have no unread topics."
|
||||||
|
|
|
@ -636,6 +636,7 @@ Discourse::Application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
resources :tag_groups, except: [:new, :edit]
|
||||||
|
|
||||||
Discourse.filters.each do |filter|
|
Discourse.filters.each do |filter|
|
||||||
root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}"
|
root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}"
|
||||||
|
|
17
db/migrate/20160602164008_create_tag_groups.rb
Normal file
17
db/migrate/20160602164008_create_tag_groups.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class CreateTagGroups < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :tag_groups do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.integer :tag_count, null: false, default: 0
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :tag_group_memberships do |t|
|
||||||
|
t.references :tag, null: false
|
||||||
|
t.references :tag_group, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :tag_group_memberships, [:tag_group_id, :tag_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -120,10 +120,25 @@ module DiscourseTagging
|
||||||
return tag_names[0...SiteSetting.max_tags_per_topic]
|
return tag_names[0...SiteSetting.max_tags_per_topic]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.add_or_create_tags_by_name(taggable, tag_names_arg)
|
||||||
|
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user)) || []
|
||||||
|
if taggable.tags.pluck(:name).sort != tag_names.sort
|
||||||
|
taggable.tags = Tag.where(name: tag_names).all
|
||||||
|
if taggable.tags.size < tag_names.size
|
||||||
|
new_tag_names = tag_names - taggable.tags.map(&:name)
|
||||||
|
new_tag_names.each do |name|
|
||||||
|
taggable.tags << Tag.create(name: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: this is unused?
|
||||||
def self.notification_key(tag_id)
|
def self.notification_key(tag_id)
|
||||||
"tags_notification:#{tag_id}"
|
"tags_notification:#{tag_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: this is unused?
|
||||||
def self.muted_tags(user)
|
def self.muted_tags(user)
|
||||||
return [] unless user
|
return [] unless user
|
||||||
UserCustomField.where(user_id: user.id, value: TopicUser.notification_levels[:muted]).pluck(:name).map { |x| x[0,17] == "tags_notification" ? x[18..-1] : nil}.compact
|
UserCustomField.where(user_id: user.id, value: TopicUser.notification_levels[:muted]).pluck(:name).map { |x| x[0,17] == "tags_notification" ? x[18..-1] : nil}.compact
|
||||||
|
|
|
@ -5,6 +5,7 @@ require_dependency 'guardian/topic_guardian'
|
||||||
require_dependency 'guardian/user_guardian'
|
require_dependency 'guardian/user_guardian'
|
||||||
require_dependency 'guardian/post_revision_guardian'
|
require_dependency 'guardian/post_revision_guardian'
|
||||||
require_dependency 'guardian/group_guardian'
|
require_dependency 'guardian/group_guardian'
|
||||||
|
require_dependency 'guardian/tag_guardian'
|
||||||
|
|
||||||
# The guardian is responsible for confirming access to various site resources and operations
|
# The guardian is responsible for confirming access to various site resources and operations
|
||||||
class Guardian
|
class Guardian
|
||||||
|
@ -15,6 +16,7 @@ class Guardian
|
||||||
include UserGuardian
|
include UserGuardian
|
||||||
include PostRevisionGuardian
|
include PostRevisionGuardian
|
||||||
include GroupGuardian
|
include GroupGuardian
|
||||||
|
include TagGuardian
|
||||||
|
|
||||||
class AnonymousUser
|
class AnonymousUser
|
||||||
def blank?; true; end
|
def blank?; true; end
|
||||||
|
@ -277,17 +279,6 @@ class Guardian
|
||||||
UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0
|
UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_create_tag?
|
|
||||||
user && user.has_trust_level?(SiteSetting.min_trust_to_create_tag.to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_tag_topics?
|
|
||||||
user && user.has_trust_level?(SiteSetting.min_trust_level_to_tag_topics.to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_admin_tags?
|
|
||||||
is_staff?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
18
lib/guardian/tag_guardian.rb
Normal file
18
lib/guardian/tag_guardian.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#mixin for all guardian methods dealing with tagging permisions
|
||||||
|
module TagGuardian
|
||||||
|
def can_create_tag?
|
||||||
|
user && SiteSetting.tagging_enabled && user.has_trust_level?(SiteSetting.min_trust_to_create_tag.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_tag_topics?
|
||||||
|
user && user.has_trust_level?(SiteSetting.min_trust_level_to_tag_topics.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_admin_tags?
|
||||||
|
is_staff? && SiteSetting.tagging_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_admin_tag_groups?
|
||||||
|
is_staff? && SiteSetting.tagging_enabled
|
||||||
|
end
|
||||||
|
end
|
|
@ -2208,4 +2208,62 @@ describe Guardian do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Tags" do
|
||||||
|
context "tagging disabled" do
|
||||||
|
before do
|
||||||
|
SiteSetting.tagging_enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can_create_tag returns false" do
|
||||||
|
expect(Guardian.new(admin).can_create_tag?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can_admin_tags returns false" do
|
||||||
|
expect(Guardian.new(admin).can_admin_tags?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can_admin_tag_groups returns false" do
|
||||||
|
expect(Guardian.new(admin).can_admin_tag_groups?).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "tagging is enabled" do
|
||||||
|
before do
|
||||||
|
SiteSetting.tagging_enabled = true
|
||||||
|
SiteSetting.min_trust_to_create_tag = 3
|
||||||
|
SiteSetting.min_trust_level_to_tag_topics = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "can_create_tag" do
|
||||||
|
it "returns false if trust level is too low" do
|
||||||
|
expect(Guardian.new(trust_level_2).can_create_tag?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if trust level is high enough" do
|
||||||
|
expect(Guardian.new(trust_level_3).can_create_tag?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true for staff" do
|
||||||
|
expect(Guardian.new(admin).can_create_tag?).to be_truthy
|
||||||
|
expect(Guardian.new(moderator).can_create_tag?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "can_tag_topics" do
|
||||||
|
it "returns false if trust level is too low" do
|
||||||
|
expect(Guardian.new(Fabricate(:user, trust_level: 0)).can_tag_topics?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if trust level is high enough" do
|
||||||
|
expect(Guardian.new(Fabricate(:user, trust_level: 1)).can_tag_topics?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true for staff" do
|
||||||
|
expect(Guardian.new(admin).can_tag_topics?).to be_truthy
|
||||||
|
expect(Guardian.new(moderator).can_tag_topics?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue