Initial commit as a clone of discourse-tagging

This commit is contained in:
Kane York 2015-06-25 09:25:15 -07:00
commit 174e6d6ecc
33 changed files with 856 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.bundle/
log/*.log
pkg/
auto_generated
Gemfile.lock
.DS_Store
*.swp

81
.jshintrc Normal file
View File

@ -0,0 +1,81 @@
{
"predef":["Ember",
"jQuery",
"$",
"RSVP",
"Discourse",
"PreloadStore",
"Handlebars",
"I18n",
"bootbox",
"module",
"moduleFor",
"moduleForComponent",
"Pretender",
"sandbox",
"integration",
"controllerFor",
"test",
"ok",
"not",
"expect",
"equal",
"blank",
"present",
"visit",
"andThen",
"click",
"currentPath",
"currentRouteName",
"currentURL",
"fillIn",
"keyEvent",
"triggerEvent",
"count",
"exists",
"asyncTestDiscourse",
"fixture",
"find",
"sinon",
"moment",
"start",
"_",
"alert",
"containsInstance",
"parseHTML",
"deepEqual",
"notEqual",
"require",
"requirejs",
"hasModule",
"Blob",
"File"],
"node" : false,
"browser" : true,
"boss" : true,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"unused": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"quotmark": false,
"lastsemic": true,
"esnext": true
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Civilized Discourse Construction Kit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

17
README.md Normal file
View File

@ -0,0 +1,17 @@
### discourse-data-explorer
This plugin allows admins to run SQL queries against the live Discourse database,
including parameterized queries and formatting for several common column types.
## Installation
Follow our [Install a Plugin](https://meta.discourse.org/t/install-a-plugin/19157)
howto, using `git clone https://github.com/discourse/discourse-data-explorer.git`
as the plugin command.
Once you've installed it, review the settings under admin and then enable
`data_explorer_enabled`.
## License
MIT

View File

@ -0,0 +1,3 @@
{{#if canEditTags}}
{{tag-chooser tags=model.tags tabIndex="4"}}
{{/if}}

View File

@ -0,0 +1,3 @@
{{#if canEditTags}}
{{tag-chooser tags=buffered.tags}}
{{/if}}

View File

@ -0,0 +1,13 @@
{{#if tags_changes}}
<div class='row'>
{{i18n "tagging.changed"}}
{{#each t in previousTagChanges}}
{{discourse-tag tagId=t}}
{{/each}}
&rarr;
&nbsp;
{{#each t in currentTagChanges}}
{{discourse-tag tagId=t}}
{{/each}}
</div>
{{/if}}

View File

@ -0,0 +1,3 @@
<li>
{{#link-to 'tags'}}{{i18n "tagging.tags"}}{{/link-to}}
</li>

View File

@ -0,0 +1,3 @@
{{#each t in topic.tags}}
{{discourse-tag tagId=t}}
{{/each}}

View File

@ -0,0 +1,7 @@
import RESTAdapter from 'discourse/adapters/rest';
export default RESTAdapter.extend({
pathFor(type, id) {
return "/tags/" + id + "/notifications";
}
});

View File

@ -0,0 +1,33 @@
export default Ember.Component.extend({
tagName: 'a',
classNameBindings: [':discourse-tag'],
attributeBindings: ['href', 'style'],
href: function() {
return "/tags/" + this.get('tagId');
}.property('tagId'),
style: function() {
const count = parseFloat(this.get('count')),
minCount = parseFloat(this.get('minCount')),
maxCount = parseFloat(this.get('maxCount'));
if (count && maxCount && minCount) {
let ratio = (count - minCount) / maxCount;
if (ratio) {
ratio = ratio + 1.0;
return "font-size: " + ratio + "em";
}
}
}.property('count', 'scaleTo'),
render(buffer) {
buffer.push(Handlebars.Utils.escapeExpression(this.get('tagId')));
},
click(e) {
e.preventDefault();
Discourse.URL.routeTo(this.get('href'));
return true;
}
});

View File

@ -0,0 +1,88 @@
function formatTag(t) {
const ret = "<a href class='discourse-tag'>" + Handlebars.Utils.escapeExpression(t.id) + "</a>";
return (t.count) ? ret + " <span class='discourse-tag-count'>x" + t.count + "</span>" : ret;
}
export default Ember.TextField.extend({
classNameBindings: [':tag-chooser'],
attributeBindings: ['tabIndex'],
_setupTags: function() {
const tags = this.get('tags') || [];
this.set('value', tags.join(", "));
}.on('init'),
_valueChanged: function() {
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
this.set('tags', tags);
}.observes('value'),
_initializeTags: function() {
const site = this.site,
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
this.$().select2({
tags: true,
placeholder: I18n.t('tagging.choose_for_topic'),
maximumInputLength: this.siteSettings.max_tag_length,
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
initSelection(element, callback) {
const data = [];
function splitVal(string, separator) {
var val, i, l;
if (string === null || string.length < 1) return [];
val = string.split(separator);
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
return val;
}
$(splitVal(element.val(), ",")).each(function () {
data.push({
id: this,
text: this
});
});
callback(data);
},
createSearchChoice: function(term, data) {
term = term.replace(filterRegexp, '').trim();
// No empty terms, make sure the user has permission to create the tag
if (!term.length || !site.get('can_create_tag')) { return; }
if ($(data).filter(function() {
return this.text.localeCompare(term) === 0;
}).length === 0) {
return { id: term, text: term };
}
},
createSearchChoicePosition: function(list, item) {
// Search term goes on the bottom
list.push(item);
},
formatSelectionCssClass: function () { return "discourse-tag"; },
formatResult: formatTag,
// formatSelection: formatTag,
multiple: true,
ajax: {
quietMillis: 200,
cache: true,
url: "/tags/filter/search",
dataType: 'json',
data: function (term) {
return { q: term };
},
results: function (data) {
return data;
}
},
});
}.on('didInsertElement'),
_destroyTags: function() {
this.$().select2('destroy');
}.on('willDestroyElement')
});

View File

@ -0,0 +1,11 @@
import NotificationsButton from 'discourse/components/notifications-button';
export default NotificationsButton.extend({
classNames: ['notification-options', 'tag-notification-menu'],
buttonIncludesText: false,
i18nPrefix: 'tagging.notifications',
clicked(id) {
this.sendAction('action', id);
}
});

View File

@ -0,0 +1,15 @@
export default Ember.Controller.extend({
tag: null,
list: null,
loadMoreTopics() {
return this.get('list').loadMore();
},
actions: {
changeTagNotification(id) {
const tagNotification = this.get('tagNotification');
tagNotification.update({ notification_level: id });
}
}
});

View File

@ -0,0 +1,44 @@
import ComposerController from 'discourse/controllers/composer';
import HistoryController from 'discourse/controllers/history';
import TopicController from 'discourse/controllers/topic';
import { needsSecondRowIf } from 'discourse/components/header-extra-info';
// Work around a quirk of custom fields -- an array of one element
// is returned as just that element. We should fix this properly
// in custom fields and remove this.
function customTagArray(fieldName) {
return function() {
var val = this.get(fieldName);
if (!val) { return val; }
if (!Array.isArray(val)) { val = [val]; }
return val;
}.property(fieldName);
}
export default {
name: 'extend-for-tagging',
initialize() {
Discourse.Composer.serializeOnCreate('tags');
Discourse.Composer.serializeToTopic('tags', 'topic.tags');
TopicController.reopen({
canEditTags: Ember.computed.not('isPrivateMessage')
});
HistoryController.reopen({
previousTagChanges: customTagArray('tags_changes.previous'),
currentTagChanges: customTagArray('tags_changes.current')
});
ComposerController.reopen({
canEditTags: function() {
return !this.site.mobileView &&
this.get('model.canEditTitle') &&
!this.get('model.creatingPrivateMessage');
}.property('model.canEditTitle', 'model.creatingPrivateMessage')
});
// Show a second row in the header if there are any tags on the topic
needsSecondRowIf('topic.tags.length', tagsLength => parseInt(tagsLength) > 0);
}
};

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return Discourse.ajax("/tags/filter/cloud.json");
}
});

View File

@ -0,0 +1,32 @@
export default Discourse.Route.extend({
model(tag) {
tag.tag_id = Handlebars.Utils.escapeExpression(tag.tag_id);
if (this.get('currentUser')) {
// If logged in, we should get the tag's user settings
const self = this;
return this.store.find('tagNotification', tag.tag_id).then(function(tn) {
self.set('tagNotification', tn);
return tag;
});
}
return tag;
},
afterModel(tag) {
const self = this;
return Discourse.TopicList.list('tags/' + tag.tag_id).then(function(list) {
self.set('list', list);
});
},
setupController(controller, model) {
controller.setProperties({
tag: model,
list: this.get('list'),
tagNotification: this.get('tagNotification')
});
}
});

View File

@ -0,0 +1,5 @@
export default function() {
this.resource('tags', function() {
this.route('show', {path: ':tag_id'});
});
}

View File

@ -0,0 +1,9 @@
<div class="container list-container">
<div class="row">
<div class="full-width">
<div id='list-area'>
{{outlet}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<h2>{{i18n "tagging.all_tags"}}</h2>
<div class='tag-cloud'>
{{#each tag in cloud}}
{{discourse-tag tagId=tag.id
count=tag.count
maxCount=model.max_count
minCount=model.min_count}}
{{/each}}
</div>

View File

@ -0,0 +1,9 @@
{{#if tagNotification}}
{{tag-notifications-button tag=tag.tag_id
action="changeTagNotification"
notificationLevel=tagNotification.notification_level}}
{{/if}}
<h2>{{{i18n "tagging.topics_tagged" tag=tag.tag_id}}}</h2>
{{topic-list topics=list.topics}}

View File

@ -0,0 +1,3 @@
import DiscoveryTopicsView from "discourse/views/discovery-topics";
export default DiscoveryTopicsView;

View File

@ -0,0 +1,68 @@
.topic-title-outlet.choose-tags {
margin-left: 25px;
margin-top: 3px;
}
.tag-cloud {
.discourse-tag {
display: inline-block;
color: #333 !important;
}
}
.extra-info-wrapper {
.discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
}
.add-tags .select2 {
margin: 0;
}
.discourse-tag-count {
font-size: 0.8em;
color: #999;
}
.select2-result-label .discourse-tag {
margin-right: 0;
}
.discourse-tag {
padding: 0;
margin: 0 5px 0 0;
font-size: 0.857em;
/* !important is needed in the select2 widget to overwrite default styles */
color: #999 !important;
border: 0 !important;
border-radius: 0 !important;
background-color: transparent !important;
font-weight: bold !important;
line-height: 1.4em !important;
}
.list-tags {
display: inline;
margin-left: 4px;
}
.tag-chooser {
width: 500px;
margin: 5px 0;
}
.tag-notification-menu {
float: right;
margin-bottom: 10px;
}
.tag-notification-menu .dropdown-menu {
right: 0;
top: 30px;
bottom: auto;
left: auto;
}

View File

@ -0,0 +1,39 @@
# encoding: utf-8
# This file contains content for the client portion of Discourse, sent out
# to the Javascript app.
#
# To work with us on translations, see:
# https://www.transifex.com/projects/p/discourse-org/
#
# This is a "source" file, which is used by Transifex to get translations for other languages.
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
#
# tx push -s
#
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
#
# To validate this YAML file after you change it, please paste it into
# http://yamllint.com/
en:
js:
tagging:
all_tags: "All Tags"
changed: "tags changed:"
tags: "Tags"
choose_for_topic: "choose optional tags for this topic"
topics_tagged: "Topics tagged <span class='discourse-tag'>{{tag}}</span>"
notifications:
watching:
title: "Watching"
description: "You will automatically watch all new topics in this tag. You will be notified of all new posts and topics, plus the count of unread and new posts will also appear next to the topic."
tracking:
title: "Tracking"
description: "You will automatically track all new topics in this tag. A count of unread and new posts will appear next to the topic."
regular:
title: "Regular"
description: "You will be notified if someone mentions your @name or replies to your post."
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."

View File

@ -0,0 +1,25 @@
# encoding: utf-8
# This file contains content for the client portion of Discourse, sent out
# to the Javascript app.
#
# To work with us on translations, see:
# https://www.transifex.com/projects/p/discourse-org/
#
# This is a "source" file, which is used by Transifex to get translations for other languages.
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
#
# tx push -s
#
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
#
# To validate this YAML file after you change it, please paste it into
# http://yamllint.com/
pl_PL:
js:
tagging:
all_tags: "Wszystkie tagi"
changed: "zmienione tagi:"
tags: "Tagi"
choose_for_topic: "wybierz opcjonalne tagi dla tego tematu"
topics_tagged: "Tematy otagowane jako <span class='discourse-tag'>{{tag}}</span>"

View File

@ -0,0 +1,24 @@
# encoding: utf-8
# This file contains content for the client portion of Discourse, sent out
# to the Javascript app.
#
# To work with us on translations, see:
# https://www.transifex.com/projects/p/discourse-org/
#
# This is a "source" file, which is used by Transifex to get translations for other languages.
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
#
# tx push -s
#
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
#
# To validate this YAML file after you change it, please paste it into
# http://yamllint.com/
ru:
js:
tagging:
all_tags: "Все теги"
tags: "Теги"
choose_for_topic: "выберите теги для темы"
topics_tagged: "Темы с тегом <span class='discourse-tag'>{{tag}}</span>"

View File

@ -0,0 +1,25 @@
# encoding: utf-8
# This file contains content for the client portion of Discourse, sent out
# to the Javascript app.
#
# To work with us on translations, see:
# https://www.transifex.com/projects/p/discourse-org/
#
# This is a "source" file, which is used by Transifex to get translations for other languages.
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
#
# tx push -s
#
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
#
# To validate this YAML file after you change it, please paste it into
# http://yamllint.com/
zh_CN:
js:
tagging:
all_tags: "全部标签"
changed: "标签更改:"
tags: "标签"
choose_for_topic: "选择主题的标签"
topics_tagged: "主题标签: <span class='discourse-tag'>{{tag}}</span>"

View File

@ -0,0 +1,9 @@
en:
site_settings:
tagging_enabled: "Allow users to tag topics?"
min_trust_to_create_tag: "The minimum trust level required to create a tag."
max_tags_per_topic: "The maximum tags that can be applied to a topic."
max_tag_length: "The maximum amount of characters that can be used in a tag."
rss_by_tag: "Topics tagged %{tag}"
rss_description:
tag: "Tagged topics"

View File

@ -0,0 +1,9 @@
pl_PL:
site_settings:
tagging_enabled: "Pozwolić użytkownikom na tagowanie tematów?"
min_trust_to_create_tag: "Minimalny poziom zaufania dla tworzenia nowych tagów."
max_tags_per_topic: "Maksymalna ilość tagów przypisanych do tematu."
max_tag_length: "Maksymalna ilość znaków per tag."
rss_by_tag: "Tematy otagowane jako %{tag}"
rss_description:
tag: "Otagowane tematy"

View File

@ -0,0 +1,4 @@
ru:
site_settings:
min_trust_to_create_tag: "Минимальный уровень доверия для создания тегов."
max_tags_per_topic: "Максимальное количество тегов для темы."

View File

@ -0,0 +1,6 @@
zh_CN:
site_settings:
tagging_enabled: "允许用户为主题设置标签?"
min_trust_to_create_tag: "允许创建标签的最小信任等级"
max_tags_per_topic: "一个主题最多允许被创建多少个标签"
max_tag_length: "一个标签允许的最大字符数"

3
config/settings.yml Normal file
View File

@ -0,0 +1,3 @@
plugins:
data_explorer_enabled:
default: false

222
plugin.rb Normal file
View File

@ -0,0 +1,222 @@
# name: discourse-data-explorer
# about: Interface for running analysis SQL queries on the live database
# version: 0.1
# authors: Riking
# url: https://github.com/discourse/discourse-data-explorer
enabled_site_setting :data_explorer_enabled
register_asset 'stylesheets/tagging.scss'
after_initialize do
TAGS_FIELD_NAME = "tags"
TAGS_FILTER_REGEXP = /[<\\\/\>\.\#\?\&\s]/
module ::DataExplorer
class Engine < ::Rails::Engine
engine_name "data_explorer"
isolate_namespace DataExplorer
end
def self.clean_tag(tag)
tag.downcase.strip[0...SiteSetting.max_tag_length].gsub(TAGS_FILTER_REGEXP, '')
end
def self.tags_for_saving(tags, guardian)
return unless tags
tags.map! {|t| clean_tag(t) }
tags.delete_if {|t| t.blank? }
tags.uniq!
# If the user can't create tags, remove any tags that don't already exist
unless guardian.can_create_tag?
tag_count = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tags).group(:value).count
tags.delete_if {|t| !tag_count.has_key?(t) }
end
return tags[0...SiteSetting.max_tags_per_topic]
end
def self.notification_key(tag_id)
"tags_notification:#{tag_id}"
end
def self.auto_notify_for(tags, topic)
key_names = tags.map {|t| notification_key(t) }
key_names_sql = ActiveRecord::Base.sql_fragment("(#{tags.map { "'%s'" }.join(', ')})", *key_names)
sql = <<-SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT ucf.user_id,
#{topic.id.to_i},
CAST(ucf.value AS INTEGER),
#{TopicUser.notification_reasons[:plugin_changed]}
FROM user_custom_fields AS ucf
WHERE ucf.name IN #{key_names_sql}
AND NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = #{topic.id.to_i} AND user_id = ucf.user_id)
AND CAST(ucf.value AS INTEGER) <> #{TopicUser.notification_levels[:regular]}
SQL
ActiveRecord::Base.exec_sql(sql)
end
end
require_dependency 'application_controller'
require_dependency 'topic_list_responder'
class DataExplorer::TagsController < ::ApplicationController
include ::TopicListResponder
requires_plugin 'discourse-tagging'
skip_before_filter :check_xhr, only: [:tag_feed, :show]
before_filter :ensure_logged_in, only: [:notifications, :update_notifications]
def cloud
cloud = self.class.tags_by_count(guardian, limit: 300).count
result, max_count, min_count = [], 0, nil
cloud.each do |t, c|
result << { id: t, count: c }
max_count = c if c > max_count
min_count = c if min_count.nil? || c < min_count
end
result.sort_by! {|r| r[:id]}
render json: { cloud: result, max_count: max_count, min_count: min_count }
end
def show
tag_id = ::DiscourseTagging.clean_tag(params[:tag_id])
topics_tagged = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tag_id).pluck(:topic_id)
page = params[:page].to_i
query = TopicQuery.new(current_user, page: page)
latest_results = query.latest_results.where(id: topics_tagged)
@list = query.create_list(:by_tag, {}, latest_results)
@list.more_topics_url = list_by_tag_path(tag_id: tag_id, page: page + 1)
@rss = "tag"
respond_with_list(@list)
end
def tag_feed
discourse_expires_in 1.minute
tag_id = ::DiscourseTagging.clean_tag(params[:tag_id])
@link = "#{Discourse.base_url}/tags/#{tag_id}"
@description = I18n.t("rss_by_tag", tag: tag_id)
@title = "#{SiteSetting.title} - #{@description}"
@atom_link = "#{Discourse.base_url}/tags/#{tag_id}.rss"
query = TopicQuery.new(current_user)
topics_tagged = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tag_id).pluck(:topic_id)
latest_results = query.latest_results.where(id: topics_tagged)
@topic_list = query.create_list(:by_tag, {}, latest_results)
render 'list/list', formats: [:rss]
end
def search
tags = self.class.tags_by_count(guardian)
term = params[:q]
if term.present?
term.gsub!(/[^a-z0-9]*/, '')
tags = tags.where('value like ?', "%#{term}%")
end
tags = tags.count(:value).map {|t, c| { id: t, text: t, count: c } }
render json: { results: tags }
end
def notifications
level = current_user.custom_fields[::DiscourseTagging.notification_key(params[:tag_id])] || 1
render json: { tag_notifications: { id: params[:tag_id], notification_level: level.to_i } }
end
def update_notifications
level = params[:tag_notifications][:notification_level].to_i
current_user.custom_fields[::DiscourseTagging.notification_key(params[:tag_id])] = level
current_user.save_custom_fields
render json: success_json
end
private
def self.tags_by_count(guardian, opts=nil)
opts = opts || {}
result = TopicCustomField.where(name: TAGS_FIELD_NAME)
.joins(:topic)
.group(:value)
.limit(opts[:limit] || 5)
.order('COUNT(topic_custom_fields.value) DESC')
guardian.filter_allowed_categories(result)
end
end
DataExplorer::Engine.routes.draw do
get '/' => 'tags#cloud'
get '/filter/cloud' => 'tags#cloud'
get '/filter/search' => 'tags#search'
get '/:tag_id.rss' => 'tags#tag_feed'
get '/:tag_id' => 'tags#show', as: 'list_by_tag'
get '/:tag_id/notifications' => 'tags#notifications'
put '/:tag_id/notifications' => 'tags#update_notifications'
end
Discourse::Application.routes.append do
mount ::DataExplorer::Engine, at: "/tags"
end
# Add a `tags` reader to the Topic model for easy reading of tags
add_to_class(:topic, :tags) do
result = custom_fields[TAGS_FIELD_NAME]
return [result].flatten if result
end
# Save the tags when the topic is saved
PostRevisor.track_topic_field(:tags_empty_array) do |tc, val|
if val.present?
tc.record_change(TAGS_FIELD_NAME, tc.topic.custom_fields[TAGS_FIELD_NAME], nil)
tc.topic.custom_fields.delete(TAGS_FIELD_NAME)
end
end
PostRevisor.track_topic_field(:tags) do |tc, tags|
if tags.present?
tags = ::DataExplorer.tags_for_saving(tags, tc.guardian)
new_tags = tags - (tc.topic.tags || [])
tc.record_change(TAGS_FIELD_NAME, tc.topic.custom_fields[TAGS_FIELD_NAME], tags)
tc.topic.custom_fields.update(TAGS_FIELD_NAME => tags)
::DataExplorer.auto_notify_for(new_tags, tc.topic) if new_tags.present?
end
end
on(:topic_created) do |topic, params, user|
tags = ::DataExplorer.tags_for_saving(params[:tags], Guardian.new(user))
if tags.present?
topic.custom_fields.update(TAGS_FIELD_NAME => tags)
topic.save
::DataExplorer.auto_notify_for(tags, topic)
end
end
add_to_class(:guardian, :can_create_tag?) do
user && user.has_trust_level?(SiteSetting.min_trust_to_create_tag.to_i)
end
# Return tag related stuff in JSON output
TopicViewSerializer.attributes_from_topic(:tags)
add_to_serializer(:site, :can_create_tag) { scope.can_create_tag? }
add_to_serializer(:site, :tags_filter_regexp) { TAGS_FILTER_REGEXP.source }
end