mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
FEATURE: Merge tagging plugin into core
This commit is contained in:
parent
7151c16c79
commit
e5918c7d00
93 changed files with 2484 additions and 20 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,8 +4,6 @@
|
|||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile ~/.gitignore_global
|
||||
|
||||
tags
|
||||
|
||||
.DS_Store
|
||||
._.DS_Store
|
||||
dump.rdb
|
||||
|
|
|
@ -11,6 +11,7 @@ var _pluginCallbacks = [];
|
|||
window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
rootElement: '#main',
|
||||
_docTitle: document.title,
|
||||
__TAGS_INCLUDED__: true,
|
||||
|
||||
getURL: function(url) {
|
||||
if (!url) return url;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import RESTAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
pathFor(store, type, id) {
|
||||
return "/tags/" + id + "/notifications";
|
||||
}
|
||||
});
|
|
@ -2,6 +2,7 @@ import userSearch from 'discourse/lib/user-search';
|
|||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
|
||||
import { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
|
@ -27,6 +28,22 @@ export default Ember.Component.extend({
|
|||
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
||||
},
|
||||
|
||||
_renderUnseenTagHashtags($preview, unseen) {
|
||||
fetchUnseenTagHashtags(unseen).then(() => {
|
||||
linkSeenTagHashtags($preview);
|
||||
});
|
||||
},
|
||||
|
||||
@on('previewRefreshed')
|
||||
paintTagHashtags($preview) {
|
||||
if (!this.siteSettings.tagging_enabled) { return; }
|
||||
|
||||
const unseenTagHashtags = linkSeenTagHashtags($preview);
|
||||
if (unseenTagHashtags.length) {
|
||||
Ember.run.debounce(this, this._renderUnseenTagHashtags, $preview, unseenTagHashtags, 500);
|
||||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
markdownOptions() {
|
||||
return {
|
||||
|
|
|
@ -3,9 +3,10 @@ import loadScript from 'discourse/lib/load-script';
|
|||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { SEPARATOR as categoryHashtagSeparator,
|
||||
categoryHashtagTriggerRule
|
||||
} from 'discourse/lib/category-hashtags';
|
||||
import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
|
||||
import { SEPARATOR } from 'discourse/lib/category-hashtags';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
|
@ -278,17 +279,22 @@ export default Ember.Component.extend({
|
|||
Ember.run.debounce(this, this._updatePreview, 30);
|
||||
},
|
||||
|
||||
_applyCategoryHashtagAutocomplete(container, $editorInput) {
|
||||
const template = container.lookup('template:category-group-autocomplete.raw');
|
||||
_applyCategoryHashtagAutocomplete(container) {
|
||||
const template = container.lookup('template:category-tag-autocomplete.raw');
|
||||
const siteSettings = this.siteSettings;
|
||||
|
||||
$editorInput.autocomplete({
|
||||
this.$('.d-editor-input').autocomplete({
|
||||
template: template,
|
||||
key: '#',
|
||||
transformComplete(category) {
|
||||
return Category.slugFor(category, categoryHashtagSeparator);
|
||||
transformComplete(obj) {
|
||||
if (obj.model) {
|
||||
return Category.slugFor(obj.model, SEPARATOR);
|
||||
} else {
|
||||
return `${obj.text}${TAG_HASHTAG_POSTFIX}`;
|
||||
}
|
||||
},
|
||||
dataSource(term) {
|
||||
return Category.search(term);
|
||||
return searchCategoryTag(term, siteSettings);
|
||||
},
|
||||
triggerRule(textarea, opts) {
|
||||
return categoryHashtagTriggerRule(textarea, opts);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export default Ember.Component.extend({
|
||||
tagName: 'a',
|
||||
classNameBindings: [':discourse-tag', 'style', 'tagClass'],
|
||||
attributeBindings: ['href'],
|
||||
|
||||
tagClass: function() {
|
||||
return "tag-" + this.get('tagRecord.id');
|
||||
}.property('tagRecord.id'),
|
||||
|
||||
href: function() {
|
||||
return '/tags/' + this.get('tagRecord.id');
|
||||
}.property('tagRecord.id'),
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
function formatTag(t) {
|
||||
return renderTag(t.id, {count: t.count});
|
||||
}
|
||||
|
||||
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,
|
||||
self = this,
|
||||
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);
|
||||
},
|
||||
formatSelection: function (data) {
|
||||
return data ? renderTag(this.text(data)) : undefined;
|
||||
},
|
||||
formatSelectionCssClass: function(){
|
||||
return "discourse-tag-select2";
|
||||
},
|
||||
formatResult: formatTag,
|
||||
multiple: true,
|
||||
ajax: {
|
||||
quietMillis: 200,
|
||||
cache: true,
|
||||
url: Discourse.getURL("/tags/filter/search"),
|
||||
dataType: 'json',
|
||||
data: function (term) {
|
||||
return { q: term, limit: self.siteSettings.max_tag_search_results };
|
||||
},
|
||||
results: function (data) {
|
||||
if (self.siteSettings.tags_sort_alphabetically) {
|
||||
data.results = data.results.sort(function(a,b) { return a.id > b.id; });
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_destroyTags: function() {
|
||||
this.$().select2('destroy');
|
||||
}.on('willDestroyElement')
|
||||
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'a',
|
||||
classNameBindings: [':tag-badge-wrapper', ':badge-wrapper', ':bullet', 'tagClass'],
|
||||
attributeBindings: ['href'],
|
||||
|
||||
href: function() {
|
||||
var url = '/tags';
|
||||
if (this.get('category')) {
|
||||
url += this.get('category.url');
|
||||
}
|
||||
return url + '/' + this.get('tagId');
|
||||
}.property('tagId', 'category'),
|
||||
|
||||
tagClass: function() {
|
||||
return "tag-" + this.get('tagId');
|
||||
}.property('tagId'),
|
||||
|
||||
render(buffer) {
|
||||
buffer.push(Handlebars.Utils.escapeExpression(this.get('tagId')));
|
||||
},
|
||||
|
||||
click(e) {
|
||||
e.preventDefault();
|
||||
DiscourseURL.routeTo(this.get('href'));
|
||||
return true;
|
||||
}
|
||||
});
|
113
app/assets/javascripts/discourse/components/tag-drop.js.es6
Normal file
113
app/assets/javascripts/discourse/components/tag-drop.js.es6
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { setting } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':tag-drop', 'tag::no-category', 'tags:has-drop','categoryStyle','tagClass'],
|
||||
categoryStyle: setting('category_style'), // match the category-drop style
|
||||
currentCategory: Em.computed.or('secondCategory', 'firstCategory'),
|
||||
showFilterByTag: setting('show_filter_by_tag'),
|
||||
showTagDropdown: Em.computed.and('showFilterByTag', 'tags'),
|
||||
tagId: null,
|
||||
|
||||
tagName: 'li',
|
||||
|
||||
tags: function() {
|
||||
if (this.siteSettings.tags_sort_alphabetically && Discourse.Site.currentProp('top_tags')) {
|
||||
return Discourse.Site.currentProp('top_tags').sort();
|
||||
} else {
|
||||
return Discourse.Site.currentProp('top_tags');
|
||||
}
|
||||
}.property('site.top_tags'),
|
||||
|
||||
iconClass: function() {
|
||||
if (this.get('expanded')) { return "fa fa-caret-down"; }
|
||||
return "fa fa-caret-right";
|
||||
}.property('expanded'),
|
||||
|
||||
tagClass: function() {
|
||||
if (this.get('tagId')) {
|
||||
return "tag-" + this.get('tagId');
|
||||
} else {
|
||||
return "tag_all";
|
||||
}
|
||||
}.property('tagId'),
|
||||
|
||||
allTagsUrl: function() {
|
||||
if (this.get('currentCategory')) {
|
||||
return this.get('currentCategory.url') + "?allTags=1";
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
}.property('firstCategory', 'secondCategory'),
|
||||
|
||||
allTagsLabel: function() {
|
||||
return I18n.t("tagging.selector_all_tags");
|
||||
}.property('tag'),
|
||||
|
||||
dropdownButtonClass: function() {
|
||||
var result = 'badge-category category-dropdown-button';
|
||||
if (Em.isNone(this.get('tag'))) {
|
||||
result += ' home';
|
||||
}
|
||||
return result;
|
||||
}.property('tag'),
|
||||
|
||||
clickEventName: function() {
|
||||
return "click.tag-drop-" + (this.get('tag') || "all");
|
||||
}.property('tag'),
|
||||
|
||||
actions: {
|
||||
expand: function() {
|
||||
var self = this;
|
||||
|
||||
if(!this.get('renderTags')){
|
||||
this.set('renderTags',true);
|
||||
Em.run.next(function(){
|
||||
self.send('expand');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('expanded')) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('tags')) {
|
||||
this.set('expanded', true);
|
||||
}
|
||||
var $dropdown = this.$()[0];
|
||||
|
||||
this.$('a[data-drop-close]').on('click.tag-drop', function() {
|
||||
self.close();
|
||||
});
|
||||
|
||||
Em.run.next(function(){
|
||||
self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) {
|
||||
var $target = $(e.target),
|
||||
closest = $target.closest($dropdown);
|
||||
|
||||
if ($(e.currentTarget).hasClass('badge-wrapper')){
|
||||
self.close();
|
||||
}
|
||||
|
||||
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeEvents: function(){
|
||||
$('html').off(this.get('clickEventName'));
|
||||
this.$('a[data-drop-close]').off('click.tag-drop');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.removeEvents();
|
||||
this.set('expanded', false);
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
this.removeEvents();
|
||||
}
|
||||
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -81,6 +81,14 @@ export default Ember.Controller.extend({
|
|||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||
canEditTags(canEditTitle, creatingPrivateMessage) {
|
||||
return !this.site.mobileView &&
|
||||
this.site.get('can_tag_topics') &&
|
||||
canEditTitle &&
|
||||
!creatingPrivateMessage;
|
||||
},
|
||||
|
||||
@computed('model.action')
|
||||
canWhisper(action) {
|
||||
const currentUser = this.currentUser;
|
||||
|
|
|
@ -3,6 +3,15 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { propertyGreaterThan, propertyLessThan } from 'discourse/lib/computed';
|
||||
|
||||
function customTagArray(fieldName) {
|
||||
return function() {
|
||||
var val = this.get(fieldName);
|
||||
if (!val) { return val; }
|
||||
if (!Array.isArray(val)) { val = [val]; }
|
||||
return val;
|
||||
}.property(fieldName);
|
||||
}
|
||||
|
||||
// This controller handles displaying of history
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: true,
|
||||
|
@ -13,6 +22,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (this.site.mobileView) { this.set("viewMode", "inline"); }
|
||||
}.on("init"),
|
||||
|
||||
previousTagChanges: customTagArray('model.tags_changes.previous'),
|
||||
currentTagChanges: customTagArray('model.tags_changes.current'),
|
||||
|
||||
refresh(postId, postVersion) {
|
||||
this.set("loading", true);
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
||||
|
||||
renameDisabled: function() {
|
||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"),
|
||||
newId = this.get('buffered.id').replace(filterRegexp, '').trim();
|
||||
|
||||
return (newId.length === 0) || (newId === this.get('model.id'));
|
||||
}.property('buffered.id', 'id'),
|
||||
|
||||
actions: {
|
||||
performRename() {
|
||||
const tag = this.get('model'),
|
||||
self = this;
|
||||
tag.update({ id: this.get('buffered.id') }).then(function() {
|
||||
self.send('closeModal');
|
||||
self.transitionToRoute('tags.show', tag.get('id'));
|
||||
}).catch(function() {
|
||||
self.flash(I18n.t('generic_error'), 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
export default Ember.Controller.extend({
|
||||
sortProperties: ['count:desc', 'id'],
|
||||
|
||||
sortedTags: Ember.computed.sort('model', 'sortProperties'),
|
||||
|
||||
actions: {
|
||||
sortByCount() {
|
||||
this.set('sortProperties', ['count:desc', 'id']);
|
||||
},
|
||||
|
||||
sortById() {
|
||||
this.set('sortProperties', ['id']);
|
||||
}
|
||||
}
|
||||
});
|
133
app/assets/javascripts/discourse/controllers/tags-show.js.es6
Normal file
133
app/assets/javascripts/discourse/controllers/tags-show.js.es6
Normal file
|
@ -0,0 +1,133 @@
|
|||
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
|
||||
import { default as NavItem, extraNavItemProperties, customNavItemHref } from 'discourse/models/nav-item';
|
||||
|
||||
if (extraNavItemProperties) {
|
||||
extraNavItemProperties(function(text, opts) {
|
||||
if (opts && opts.tagId) {
|
||||
return {tagId: opts.tagId};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (customNavItemHref) {
|
||||
customNavItemHref(function(navItem) {
|
||||
if (navItem.get('tagId')) {
|
||||
var name = navItem.get('name');
|
||||
|
||||
if ( !Discourse.Site.currentProp('filters').contains(name) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = "/tags/",
|
||||
category = navItem.get("category");
|
||||
|
||||
if(category){
|
||||
path += "c/";
|
||||
path += Discourse.Category.slugFor(category);
|
||||
if (navItem.get('noSubcategories')) { path += '/none'; }
|
||||
path += "/";
|
||||
}
|
||||
|
||||
path += navItem.get('tagId') + "/l/";
|
||||
return path + name.replace(' ', '-');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default Ember.Controller.extend(BulkTopicSelection, {
|
||||
needs: ["application"],
|
||||
|
||||
tag: null,
|
||||
list: null,
|
||||
canAdminTag: Ember.computed.alias("currentUser.staff"),
|
||||
filterMode: null,
|
||||
navMode: 'latest',
|
||||
loading: false,
|
||||
canCreateTopic: false,
|
||||
order: 'default',
|
||||
ascending: false,
|
||||
status: null,
|
||||
state: null,
|
||||
search: null,
|
||||
max_posts: null,
|
||||
q: null,
|
||||
|
||||
queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'],
|
||||
|
||||
navItems: function() {
|
||||
return NavItem.buildList(this.get('category'), {tagId: this.get('tag.id'), filterMode: this.get('filterMode')});
|
||||
}.property('category', 'tag.id', 'filterMode'),
|
||||
|
||||
showTagFilter: function() {
|
||||
return Discourse.SiteSettings.show_filter_by_tag;
|
||||
}.property('category'),
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
showAdminControls: function() {
|
||||
return this.get('canAdminTag') && !this.get('category');
|
||||
}.property('canAdminTag', 'category'),
|
||||
|
||||
loadMoreTopics() {
|
||||
return this.get("list").loadMore();
|
||||
},
|
||||
|
||||
_showFooter: function() {
|
||||
this.set("controllers.application.showFooter", !this.get("list.canLoadMore"));
|
||||
}.observes("list.canLoadMore"),
|
||||
|
||||
footerMessage: function() {
|
||||
if (this.get('loading') || this.get('list.topics.length') !== 0) { return; }
|
||||
|
||||
if (this.get('list.topics.length') === 0) {
|
||||
return I18n.t('tagging.topics.none.' + this.get('navMode'), {tag: this.get('tag.id')});
|
||||
} else {
|
||||
return I18n.t('tagging.topics.bottom.' + this.get('navMode'), {tag: this.get('tag.id')});
|
||||
}
|
||||
}.property('navMode', 'list.topics.length', 'loading'),
|
||||
|
||||
actions: {
|
||||
changeSort(sortBy) {
|
||||
if (sortBy === this.get('order')) {
|
||||
this.toggleProperty('ascending');
|
||||
} else {
|
||||
this.setProperties({ order: sortBy, ascending: false });
|
||||
}
|
||||
this.send('invalidateModel');
|
||||
},
|
||||
|
||||
refresh() {
|
||||
const self = this;
|
||||
// TODO: this probably doesn't work anymore
|
||||
return this.store.findFiltered('topicList', {filter: 'tags/' + this.get('tag.id')}).then(function(list) {
|
||||
self.set("list", list);
|
||||
self.resetSelected();
|
||||
});
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
const self = this;
|
||||
bootbox.confirm(I18n.t("tagging.delete_confirm"), function(result) {
|
||||
if (!result) { return; }
|
||||
|
||||
self.get("tag").destroyRecord().then(function() {
|
||||
self.transitionToRoute("tags.index");
|
||||
}).catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
changeTagNotification(id) {
|
||||
const tagNotification = this.get("tagNotification");
|
||||
tagNotification.update({ notification_level: id });
|
||||
}
|
||||
}
|
||||
});
|
|
@ -14,11 +14,15 @@ addBulkButton('archiveTopics', 'archive_topics');
|
|||
addBulkButton('showNotificationLevel', 'notification_level');
|
||||
addBulkButton('resetRead', 'reset_read');
|
||||
addBulkButton('unlistTopics', 'unlist_topics');
|
||||
addBulkButton('showTagTopics', 'change_tags');
|
||||
|
||||
// Modal for performing bulk actions on topics
|
||||
export default Ember.ArrayController.extend(ModalFunctionality, {
|
||||
tags: null,
|
||||
buttonRows: null,
|
||||
|
||||
emptyTags: Ember.computed.empty('tags'),
|
||||
|
||||
onShow() {
|
||||
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal small');
|
||||
|
||||
|
@ -73,6 +77,15 @@ export default Ember.ArrayController.extend(ModalFunctionality, {
|
|||
},
|
||||
|
||||
actions: {
|
||||
showTagTopics() {
|
||||
this.set('tags', '');
|
||||
this.send('changeBulkTemplate', 'bulk-tag');
|
||||
},
|
||||
|
||||
changeTags() {
|
||||
this.performAndRefresh({type: 'change_tags', tags: this.get('tags')});
|
||||
},
|
||||
|
||||
showChangeCategory() {
|
||||
this.send('changeBulkTemplate', 'modal/bulk_change_category');
|
||||
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal full');
|
||||
|
|
|
@ -108,6 +108,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
return post => this.postSelected(post);
|
||||
}.property(),
|
||||
|
||||
@computed('model.isPrivateMessage')
|
||||
canEditTags(isPrivateMessage) {
|
||||
return !isPrivateMessage && this.site.get('can_tag_topics');
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
fillGapBefore(args) {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
export default registerUnbound('discourse-tag', function(name, params) {
|
||||
return new Handlebars.SafeString(renderTag(name, params));
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import { CANCELLED_STATUS } from 'discourse/lib/autocomplete';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
var cache = {};
|
||||
var cacheTime;
|
||||
var oldSearch;
|
||||
|
||||
function updateCache(term, results) {
|
||||
cache[term] = results;
|
||||
cacheTime = new Date();
|
||||
return results;
|
||||
}
|
||||
|
||||
function searchTags(term, categories, limit) {
|
||||
return new Ember.RSVP.Promise((resolve) => {
|
||||
const clearPromise = setTimeout(() => {
|
||||
resolve(CANCELLED_STATUS);
|
||||
}, 5000);
|
||||
|
||||
const debouncedSearch = _.debounce((q, cats, resultFunc) => {
|
||||
oldSearch = $.ajax(Discourse.getURL("/tags/filter/search"), {
|
||||
type: 'GET',
|
||||
cache: true,
|
||||
data: { limit: limit, q }
|
||||
});
|
||||
|
||||
var returnVal = CANCELLED_STATUS;
|
||||
|
||||
oldSearch.then((r) => {
|
||||
var tags = r.results.map((tag) => { return { text: tag.text, count: tag.count }; });
|
||||
returnVal = cats.concat(tags);
|
||||
}).always(() => {
|
||||
oldSearch = null;
|
||||
resultFunc(returnVal);
|
||||
});
|
||||
}, 300);
|
||||
|
||||
debouncedSearch(term, categories, (result) => {
|
||||
clearTimeout(clearPromise);
|
||||
resolve(updateCache(term, result));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function search(term, siteSettings) {
|
||||
if (oldSearch) {
|
||||
oldSearch.abort();
|
||||
oldSearch = null;
|
||||
}
|
||||
|
||||
if ((new Date() - cacheTime) > 30000) cache = {};
|
||||
const cached = cache[term];
|
||||
if (cached) return cached;
|
||||
|
||||
const limit = 5;
|
||||
var categories = Category.search(term, { limit });
|
||||
var numOfCategories = categories.length;
|
||||
categories = categories.map((category) => { return { model: category }; });
|
||||
|
||||
if (numOfCategories !== limit && siteSettings.tagging_enabled) {
|
||||
return searchTags(term, categories, limit - numOfCategories);
|
||||
} else {
|
||||
return updateCache(term, categories);
|
||||
}
|
||||
};
|
52
app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
Normal file
52
app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { replaceSpan } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
|
||||
const validTagHashtags = {};
|
||||
const checkedTagHashtags = [];
|
||||
const testedClass = 'tag-hashtag-tested';
|
||||
|
||||
function updateFound($hashtags, tagValues) {
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
$hashtags.each((index, hashtag) => {
|
||||
const tagValue = tagValues[index];
|
||||
const link = validTagHashtags[tagValue];
|
||||
const $hashtag = $(hashtag);
|
||||
|
||||
if (link) {
|
||||
replaceSpan($hashtag, tagValue, link);
|
||||
} else if (checkedTagHashtags.indexOf(tagValue) !== -1) {
|
||||
$hashtag.addClass(testedClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function linkSeenTagHashtags($elem) {
|
||||
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
|
||||
const unseen = [];
|
||||
|
||||
if ($hashtags.length) {
|
||||
const tagValues = $hashtags.map((_, hashtag) => {
|
||||
return $(hashtag).text().substr(1).replace(`${TAG_HASHTAG_POSTFIX}`, "");
|
||||
});
|
||||
|
||||
if (tagValues.length) {
|
||||
_.uniq(tagValues).forEach((tagValue) => {
|
||||
if (checkedTagHashtags.indexOf(tagValue) === -1) unseen.push(tagValue);
|
||||
});
|
||||
}
|
||||
updateFound($hashtags, tagValues);
|
||||
}
|
||||
|
||||
return unseen;
|
||||
};
|
||||
|
||||
export function fetchUnseenTagHashtags(tagValues) {
|
||||
return Discourse.ajax("/tags/check", { data: { tag_values: tagValues } })
|
||||
.then((response) => {
|
||||
response.valid.forEach((tag) => {
|
||||
validTagHashtags[tag.value] = tag.url;
|
||||
});
|
||||
checkedTagHashtags.push.apply(checkedTagHashtags, tagValues);
|
||||
});
|
||||
}
|
37
app/assets/javascripts/discourse/lib/render-tag.js.es6
Normal file
37
app/assets/javascripts/discourse/lib/render-tag.js.es6
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { h } from 'virtual-dom';
|
||||
|
||||
export default function renderTag(tag, params) {
|
||||
params = params || {};
|
||||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
const href = tagName === "a" ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
}
|
||||
|
||||
let val = "<" + tagName + href + " class='" + classes.join(" ") + "'>" + tag + "</" + tagName + ">";
|
||||
|
||||
if (params.count) {
|
||||
val += " <span class='discourse-tag-count'>x" + params.count + "</span>";
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
export function tagNode(tag, params) {
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
}
|
||||
|
||||
if (tagName === 'a') {
|
||||
const href = Discourse.getURL(`/tags/${tag}`);
|
||||
return h(tagName, { className: classes.join(' '), attributes: { href } }, tag);
|
||||
} else {
|
||||
return h(tagName, { className: classes.join(' ') }, tag);
|
||||
}
|
||||
}
|
1
app/assets/javascripts/discourse/lib/tag-hashtags.js.es6
Normal file
1
app/assets/javascripts/discourse/lib/tag-hashtags.js.es6
Normal file
|
@ -0,0 +1 @@
|
|||
export const TAG_HASHTAG_POSTFIX = '::tag';
|
|
@ -28,12 +28,14 @@ const CLOSED = 'closed',
|
|||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
composer_open_duration_msecs: 'composerTime'
|
||||
composer_open_duration_msecs: 'composerTime',
|
||||
tags: 'tags'
|
||||
},
|
||||
|
||||
_edit_topic_serializer = {
|
||||
title: 'topic.title',
|
||||
categoryId: 'topic.category.id'
|
||||
categoryId: 'topic.category.id',
|
||||
tags: 'topic.tags'
|
||||
};
|
||||
|
||||
const Composer = RestModel.extend({
|
||||
|
|
|
@ -72,6 +72,24 @@ const Topic = RestModel.extend({
|
|||
return this.store.createRecord('postStream', {id: this.get('id'), topic: this});
|
||||
}.property(),
|
||||
|
||||
@computed('tags')
|
||||
visibleListTags(tags) {
|
||||
if (!tags || !Discourse.SiteSettings.suppress_overlapping_tags_in_list) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
const title = this.get('title');
|
||||
const newTags = [];
|
||||
|
||||
tags.forEach(function(tag){
|
||||
if (title.toLowerCase().indexOf(tag) === -1 || Discourse.SiteSettings.staff_tags.indexOf(tag) !== -1) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
return newTags;
|
||||
},
|
||||
|
||||
replyCount: function() {
|
||||
return this.get('posts_count') - 1;
|
||||
}.property('posts_count'),
|
||||
|
|
|
@ -118,4 +118,16 @@ export default function() {
|
|||
this.resource('queued-posts', { path: '/queued-posts' });
|
||||
|
||||
this.route('full-page-search', {path: '/search'});
|
||||
|
||||
this.resource('tags', function() {
|
||||
this.route('show', {path: '/:tag_id'});
|
||||
this.route('showCategory', {path: '/c/:category/:tag_id'});
|
||||
this.route('showParentCategory', {path: '/c/:parent_category/:category/:tag_id'});
|
||||
|
||||
Discourse.Site.currentProp('filters').forEach(filter => {
|
||||
this.route('show' + filter.capitalize(), {path: '/:tag_id/l/' + filter});
|
||||
this.route('showCategory' + filter.capitalize(), {path: '/c/:category/:tag_id/l/' + filter});
|
||||
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
12
app/assets/javascripts/discourse/routes/tags-index.js.es6
Normal file
12
app/assets/javascripts/discourse/routes/tags-index.js.es6
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('tag');
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.controllerFor("application").set("showFooter", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
137
app/assets/javascripts/discourse/routes/tags-show.js.es6
Normal file
137
app/assets/javascripts/discourse/routes/tags-show.js.es6
Normal file
|
@ -0,0 +1,137 @@
|
|||
import Composer from 'discourse/models/composer';
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { findTopicList } from 'discourse/routes/build-topic-route';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
navMode: 'latest',
|
||||
|
||||
renderTemplate() {
|
||||
const controller = this.controllerFor('tags.show');
|
||||
this.render('tags.show', { controller });
|
||||
},
|
||||
|
||||
model(params) {
|
||||
var tag = this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) }),
|
||||
f = '';
|
||||
|
||||
if (params.category) {
|
||||
f = 'c/';
|
||||
if (params.parent_category) { f += params.parent_category + '/'; }
|
||||
f += params.category + '/l/';
|
||||
}
|
||||
f += this.get('navMode');
|
||||
this.set('filterMode', f);
|
||||
|
||||
if (params.category) { this.set('categorySlug', params.category); }
|
||||
if (params.parent_category) { this.set('parentCategorySlug', params.parent_category); }
|
||||
|
||||
if (this.get("currentUser")) {
|
||||
// If logged in, we should get the tag"s user settings
|
||||
return this.store.find("tagNotification", tag.get("id")).then(tn => {
|
||||
this.set("tagNotification", tn);
|
||||
return tag;
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
},
|
||||
|
||||
afterModel(tag) {
|
||||
const controller = this.controllerFor('tags.show');
|
||||
controller.set('loading', true);
|
||||
|
||||
const params = controller.getProperties('order', 'ascending');
|
||||
|
||||
const categorySlug = this.get('categorySlug');
|
||||
const parentCategorySlug = this.get('parentCategorySlug');
|
||||
const filter = this.get('navMode');
|
||||
|
||||
if (categorySlug) {
|
||||
var category = Discourse.Category.findBySlug(categorySlug, parentCategorySlug);
|
||||
if (parentCategorySlug) {
|
||||
params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag.id}/l/${filter}`;
|
||||
} else {
|
||||
params.filter = `tags/c/${categorySlug}/${tag.id}/l/${filter}`;
|
||||
}
|
||||
|
||||
this.set('category', category);
|
||||
} else {
|
||||
params.filter = `tags/${tag.id}/l/${filter}`;
|
||||
this.set('category', null);
|
||||
}
|
||||
|
||||
return findTopicList(this.store, this.topicTrackingState, params.filter, params, {}).then(function(list) {
|
||||
controller.set('list', list);
|
||||
controller.set('canCreateTopic', list.get('can_create_topic'));
|
||||
if (list.topic_list.tags) {
|
||||
Discourse.Site.currentProp('top_tags', list.topic_list.tags);
|
||||
}
|
||||
controller.set('loading', false);
|
||||
});
|
||||
},
|
||||
|
||||
titleToken() {
|
||||
const filterText = I18n.t('filters.' + this.get('navMode').replace('/', '.') + '.title'),
|
||||
controller = this.controllerFor('tags.show');
|
||||
|
||||
if (this.get('category')) {
|
||||
return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')});
|
||||
} else {
|
||||
return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')});
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this.controllerFor('tags.show').setProperties({
|
||||
model,
|
||||
tag: model,
|
||||
category: this.get('category'),
|
||||
filterMode: this.get('filterMode'),
|
||||
navMode: this.get('navMode'),
|
||||
tagNotification: this.get('tagNotification')
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
invalidateModel() {
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
renameTag(tag) {
|
||||
showModal("rename-tag", { model: tag });
|
||||
},
|
||||
|
||||
createTopic() {
|
||||
var controller = this.controllerFor("tags.show"),
|
||||
self = this;
|
||||
|
||||
this.controllerFor('composer').open({
|
||||
categoryId: controller.get('category.id'),
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: controller.get('list.draft_key'),
|
||||
draftSequence: controller.get('list.draft_sequence')
|
||||
}).then(function() {
|
||||
// Pre-fill the tags input field
|
||||
if (controller.get('model.id')) {
|
||||
var c = self.controllerFor('composer').get('model');
|
||||
c.set('tags', [controller.get('model.id')]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
didTransition() {
|
||||
this.controllerFor("tags.show")._showFooter();
|
||||
return true;
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
if (!Discourse.SiteSettings.show_filter_by_tag) { return true; }
|
||||
|
||||
if ((transition.targetName.indexOf("discovery.parentCategory") !== -1 ||
|
||||
transition.targetName.indexOf("discovery.category") !== -1) && !transition.queryParams.allTags ) {
|
||||
this.transitionTo("/tags" + transition.intent.url + "/" + this.currentModel.get("id"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
5
app/assets/javascripts/discourse/templates/bulk-tag.hbs
Normal file
5
app/assets/javascripts/discourse/templates/bulk-tag.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<p>{{i18n "topics.bulk.choose_new_tags"}}</p>
|
||||
|
||||
<p>{{tag-chooser tags=tags}}</p>
|
||||
|
||||
{{d-button action="changeTags" disabled=emptyTags label="topics.bulk.change_tags"}}
|
|
@ -0,0 +1,13 @@
|
|||
<div class='autocomplete'>
|
||||
<ul>
|
||||
{{#each option in options}}
|
||||
<li>
|
||||
{{#if option.model}}
|
||||
<a href>{{category-link option.model allowUncategorized="true" link="false"}}</a>
|
||||
{{else}}
|
||||
<a href>{{fa-icon 'tag'}}{{option.text}} x {{option.count}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
|
@ -4,6 +4,10 @@
|
|||
{{category-drop category=secondCategory parentCategory=firstCategory categories=childCategories subCategory="true" noSubcategories=noSubcategories}}
|
||||
{{/if}}
|
||||
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
{{tag-drop firstCategory=firstCategory secondCategory=secondCategory tagId=tagId}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "bread-crumbs-right" tagName="li"}}
|
||||
|
||||
<div class='clear'></div>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{{tagRecord.id}}
|
|
@ -0,0 +1,21 @@
|
|||
{{#if showTagDropdown}}
|
||||
{{#if tagId}}
|
||||
<a href {{action "expand"}} {{bind-attr class=":badge-category tagClass"}}>{{tagId}}</a>
|
||||
{{else}}
|
||||
<a href {{action "expand"}} {{bind-attr class=":badge-category tagClass :home"}}>{{allTagsLabel}}</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if tags}}
|
||||
<a href {{action "expand"}} class={{dropdownButtonClass}}><i class={{iconClass}}></i></a>
|
||||
<section {{bind-attr class="expanded::hidden :category-dropdown-menu :chooser"}}>
|
||||
<div class='cat'><a {{bind-attr href=allTagsUrl}} data-drop-close="true" class='badge-category home'>{{allTagsLabel}}</a></div>
|
||||
{{#if renderTags}}
|
||||
{{#each t in tags}}
|
||||
<div class='cat'>
|
||||
{{tag-drop-link tagId=t category=currentCategory}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</section>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -2,5 +2,12 @@
|
|||
{{bound-category-link topic.category.parentCategory}}
|
||||
{{/if}}
|
||||
{{bound-category-link topic.category hideParent=true}}
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
<div class="list-tags">
|
||||
{{#each t in topic.tags}}
|
||||
{{discourse-tag t}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "topic-category"}}
|
||||
|
|
|
@ -97,6 +97,9 @@
|
|||
{{#if currentUser}}
|
||||
<div class='submit-panel'>
|
||||
{{plugin-outlet "composer-fields-below"}}
|
||||
{{#if canEditTags}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4"}}
|
||||
{{/if}}
|
||||
<button {{action "save"}} tabindex="5" {{bind-attr class=":btn :btn-primary :create disableSubmit:disabled"}} title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button>
|
||||
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
|
||||
|
||||
|
|
|
@ -70,6 +70,9 @@
|
|||
</a>
|
||||
<div class='search-category'>
|
||||
{{category-link result.topic.category}}
|
||||
{{#each result.topic.tags as |tag|}}
|
||||
{{discourse-tag tag}}
|
||||
{{/each}}
|
||||
{{plugin-outlet "full-page-search-category"}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,13 @@
|
|||
{{#if controller.showTopicPostBadges}}
|
||||
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
|
||||
{{/if}}
|
||||
{{#if topic.tags}}
|
||||
<div class='discourse-tags'>
|
||||
{{#each tag in topic.visibleListTags}}
|
||||
{{discourse-tag tag}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{plugin-outlet "topic-list-tags"}}
|
||||
{{#if expandPinned}}
|
||||
{{raw "list/topic-excerpt" topic=topic}}
|
||||
|
|
|
@ -27,6 +27,14 @@
|
|||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if context.topic.tags}}
|
||||
<div class='discourse-tags'>
|
||||
{{#each tag in context.topic.visibleListTags}}
|
||||
{{discourse-tag tag}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "topic-list-tags"}}
|
||||
|
||||
<div class="pull-right">
|
||||
|
|
|
@ -73,6 +73,19 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if model.tags_changes}}
|
||||
<div class='row'>
|
||||
{{i18n "tagging.changed"}}
|
||||
{{#each previousTagChanges as |t|}}
|
||||
{{discourse-tag t}}
|
||||
{{/each}}
|
||||
→
|
||||
|
||||
{{#each currentTagChanges as |t|}}
|
||||
{{discourse-tag t}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "post-revisions"}}
|
||||
|
||||
|
|
13
app/assets/javascripts/discourse/templates/rename-tag.hbs
Normal file
13
app/assets/javascripts/discourse/templates/rename-tag.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="modal-body">
|
||||
<label>
|
||||
<p>{{i18n "tagging.rename_instructions"}}</p>
|
||||
{{input value=buffered.id maxlength=siteSettings.max_tag_length}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button class="btn-primary"
|
||||
action="performRename"
|
||||
label="tagging.rename_tag"
|
||||
disabled=renameDisabled}}
|
||||
</div>
|
9
app/assets/javascripts/discourse/templates/tags.hbs
Normal file
9
app/assets/javascripts/discourse/templates/tags.hbs
Normal 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>
|
15
app/assets/javascripts/discourse/templates/tags/index.hbs
Normal file
15
app/assets/javascripts/discourse/templates/tags/index.hbs
Normal file
|
@ -0,0 +1,15 @@
|
|||
<h2>{{i18n "tagging.all_tags"}}</h2>
|
||||
|
||||
<div class='tag-sort-options'>
|
||||
{{i18n "tagging.sort_by"}}
|
||||
<a {{action "sortByCount"}}>{{i18n "tagging.sort_by_count"}}</a>
|
||||
<a {{action "sortById"}}>{{i18n "tagging.sort_by_name"}}</a>
|
||||
</div>
|
||||
|
||||
<div class='tag-list'>
|
||||
{{#each tag in sortedTags}}
|
||||
<div class='tag-box'>
|
||||
{{discourse-tag tag.id}} <span class='tag-count'>x {{tag.count}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
58
app/assets/javascripts/discourse/templates/tags/show.hbs
Normal file
58
app/assets/javascripts/discourse/templates/tags/show.hbs
Normal file
|
@ -0,0 +1,58 @@
|
|||
<div class="list-controls">
|
||||
<div class="container">
|
||||
{{#if tagNotification}}
|
||||
{{tag-notifications-button action="changeTagNotification"
|
||||
notificationLevel=tagNotification.notification_level}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showAdminControls}}
|
||||
{{d-button action="deleteTag" label="tagging.delete_tag" icon="trash-o" class="admin-tag btn-danger"}}
|
||||
{{d-button action="renameTag" actionParam=tag label="tagging.rename_tag" icon="pencil" class="admin-tag"}}
|
||||
{{else}}
|
||||
{{#if canCreateTopic}}
|
||||
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showTagFilter}}
|
||||
{{bread-crumbs categories=categories
|
||||
category=category
|
||||
tagId=tag.id
|
||||
noSubcategories=noSubcategories
|
||||
hideSubcategories=showingSubcategoryList}}
|
||||
|
||||
{{navigation-bar navItems=navItems filterMode=filterMode}}
|
||||
{{else}}
|
||||
<h2 class="tag-show-heading">
|
||||
{{#link-to 'tags'}}{{i18n "tagging.tags"}}{{/link-to}}
|
||||
{{fa-icon "angle-right"}}
|
||||
{{discourse-tag-bound tagRecord=tag style="simple"}}
|
||||
</h2>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class='topic-list-bottom'>
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
|
||||
{{#unless loading}}
|
||||
{{#if list.topics}}
|
||||
{{bulk-select-button selected=selected refreshTarget=controller}}
|
||||
|
||||
{{topic-list topics=list.topics
|
||||
canBulkSelect=canBulkSelect
|
||||
toggleBulkSelect="toggleBulkSelect"
|
||||
bulkSelectEnabled=bulkSelectEnabled
|
||||
selected=selected
|
||||
showPosters=true
|
||||
currentUser=currentUser
|
||||
order=order
|
||||
ascending=ascending
|
||||
changeSort="changeSort"}}
|
||||
{{else}}
|
||||
<h3>
|
||||
{{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}
|
||||
</h3>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</footer>
|
|
@ -23,6 +23,10 @@
|
|||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
{{tag-chooser tags=buffered.tags}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "edit-topic"}}
|
||||
|
||||
{{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
|
|
5
app/assets/javascripts/discourse/views/rename-tag.js.es6
Normal file
5
app/assets/javascripts/discourse/views/rename-tag.js.es6
Normal file
|
@ -0,0 +1,5 @@
|
|||
import ModalBodyView from 'discourse/views/modal-body';
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
title: I18n.t("tagging.rename_tag")
|
||||
});
|
3
app/assets/javascripts/discourse/views/tags-show.js.es6
Normal file
3
app/assets/javascripts/discourse/views/tags-show.js.es6
Normal file
|
@ -0,0 +1,3 @@
|
|||
import DiscoveryTopicsView from "discourse/views/discovery-topics";
|
||||
|
||||
export default DiscoveryTopicsView;
|
|
@ -83,6 +83,10 @@ export default createWidget('hamburger-menu', {
|
|||
links.push({ route: 'users', className: 'user-directory-link', label: 'directory.title' });
|
||||
}
|
||||
|
||||
if (this.siteSettings.tagging_enabled) {
|
||||
links.push({ route: 'tags', label: 'tagging.tags' });
|
||||
}
|
||||
|
||||
return links.map(l => this.attach('link', l));
|
||||
},
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { h } from 'virtual-dom';
|
|||
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import RawHtml from 'discourse/widgets/raw-html';
|
||||
import { tagNode } from 'discourse/lib/render-tag';
|
||||
|
||||
export default createWidget('header-topic-info', {
|
||||
tagName: 'div.extra-info-wrapper',
|
||||
|
@ -42,6 +43,13 @@ export default createWidget('header-topic-info', {
|
|||
}
|
||||
title.push(this.attach('category-link', { category }));
|
||||
}
|
||||
|
||||
if (this.siteSettings.tagging_enabled) {
|
||||
const tags = topic.get('tags') || [];
|
||||
if (tags.length) {
|
||||
title.push(h('div.list-tags', tags.map(tagNode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contents = h('div.title-wrapper', title);
|
||||
|
|
192
app/assets/stylesheets/common/base/tagging.scss
Normal file
192
app/assets/stylesheets/common/base/tagging.scss
Normal file
|
@ -0,0 +1,192 @@
|
|||
|
||||
.topic-title-outlet.choose-tags {
|
||||
margin-left: 25px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.tag-box {
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
margin-bottom: 1em;
|
||||
float: left;
|
||||
|
||||
.discourse-tag {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-info-wrapper {
|
||||
.list-tags {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.discourse-tag {
|
||||
-webkit-animation: fadein .7s;
|
||||
animation: fadein .7s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.add-tags .select2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
$tag-color: scale-color($primary, $lightness: 40%);
|
||||
|
||||
.discourse-tag-count {
|
||||
font-size: 0.8em;
|
||||
color: $tag-color;
|
||||
}
|
||||
|
||||
.select2-result-label .discourse-tag {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.discourse-tag {
|
||||
padding: 0;
|
||||
margin: 0 5px 0 0;
|
||||
color: $tag-color;
|
||||
|
||||
&:visited, &:hover {
|
||||
color: $tag-color;
|
||||
}
|
||||
|
||||
&.box {
|
||||
background-color: scale-color($primary, $lightness: 90%);
|
||||
color: scale-color($primary, $lightness: 30%);
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
&.simple, &.simple:visited, &.simple:hover {
|
||||
margin-right: 0px;
|
||||
color: scale-color($primary, $lightness: 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.discourse-tags, .list-tags {
|
||||
.discourse-tag.simple:not(:last-child):after {
|
||||
content: ", ";
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice.discourse-tag-select2 {
|
||||
padding-top: 5px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.fps-result .add-full-page-tags {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.topic-list-item .discourse-tags {
|
||||
display: block;
|
||||
font-size: 0.75em;
|
||||
font-weight: normal;
|
||||
clear: both;
|
||||
margin-top: 5px;
|
||||
|
||||
.discourse-tag.box {
|
||||
position:relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-view .topic-list-item .discourse-tags {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
margin-top: 0;
|
||||
.discourse-tag.box {
|
||||
position:relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.discourse-tag.bullet:before {
|
||||
content: "\f04d";
|
||||
font-family: FontAwesome;
|
||||
color: scale-color($primary, $lightness: 70%);
|
||||
margin-right: 5px;
|
||||
font-size: 0.7em;
|
||||
position:relative;
|
||||
top: -0.1em;
|
||||
}
|
||||
|
||||
header .discourse-tag {color: $tag-color !important; }
|
||||
|
||||
.list-tags {
|
||||
display: inline;
|
||||
margin: 0 0 0 5px;
|
||||
font-size: 0.857em;
|
||||
}
|
||||
|
||||
.tag-chooser {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.admin-tag {
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tag-notification-menu {
|
||||
float: right;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tag-notification-menu .dropdown-menu {
|
||||
right: 0;
|
||||
top: 30px;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bullet + .list-tags {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bar + .list-tags {
|
||||
line-height: 1.25;
|
||||
.discourse-tag {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.box + .list-tags {
|
||||
display: inline-block;
|
||||
margin: 5px 0 0 5px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.tag-sort-options a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
.fa-tag {
|
||||
color: dark-light-choose($primary, scale-color($primary, $lightness: 70%));
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $tag-color;
|
||||
}
|
||||
}
|
192
app/controllers/tags_controller.rb
Normal file
192
app/controllers/tags_controller.rb
Normal file
|
@ -0,0 +1,192 @@
|
|||
require_dependency 'topic_list_responder'
|
||||
require_dependency 'topics_bulk_action'
|
||||
require_dependency 'topic_query'
|
||||
|
||||
class TagsController < ::ApplicationController
|
||||
include TopicListResponder
|
||||
|
||||
before_filter :ensure_tags_enabled
|
||||
|
||||
skip_before_filter :check_xhr, only: [:tag_feed, :show]
|
||||
before_filter :ensure_logged_in, only: [:notifications, :update_notifications, :update]
|
||||
before_filter :set_category_from_params, except: [:index, :update, :destroy, :tag_feed, :search, :notifications, :update_notifications]
|
||||
|
||||
def index
|
||||
tag_counts = self.class.tags_by_count(guardian, limit: 300).count
|
||||
tags = tag_counts.map {|t, c| { id: t, text: t, count: c } }
|
||||
render json: { tags: tags }
|
||||
end
|
||||
|
||||
Discourse.filters.each do |filter|
|
||||
define_method("show_#{filter}") do
|
||||
@tag_id = DiscourseTagging.clean_tag(params[:tag_id])
|
||||
|
||||
# TODO PERF: doesn't scale:
|
||||
topics_tagged = TopicCustomField.where(name: DiscourseTagging::TAGS_FIELD_NAME, value: @tag_id).pluck(:topic_id)
|
||||
|
||||
page = params[:page].to_i
|
||||
|
||||
query = TopicQuery.new(current_user, build_topic_list_options)
|
||||
|
||||
results = query.send("#{filter}_results").where(id: topics_tagged)
|
||||
|
||||
if @filter_on_category
|
||||
category_ids = [@filter_on_category.id] + @filter_on_category.subcategories.pluck(:id)
|
||||
results = results.where(category_id: category_ids)
|
||||
end
|
||||
|
||||
@list = query.create_list(:by_tag, {}, results)
|
||||
|
||||
@list.draft_key = Draft::NEW_TOPIC
|
||||
@list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC)
|
||||
@list.draft = Draft.get(current_user, @list.draft_key, @list.draft_sequence) if current_user
|
||||
|
||||
@list.more_topics_url = list_by_tag_path(tag_id: @tag_id, page: page + 1)
|
||||
@rss = "tag"
|
||||
|
||||
|
||||
if @list.topics.size == 0 && !TopicCustomField.where(name: DiscourseTagging::TAGS_FIELD_NAME, value: @tag_id).exists?
|
||||
raise Discourse::NotFound
|
||||
else
|
||||
respond_with_list(@list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
show_latest
|
||||
end
|
||||
|
||||
def update
|
||||
guardian.ensure_can_admin_tags!
|
||||
|
||||
new_tag_id = DiscourseTagging.clean_tag(params[:tag][:id])
|
||||
if current_user.staff?
|
||||
DiscourseTagging.rename_tag(current_user, params[:tag_id], new_tag_id)
|
||||
end
|
||||
render json: { tag: { id: new_tag_id }}
|
||||
end
|
||||
|
||||
def destroy
|
||||
guardian.ensure_can_admin_tags!
|
||||
tag_id = params[:tag_id]
|
||||
TopicCustomField.transaction do
|
||||
TopicCustomField.where(name: DiscourseTagging::TAGS_FIELD_NAME, value: tag_id).delete_all
|
||||
UserCustomField.delete_all(name: ::DiscourseTagging.notification_key(tag_id))
|
||||
StaffActionLogger.new(current_user).log_custom('deleted_tag', subject: tag_id)
|
||||
end
|
||||
render json: success_json
|
||||
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: DiscourseTagging::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, params.slice(:limit))
|
||||
term = params[:q]
|
||||
if term.present?
|
||||
term.gsub!(/[^a-z0-9\.\-\_]*/, '')
|
||||
term.gsub!("_", "\\_")
|
||||
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_notification: { id: params[:tag_id], notification_level: level.to_i } }
|
||||
end
|
||||
|
||||
def update_notifications
|
||||
level = params[:tag_notification][:notification_level].to_i
|
||||
|
||||
current_user.custom_fields[::DiscourseTagging.notification_key(params[:tag_id])] = level
|
||||
current_user.save_custom_fields
|
||||
|
||||
render json: {notification_level: level}
|
||||
end
|
||||
|
||||
def check_hashtag
|
||||
tag_values = params[:tag_values].each(&:downcase!)
|
||||
|
||||
valid_tags = TopicCustomField.where(name: DiscourseTagging::TAGS_FIELD_NAME, value: tag_values).map do |tag|
|
||||
{ value: tag.value, url: "#{Discourse.base_url}/tags/#{tag.value}" }
|
||||
end.compact
|
||||
|
||||
render json: { valid: valid_tags }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_tags_enabled
|
||||
raise Discourse::NotFound unless SiteSetting.tagging_enabled?
|
||||
end
|
||||
|
||||
def self.tags_by_count(guardian, opts=nil)
|
||||
opts = opts || {}
|
||||
result = TopicCustomField.where(name: DiscourseTagging::TAGS_FIELD_NAME)
|
||||
.joins(:topic)
|
||||
.group(:value)
|
||||
.limit(opts[:limit] || 5)
|
||||
.order('COUNT(topic_custom_fields.value) DESC')
|
||||
|
||||
guardian.filter_allowed_categories(result)
|
||||
end
|
||||
|
||||
def set_category_from_params
|
||||
slug_or_id = params[:category]
|
||||
return true if slug_or_id.nil?
|
||||
|
||||
parent_slug_or_id = params[:parent_category]
|
||||
|
||||
parent_category_id = nil
|
||||
if parent_slug_or_id.present?
|
||||
parent_category_id = Category.query_parent_category(parent_slug_or_id)
|
||||
raise Discourse::NotFound if parent_category_id.blank?
|
||||
end
|
||||
|
||||
@filter_on_category = Category.query_category(slug_or_id, parent_category_id)
|
||||
raise Discourse::NotFound if !@filter_on_category
|
||||
|
||||
guardian.ensure_can_see!(@filter_on_category)
|
||||
end
|
||||
|
||||
def build_topic_list_options
|
||||
options = {
|
||||
page: params[:page],
|
||||
topic_ids: param_to_integer_list(:topic_ids),
|
||||
exclude_category_ids: params[:exclude_category_ids],
|
||||
category: params[:category],
|
||||
order: params[:order],
|
||||
ascending: params[:ascending],
|
||||
min_posts: params[:min_posts],
|
||||
max_posts: params[:max_posts],
|
||||
status: params[:status],
|
||||
filter: params[:filter],
|
||||
state: params[:state],
|
||||
search: params[:search],
|
||||
q: params[:q]
|
||||
}
|
||||
options[:no_subcategories] = true if params[:no_subcategories] == 'true'
|
||||
options[:slow_platform] = true if slow_platform?
|
||||
|
||||
options
|
||||
end
|
||||
end
|
|
@ -14,6 +14,15 @@ module TopicsHelper
|
|||
end
|
||||
breadcrumb.push url: category.url, name: category.name
|
||||
end
|
||||
|
||||
if (tags = topic.tags).present?
|
||||
tags.each do |tag|
|
||||
tag_id = DiscourseTagging.clean_tag(tag)
|
||||
url = "#{Discourse.base_url}/tags/#{tag_id}"
|
||||
breadcrumb << {url: url, name: tag}
|
||||
end
|
||||
end
|
||||
|
||||
Plugin::Filter.apply(:topic_categories_breadcrumb, topic, breadcrumb)
|
||||
end
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ require_dependency 'text_sentinel'
|
|||
require_dependency 'text_cleaner'
|
||||
require_dependency 'archetype'
|
||||
require_dependency 'html_prettify'
|
||||
require_dependency 'discourse_tagging'
|
||||
|
||||
class Topic < ActiveRecord::Base
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
@ -1043,6 +1044,11 @@ SQL
|
|||
builder.exec.first["count"].to_i
|
||||
end
|
||||
|
||||
def tags
|
||||
result = custom_fields[DiscourseTagging::TAGS_FIELD_NAME]
|
||||
[result].flatten unless result.blank?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_category_topic_count_by(num)
|
||||
|
|
|
@ -81,8 +81,11 @@ class TopicList
|
|||
ft.topic_list = self
|
||||
end
|
||||
|
||||
if TopicList.preloaded_custom_fields.present?
|
||||
Topic.preload_custom_fields(@topics, TopicList.preloaded_custom_fields)
|
||||
preload_custom_fields = TopicList.preloaded_custom_fields
|
||||
preload_custom_fields << DiscourseTagging::TAGS_FIELD_NAME if SiteSetting.tagging_enabled
|
||||
|
||||
if preload_custom_fields.present?
|
||||
Topic.preload_custom_fields(@topics, preload_custom_fields)
|
||||
end
|
||||
|
||||
@topics
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require_dependency 'discourse_tagging'
|
||||
|
||||
class SiteSerializer < ApplicationSerializer
|
||||
|
||||
attributes :default_archetype,
|
||||
|
@ -14,7 +16,11 @@ class SiteSerializer < ApplicationSerializer
|
|||
:user_field_max_length,
|
||||
:suppressed_from_homepage_category_ids,
|
||||
:post_action_types,
|
||||
:topic_flag_types
|
||||
:topic_flag_types,
|
||||
:can_create_tag,
|
||||
:can_tag_topics,
|
||||
:tags_filter_regexp,
|
||||
:top_tags
|
||||
|
||||
has_many :categories, serializer: BasicCategorySerializer, embed: :objects
|
||||
has_many :trust_levels, embed: :objects
|
||||
|
@ -81,4 +87,26 @@ class SiteSerializer < ApplicationSerializer
|
|||
UserField.max_length
|
||||
end
|
||||
|
||||
def can_create_tag
|
||||
SiteSetting.tagging_enabled && scope.can_create_tag?
|
||||
end
|
||||
|
||||
def can_tag_topics
|
||||
SiteSetting.tagging_enabled && scope.can_tag_topics?
|
||||
end
|
||||
|
||||
def include_tags_filter_regexp?
|
||||
SiteSetting.tagging_enabled
|
||||
end
|
||||
def tags_filter_regexp
|
||||
DiscourseTagging::TAGS_FILTER_REGEXP.source
|
||||
end
|
||||
|
||||
def include_top_tags?
|
||||
SiteSetting.tagging_enabled && SiteSetting.show_filter_by_tag
|
||||
end
|
||||
def top_tags
|
||||
DiscourseTagging.top_tags
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -9,7 +9,8 @@ class TopicListItemSerializer < ListableTopicSerializer
|
|||
:op_like_count,
|
||||
:pinned_globally,
|
||||
:bookmarked_post_numbers,
|
||||
:liked_post_numbers
|
||||
:liked_post_numbers,
|
||||
:tags
|
||||
|
||||
has_many :posters, serializer: TopicPosterSerializer, embed: :objects
|
||||
has_many :participants, serializer: TopicPosterSerializer, embed: :objects
|
||||
|
@ -63,4 +64,11 @@ class TopicListItemSerializer < ListableTopicSerializer
|
|||
object.association(:first_post).loaded?
|
||||
end
|
||||
|
||||
def include_tags?
|
||||
SiteSetting.tagging_enabled
|
||||
end
|
||||
def tags
|
||||
object.tags
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ class TopicListSerializer < ApplicationSerializer
|
|||
:draft_key,
|
||||
:draft_sequence,
|
||||
:for_period,
|
||||
:per_page
|
||||
:per_page,
|
||||
:tags
|
||||
|
||||
has_many :topics, serializer: TopicListItemSerializer, embed: :objects
|
||||
|
||||
|
@ -22,4 +23,11 @@ class TopicListSerializer < ApplicationSerializer
|
|||
object.more_topics_url.present? && (object.topics.size == object.per_page)
|
||||
end
|
||||
|
||||
def include_tags?
|
||||
SiteSetting.tagging_enabled && SiteSetting.show_filter_by_tag
|
||||
end
|
||||
def tags
|
||||
DiscourseTagging.top_tags
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -33,7 +33,8 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
:word_count,
|
||||
:deleted_at,
|
||||
:pending_posts_count,
|
||||
:user_id
|
||||
:user_id,
|
||||
:tags
|
||||
|
||||
attributes :draft,
|
||||
:draft_key,
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
<%= server_plugin_outlet "topic_list_header" %>
|
||||
|
||||
<%- if SiteSetting.tagging_enabled && @tag_id %>
|
||||
<h1>
|
||||
<%= link_to "#{Discourse.base_url}/t/#{@tag_id}", itemprop: 'item' do %>
|
||||
<span itemprop='name'><%= @tag_id %></span>
|
||||
<% end %>
|
||||
</h1>
|
||||
<% end %>
|
||||
|
||||
<% if @category %>
|
||||
<h1>
|
||||
<% if @category.parent_category %>
|
||||
|
|
|
@ -17,7 +17,19 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%- if SiteSetting.tagging_enabled && @topic_view.topic.tags.present? %>
|
||||
<div class='tags'>
|
||||
<%= t 'js.tagging.tags' %>:
|
||||
|
||||
<%- @topic_view.topic.tags.each do |t| %>
|
||||
<%= t %>
|
||||
<%- end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= server_plugin_outlet "topic_header" %>
|
||||
|
||||
<hr>
|
||||
|
||||
<%- if include_crawler_content? %>
|
||||
|
|
|
@ -1052,6 +1052,9 @@ de:
|
|||
selected:
|
||||
one: "Du hast <b>ein</b> Thema ausgewählt."
|
||||
other: "Du hast <b>{{count}}</b> Themen ausgewählt."
|
||||
change_tags: "Tags ändern"
|
||||
choose_new_tags: "Wähle neue Tags für diese Themen:"
|
||||
changed_tags: "Die Tags dieser Themen wurden geändert."
|
||||
none:
|
||||
unread: "Du hast alle Themen gelesen."
|
||||
new: "Es gibt für dich keine neuen Themen."
|
||||
|
@ -2204,6 +2207,8 @@ de:
|
|||
grant_moderation: "Moderation gewähren"
|
||||
revoke_moderation: "Moderation entziehen"
|
||||
backup_operation: "Backup läuft"
|
||||
deleted_tag: "Tag löschen"
|
||||
renamed_tag: "Tag umbenennen"
|
||||
screened_emails:
|
||||
title: "Gefilterte E-Mails"
|
||||
description: "Wenn jemand ein Konto erstellt, werden die folgenden E-Mail-Adressen überprüft und es wird die Anmeldung blockiert oder eine andere Aktion ausgeführt."
|
||||
|
@ -2699,3 +2704,32 @@ de:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
tagging:
|
||||
all_tags: "Alle Tags"
|
||||
selector_all_tags: "Alle Tags"
|
||||
changed: "Tags geändert:"
|
||||
tags: "Tags"
|
||||
choose_for_topic: "Wähle optional Tags für dieses Thema"
|
||||
topics_tagged: "Themen getaggt mit <span class='discourse-tag'>{{tag}}</span>"
|
||||
delete_tag: "Tag löschen"
|
||||
delete_confirm: "Bist du sicher, dass du das Tag löschen möchtest?"
|
||||
rename_tag: "Tag umbenennen"
|
||||
rename_instructions: "Wähle einen neuen Namen für das Tag:"
|
||||
sort_by: "Sortieren nach:"
|
||||
sort_by_count: "Anzahl"
|
||||
sort_by_name: "Name"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Beobachten"
|
||||
description: "Du wirst automatisch alle neuen Themen dieses Tags beobachten. Du wirst über jeden neuen Beitrag und jedes neue Thema benachrichtigt. Die Anzahl der ungelesenen und neuen Beiträge wird neben dem Thema erscheinen."
|
||||
tracking:
|
||||
title: "Verfolgen"
|
||||
description: "Du wirst automatisch alle neuen Themen dieses Tags verfolgen. Die Anzahl der ungelesenen und neuen Beiträge wird neben dem Thema erscheinen."
|
||||
regular:
|
||||
title: "Normal"
|
||||
description: "Du wirst benachrichtigt, wenn jemand deinen @Namen erwähnt oder auf deinen Beitrag antwortet."
|
||||
muted:
|
||||
title: "Stummgeschaltet"
|
||||
description: "Du erhältst keine Benachrichtigungen über neue Themen mit diesem Tag und es wird nicht in deiner Liste ungelesener Tags erscheinen."
|
||||
|
|
|
@ -1164,6 +1164,9 @@ en:
|
|||
selected:
|
||||
one: "You have selected <b>1</b> topic."
|
||||
other: "You have selected <b>{{count}}</b> topics."
|
||||
change_tags: "Change Tags"
|
||||
choose_new_tags: "Choose new tags for these topics:"
|
||||
changed_tags: "The tags of those topics were changed."
|
||||
|
||||
none:
|
||||
unread: "You have no unread topics."
|
||||
|
@ -2410,6 +2413,8 @@ en:
|
|||
grant_moderation: "grant moderation"
|
||||
revoke_moderation: "revoke moderation"
|
||||
backup_operation: "backup operation"
|
||||
deleted_tag: "deleted tag"
|
||||
renamed_tag: "renamed tag"
|
||||
screened_emails:
|
||||
title: "Screened Emails"
|
||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||
|
@ -2704,6 +2709,7 @@ en:
|
|||
login: "Login"
|
||||
plugins: "Plugins"
|
||||
user_preferences: "User Preferences"
|
||||
tags: "Tags"
|
||||
|
||||
badges:
|
||||
title: Badges
|
||||
|
@ -2922,3 +2928,58 @@ en:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
tagging:
|
||||
all_tags: "All Tags"
|
||||
selector_all_tags: "all tags"
|
||||
changed: "tags changed:"
|
||||
tags: "Tags"
|
||||
choose_for_topic: "choose optional tags for this topic"
|
||||
delete_tag: "Delete Tag"
|
||||
delete_confirm: "Are you sure you want to delete that tag?"
|
||||
rename_tag: "Rename Tag"
|
||||
rename_instructions: "Choose a new name for the tag:"
|
||||
sort_by: "Sort by:"
|
||||
sort_by_count: "count"
|
||||
sort_by_name: "name"
|
||||
|
||||
filters:
|
||||
without_category: "%{filter} %{tag} topics"
|
||||
with_category: "%{filter} %{tag} topics in %{category}"
|
||||
|
||||
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."
|
||||
|
||||
topics:
|
||||
none:
|
||||
unread: "You have no unread topics."
|
||||
new: "You have no new topics."
|
||||
read: "You haven't read any topics yet."
|
||||
posted: "You haven't posted in any topics yet."
|
||||
latest: "There are no latest topics."
|
||||
hot: "There are no hot topics."
|
||||
bookmarks: "You have no bookmarked topics yet."
|
||||
top: "There are no top topics."
|
||||
search: "There are no search results."
|
||||
bottom:
|
||||
latest: "There are no more latest topics."
|
||||
hot: "There are no more hot topics."
|
||||
posted: "There are no more posted topics."
|
||||
read: "There are no more read topics."
|
||||
new: "There are no more new topics."
|
||||
unread: "There are no more unread topics."
|
||||
top: "There are no more top topics."
|
||||
bookmarks: "There are no more bookmarked topics."
|
||||
search: "There are no more search results."
|
||||
|
||||
|
|
|
@ -1052,6 +1052,9 @@ es:
|
|||
selected:
|
||||
one: "Has seleccionado <b>1</b> tema."
|
||||
other: "Has seleccionado <b>{{count}}</b> temas."
|
||||
change_tags: "Cambiar etiquetas"
|
||||
choose_new_tags: "Elegir nuevas etiquetas para estos temas:"
|
||||
changed_tags: "Las etiquetas de estos temas fueron cambiadas."
|
||||
none:
|
||||
unread: "No hay temas que sigas y que no hayas leído ya."
|
||||
new: "No tienes temas nuevos por leer."
|
||||
|
@ -2208,6 +2211,8 @@ es:
|
|||
grant_moderation: "conceder moderación"
|
||||
revoke_moderation: "revocar moderación"
|
||||
backup_operation: "operación de copia de seguridad de respaldo"
|
||||
deleted_tag: "etiqueta eliminada"
|
||||
renamed_tag: "etiqueta renombrada"
|
||||
screened_emails:
|
||||
title: "Correos bloqueados"
|
||||
description: "Cuando alguien trata de crear una cuenta nueva, los siguientes correos serán revisados y el registro será bloqueado, o alguna otra acción será realizada."
|
||||
|
@ -2703,3 +2708,54 @@ es:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
tagging:
|
||||
all_tags: "Etiquetas"
|
||||
selector_all_tags: "etiquetas"
|
||||
changed: "etiquetas cambiadas:"
|
||||
tags: "Etiquetas"
|
||||
choose_for_topic: "elegir etiquetas para este tema (opcional)"
|
||||
delete_tag: "Eliminar etiqueta"
|
||||
delete_confirm: "¿Seguro que quieres eliminar esta etiqueta?"
|
||||
rename_tag: "Renombrar etiqueta"
|
||||
rename_instructions: "Elige un nuevo nombre para la etiqueta:"
|
||||
sort_by: "Ordenar por:"
|
||||
sort_by_count: "volumen"
|
||||
sort_by_name: "nombre"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Vigilar"
|
||||
description: "Vigilarás automáticamente todos los nuevos temas con esta etiqueta. Se añadirá un contador de posts nuevos y sin leer al lado del tema y además, se te notificará de cada nuevo tema y post."
|
||||
tracking:
|
||||
title: "Seguir"
|
||||
description: "Seguirás automáticamente todos los nuevos temas con esta etiqueta. Se añadirá un contador de posts nuevos y sin leer al lado del tema."
|
||||
regular:
|
||||
title: "Normal"
|
||||
description: "Se te notificará solo si alguien te menciona con tu @usuario o responde a algún post tuyo."
|
||||
muted:
|
||||
title: "Silenciar"
|
||||
description: "No se te notificará de nuevos temas con esta etiqueta, ni aparecerán en tu pestaña de temas no leídos."
|
||||
|
||||
topics:
|
||||
none:
|
||||
unread: "No tienes temas sin leer."
|
||||
new: "No hay temas nuevos."
|
||||
read: "Aún no has leído ningún tema."
|
||||
posted: "Aún no has publicado en ningún tema."
|
||||
latest: "No hay temas recientes."
|
||||
hot: "No hay temas candentes"
|
||||
bookmarks: "Aún no has guardado temas en marcadores."
|
||||
top: "No hay temas top."
|
||||
search: "No hay resultados resultados de búsqueda."
|
||||
bottom:
|
||||
latest: "No hay más temas recientes."
|
||||
hot: "No hay más temas candentes."
|
||||
posted: "No hay más temas en los que hayas publicado."
|
||||
read: "No hay más temas que hayas leído."
|
||||
new: "No hay más temas nuevos."
|
||||
unread: "No hay más temas sin leer."
|
||||
top: "No hay más temas top."
|
||||
bookmarks: "No hay más temas en marcadores."
|
||||
search: "No hay más resultados de búsqueda."
|
||||
|
||||
|
|
|
@ -918,6 +918,9 @@ fa_IR:
|
|||
choose_new_category: "یک دسته بندی جدید برای موضوع انتخاب نمایید"
|
||||
selected:
|
||||
other: "شما تعداد <b>{{count}}</b> موضوع را انتخاب کرده اید."
|
||||
change_tags: "تغییر برچسب ها"
|
||||
choose_new_tags: "انتخاب برچسب های جدید برای این موضوع:"
|
||||
changed_tags: "برچسب های انتخابی برای این موضوع جایگزین شد."
|
||||
none:
|
||||
unread: "موضوع خوانده نشدهای ندارید."
|
||||
new: "شما هیچ موضوع تازهای ندارید"
|
||||
|
@ -1956,6 +1959,8 @@ fa_IR:
|
|||
change_category_settings: "تغییر تنظیمات دسته بندی"
|
||||
delete_category: "حذف دسته بندی"
|
||||
create_category: "ساخت دسته بندی"
|
||||
deleted_tag: "حذف برچسب"
|
||||
renamed_tag: "تغییر نام برچسب"
|
||||
screened_emails:
|
||||
title: "ایمیل ها نمایش داده شده"
|
||||
description: "وقتی کسی سعی می کند یک حساب جدید ایجاد کند، از آدرس ایمیل زیر بررسی و ثبت نام مسدود خواهد شد، و یا برخی از اقدام های دیگر انجام می شود."
|
||||
|
@ -2430,3 +2435,27 @@ fa_IR:
|
|||
</form>
|
||||
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "تمام پرچسب ها"
|
||||
changed: "برچسب های تغییر یافته:"
|
||||
tags: "برچسب ها"
|
||||
choose_for_topic: "انتخاب برچسب اختیاری برای این موضوع"
|
||||
delete_tag: "حذف برچسب"
|
||||
delete_confirm: "آیا مطمئنید که می خواهید این برچسب را خذف کنید?"
|
||||
rename_tag: "تغییر نام برچسب"
|
||||
rename_instructions: "یک نام جدید برای برچسب انتخاب نمایید:"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "تماشا"
|
||||
description: "شما به صورت خودکار تمام نوشته های این برچسب را مشاهده خواهید کرد، تمام نوشته ها به شما اطلاع رسانی خواهد شد.و تعداد نوشته های خوانده نشده در جلوی موضوعات نشان داده خواهد شد."
|
||||
tracking:
|
||||
title: "پی گیری"
|
||||
description: "شما به صورت خودکار تمام نوشته های این برچسب را پی گیری خواهید کرد، تمام نوشته های خوانده نشده جلوی موضوعات نشان داده خواهد شد."
|
||||
regular:
|
||||
title: "منظم"
|
||||
description: "به شما اطلاع داده خواهد شد اگر کسی به شما اششاره@name یا به نوشته شما پاسخ دهند."
|
||||
muted:
|
||||
title: "بی صدا"
|
||||
description: "به شما هیچ چیزی در باره نوشته های این تگ اطلاع رسانی نخواهد شد."
|
||||
|
||||
|
|
|
@ -1019,6 +1019,9 @@ fi:
|
|||
selected:
|
||||
one: "Olet valinnut <b>yhden</b> ketjun."
|
||||
other: "Olet valinnut <b>{{count}}</b> ketjua."
|
||||
change_tags: "Muuta tagit"
|
||||
choose_new_tags: "Valitse uudet tagit näille aiheille:"
|
||||
changed_tags: "Aiheiden tagit on vaihdettu."
|
||||
none:
|
||||
unread: "Sinulla ei ole lukemattomia ketjuja."
|
||||
new: "Sinulla ei ole uusia ketjuja."
|
||||
|
@ -2166,6 +2169,8 @@ fi:
|
|||
grant_moderation: "myönnä valvojan oikeudet"
|
||||
revoke_moderation: "peru valvojan oikeudet"
|
||||
backup_operation: "varmuuskopiointi"
|
||||
deleted_tag: "poistettu tagi"
|
||||
renamed_tag: "uudelleennimetty tagi"
|
||||
screened_emails:
|
||||
title: "Seulottavat sähköpostiosoitteet"
|
||||
description: "Uuden käyttäjätunnuksen luonnin yhteydessä annettua sähköpostiosoitetta verrataan alla olevaan listaan ja tarvittaessa tunnuksen luonti joko estetään tai suoritetaan muita toimenpiteitä."
|
||||
|
@ -2656,3 +2661,27 @@ fi:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Kaikki tagit"
|
||||
changed: "muutetut tagit:"
|
||||
tags: "Tagit"
|
||||
choose_for_topic: "valitse sopivat tagit aiheelle"
|
||||
topics_tagged: "Aiheet tagattu <span class='discourse-tag'>{{tag}}</span>"
|
||||
delete_tag: "Poista tagi"
|
||||
delete_confirm: "Haluatko varmasti poistaa tagin?"
|
||||
rename_tag: "Uudelleennimeä tagi"
|
||||
rename_instructions: "Valitse tagin uusi nimi:"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Tarkkaile"
|
||||
description: "Saat ilmoituksen kaikista uusista viesteistä ja ketjuista jotka käyttää tätä tagia. Uusien ja lukemattomien määrä näkyy ketjun yhteydessä."
|
||||
tracking:
|
||||
title: "Seuraa"
|
||||
description: "Seuraat automaattisesti tämän tagin ketjuja ja viestejä. Uusien ja lukemattomien määrä näkyy ketjun yhteydessä."
|
||||
regular:
|
||||
title: "Tavallinen"
|
||||
description: "Saat ilmoituksen kun joku mainitsee @nimesi tai vastaa ketjuun."
|
||||
muted:
|
||||
title: "Vaimenna"
|
||||
description: "Et saa ilmoituksia, eikä se näy lukemattomissa."
|
||||
|
|
|
@ -1018,6 +1018,9 @@ fr:
|
|||
selected:
|
||||
one: "Vous avez sélectionné <b>1</b> sujet."
|
||||
other: "Vous avez sélectionné <b>{{count}}</b> sujets."
|
||||
change_tags: "Modifier les tags"
|
||||
choose_new_tags: "Choisir de nouveaux tags pour ces sujets :"
|
||||
changed_tags: "Les tags de ces sujets ont été modifiés."
|
||||
none:
|
||||
unread: "Vous n'avez aucun sujet non lu."
|
||||
new: "Vous n'avez aucun nouveau sujet."
|
||||
|
@ -2164,6 +2167,8 @@ fr:
|
|||
grant_moderation: "Accorder les droits de modération"
|
||||
revoke_moderation: "Révoquer les droits de modération"
|
||||
backup_operation: "sauvegarde"
|
||||
deleted_tag: "tag supprimé"
|
||||
renamed_tag: "tag renommé"
|
||||
screened_emails:
|
||||
title: "Courriels affichés"
|
||||
description: "Lorsque quelqu'un essaye de créé un nouveau compte, les adresses de courriel suivantes seront vérifiées et l'inscription sera bloquée, ou une autre action sera réalisée."
|
||||
|
@ -2649,3 +2654,26 @@ fr:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Tous les tags"
|
||||
changed: "tags modifiés:"
|
||||
tags: "Tags"
|
||||
choose_for_topic: "choisissez des tags pour ce sujet (optionnel)"
|
||||
delete_tag: "Supprimer ce tag"
|
||||
delete_confirm: "Êtes-vous sûr de vouloir supprimer ce tag ?"
|
||||
rename_tag: "Renommer ce tag"
|
||||
rename_instructions: "Choisissez un nouveau nom pour le tag :"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Surveiller"
|
||||
description: "Vous surveillerez automatiquement les nouveaux sujets portant ce tag. Vous serez averti de tous les nouveaux messages et sujets. De plus, le nombre de messages non lus et nouveaux apparaîtra en regard du sujet."
|
||||
tracking:
|
||||
title: "Suivre"
|
||||
description: "Vous suivrez automatiquement les nouveaux sujets portant ce tag. Le nombre de messages non lus et nouveaux apparaîtra en regard du sujet."
|
||||
regular:
|
||||
title: "Normal"
|
||||
description: "Vous serez averti si quelqu'un mentionne votre @pseudo ou répond à votre message."
|
||||
muted:
|
||||
title: "Silencieux"
|
||||
description: "Vous ne recevrez aucune notification des sujets portant ce tag, et ils n'apparaîtront pas dans votre onglet non lus."
|
||||
|
|
|
@ -899,6 +899,9 @@ nb_NO:
|
|||
selected:
|
||||
one: "Du har valgt <b>1</b> emne."
|
||||
other: "Du har valgt <b>{{count}}</b> emner."
|
||||
change_tags: "Endre emneord"
|
||||
choose_new_tags: "Velg nye emneord for disse emnene:"
|
||||
changed_tags: "Emneord for disse emnene ble endret."
|
||||
none:
|
||||
unread: "Du har ingen uleste emner å lese."
|
||||
new: "Du har ingen nye emner å lese."
|
||||
|
@ -1950,6 +1953,8 @@ nb_NO:
|
|||
roll_up: "rull opp IP-blokker"
|
||||
delete_category: "slett kategori"
|
||||
create_category: "opprett kategori"
|
||||
deleted_tag: "Slettet emneord"
|
||||
renamed_tag: "Endret emneord"
|
||||
screened_emails:
|
||||
title: "Kontrollerte e-poster"
|
||||
description: "Når noen forsøker å lage en ny konto, vil de følgende e-postadressene bli sjekket, og registreringen vil bli blokkert, eller en annen handling vil bli utført."
|
||||
|
@ -2374,3 +2379,27 @@ nb_NO:
|
|||
name: Annet
|
||||
posting:
|
||||
name: Posting
|
||||
tagging:
|
||||
all_tags: "Alle emneord"
|
||||
changed: "emneord endret:"
|
||||
tags: "Emneord"
|
||||
choose_for_topic: "velg ekstra emneord for dette emnet"
|
||||
delete_tag: "Slett emneord"
|
||||
delete_confirm: "Er du sikker påat du ønsker å slette dette emneordet?"
|
||||
rename_tag: "Endre emneord"
|
||||
rename_instructions: "Velg et nytt navn på dette emneordet:"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Følger"
|
||||
description: "Du vil automatisk følge alle nye emner med dette emneordet. Du vil bli varslet om alle nye innlegg og emner samt at antallet uleste og nye innlegg vil vises sammen med emneoppføringen."
|
||||
tracking:
|
||||
title: "Følger"
|
||||
description: "Du vil automatisk følge alle nye emner med dette emneordet. Antallet uleste og nye innlegg vil vises sammen med emneoppføringen."
|
||||
regular:
|
||||
title: "Vanlig"
|
||||
description: "Du vil bli varslet om noen nevnet ditt @navn eller svarer på din post."
|
||||
muted:
|
||||
title: "Dempet"
|
||||
description: "Du vil ikke bli varslet om noe vedrørende disse emneneordene og de vil ikke vises i din ulest-liste."
|
||||
|
||||
|
|
|
@ -2699,3 +2699,9 @@ pl_PL:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
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>"
|
||||
|
|
|
@ -1052,6 +1052,9 @@ pt:
|
|||
selected:
|
||||
one: "Selecionou <b>1</b> tópico."
|
||||
other: "Selecionou <b>{{count}}</b> tópicos."
|
||||
change_tags: "Alterar etiquetas"
|
||||
choose_new_tags: "Escolher novas etiquetas para estes tópicos:"
|
||||
changed_tags: "As etiquetas desses tópicos foram alteradas."
|
||||
none:
|
||||
unread: "Tem tópicos não lidos."
|
||||
new: "Não tem novos tópicos."
|
||||
|
@ -2208,6 +2211,8 @@ pt:
|
|||
grant_moderation: "conceder moderação"
|
||||
revoke_moderation: "revogar moderação"
|
||||
backup_operation: "operação de cópia de segurança"
|
||||
deleted_tag: "etiqueta apagada"
|
||||
renamed_tag: "etiqueta renomeada"
|
||||
screened_emails:
|
||||
title: "Emails Filtrados"
|
||||
description: "Quando alguém tenta criar uma nova conta, os seguintes endereços de email serão verificados e o registo será bloqueado, ou outra ação será executada."
|
||||
|
@ -2703,3 +2708,25 @@ pt:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Todas as etiquetas"
|
||||
changed: "etiquetas alteradas:"
|
||||
tags: "Etiquetas"
|
||||
choose_for_topic: "escolher etiqetas opcionais para este tópico"
|
||||
delete_tag: "Apagar etiqueta"
|
||||
delete_confirm: "Tem a certeza que quer apagar esta etiqueta?"
|
||||
rename_tag: "Renomear Etiqueta"
|
||||
rename_instructions: "Escolher um novo nome para a etiqueta:"
|
||||
notifications:
|
||||
watching:
|
||||
title: "A vigiar"
|
||||
description: "Irá automaticamente vigiar todos os novos tópicos com esta etiqueta. Será notoficado de todas as novas mensagens e tópicos."
|
||||
tracking:
|
||||
title: "A acompanhar"
|
||||
description: "Irá automaticamente acompanhar todos os novos tópcicos com esta etiqueta"
|
||||
regular:
|
||||
title: "Habitual"
|
||||
description: "Será notoficado se mencionarem o seu @nome ou se alguém lhe responder"
|
||||
muted:
|
||||
title: "Silenciado"
|
||||
description: "Não será notoficado sobre nenhum tópico com esta etiqueta"
|
||||
|
|
|
@ -1049,6 +1049,9 @@ pt_BR:
|
|||
selected:
|
||||
one: "Você selecionou <b>1</b> tópico."
|
||||
other: "Você selecionou <b>{{count}}</b> tópicos."
|
||||
change_tags: "Alterar Tags"
|
||||
choose_new_tags: "Escolha novas tags para estes tópicos:"
|
||||
changed_tags: "As tags desses tópicos foram alteradas."
|
||||
none:
|
||||
unread: "Não há nenhum tópico não lido."
|
||||
new: "Não há tópicos novos."
|
||||
|
@ -2148,6 +2151,8 @@ pt_BR:
|
|||
block_user: "bloquear usuário"
|
||||
unblock_user: "desbloquear usuário"
|
||||
backup_operation: "operação de backup"
|
||||
deleted_tag: "tag removida"
|
||||
renamed_tag: "tag renomeada"
|
||||
screened_emails:
|
||||
title: "Emails Filtrados"
|
||||
description: "Quando alguém tenta cria uma nova conta, os seguintes endereços de email serão verificados e o registro será bloqueado, ou outra ação será executada."
|
||||
|
@ -2628,3 +2633,52 @@ pt_BR:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Todas as Tags"
|
||||
selector_all_tags: "todas as tags"
|
||||
changed: "tags alteradas:"
|
||||
tags: "Tags"
|
||||
choose_for_topic: "escolha tags opcionais para este tópico"
|
||||
delete_tag: "Remover Tag"
|
||||
delete_confirm: "Você tem certeza que quer remover essa tag?"
|
||||
rename_tag: "Renomear Tag"
|
||||
rename_instructions: "Escolha um novo nome para a tag:"
|
||||
sort_by: "Ordenar por:"
|
||||
sort_by_count: "número"
|
||||
sort_by_name: "nome"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Observando"
|
||||
description: "Você vai observar automaticamente todos os novos tópicos dessa tag. Você será notificado de todas as novas mensagens e tópicos, e uma contagem de novas respostas será mostrada ao lado do tópico."
|
||||
tracking:
|
||||
title: "Monitorando"
|
||||
description: "Você vai monitorar automaticamente todos os novos tópicos dessa tag. Uma contagem de novas respostas será mostrada ao lado do tópico."
|
||||
regular:
|
||||
title: "Normal"
|
||||
description: "Você será notificado se alguém mencionar o seu @nome ou responder à sua mensagem."
|
||||
muted:
|
||||
title: "Silenciado"
|
||||
description: "Você nunca será notificado sobre novos tópicos nessa tag, e eles não aparecerão na sua aba não lidas."
|
||||
|
||||
topics:
|
||||
none:
|
||||
unread: "Você não possui tópicos não lidos."
|
||||
new: "Você não possui novos tópicos."
|
||||
read: "Você não leu nenhum tópico ainda."
|
||||
posted: "Você não postou em nenhum tópico ainda."
|
||||
latest: "Não há tópicos mais recentes."
|
||||
hot: "Não há tópicos quentes."
|
||||
bookmarks: "Você não possui tópicos favoritos ainda."
|
||||
top: "Não há melhores tópicos."
|
||||
search: "Nenhum resultado encontrado."
|
||||
bottom:
|
||||
latest: "Não há mais tópicos mais recentes."
|
||||
hot: "Não há mais tópicos quentes."
|
||||
posted: "Não há mais tópicos postados."
|
||||
read: "Não há mais tópicos lidos."
|
||||
new: "Não há mais tópicos novos."
|
||||
unread: "Não há mais tópicos não lidos."
|
||||
top: "Não há mais melhores tópicos."
|
||||
bookmarks: "Não há mais tópicos favoritos."
|
||||
search: "Não há mais resultados de busca."
|
||||
|
|
|
@ -1098,6 +1098,9 @@ ru:
|
|||
few: "Вы выбрали <b>{{count}}</b> темы."
|
||||
many: "Вы выбрали <b>{{count}}</b> тем."
|
||||
other: "Вы выбрали <b>{{count}}</b> тем."
|
||||
change_tags: "Изменить теги"
|
||||
choose_new_tags: "Выберите новые теги для этих тем:"
|
||||
changed_tags: "Теги изменены."
|
||||
none:
|
||||
unread: "У вас нет непрочитанных тем."
|
||||
new: "У вас нет новых тем."
|
||||
|
@ -2303,6 +2306,8 @@ ru:
|
|||
revoke_admin: "отозваны права администратора"
|
||||
grant_moderation: "выданы права модератора"
|
||||
revoke_moderation: "отозваны права модератора"
|
||||
deleted_tag: "удалить тег"
|
||||
renamed_tag: "переименовать тег"
|
||||
screened_emails:
|
||||
title: "Почтовые адреса"
|
||||
description: "Когда кто-то создает новую учетную запись, проверяется данный почтовый адрес и регистрация блокируется или производятся другие дополнительные действия."
|
||||
|
@ -2793,3 +2798,30 @@ ru:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Все теги"
|
||||
selector_all_tags: "все теги"
|
||||
changed: "теги изменены:"
|
||||
tags: "Теги"
|
||||
choose_for_topic: "выберите теги для темы"
|
||||
delete_tag: "Удалить тег"
|
||||
delete_confirm: "Удалить тег?"
|
||||
rename_tag: "Переименовать тег"
|
||||
rename_instructions: "Выберите новое имя для тега:"
|
||||
sort_by: "Сортировать по:"
|
||||
sort_by_count: "количеству"
|
||||
sort_by_name: "имени"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Наблюдать"
|
||||
description: "Уведомлять о каждой новой теме с этим тегом и показывать счетчик новых непрочитанных ответов."
|
||||
tracking:
|
||||
title: "Следить"
|
||||
description: "Количество непрочитанных сообщений появится рядом с названием новых тем с этим тегом."
|
||||
regular:
|
||||
title: "Уведомлять"
|
||||
description: "Вам придет уведомление, только если кто-нибудь упомянет ваш @псевдоним или ответит на ваше сообщение."
|
||||
muted:
|
||||
title: "Без уведомлений"
|
||||
description: "Не уведомлять об изменениях в новых темах с этим тегом и скрыть их из непрочитанных."
|
||||
|
|
|
@ -991,6 +991,9 @@ tr_TR:
|
|||
choose_new_category: "Konular için yeni bir kategori seçin:"
|
||||
selected:
|
||||
other: "<b>{{count}}</b> konu seçtiniz."
|
||||
change_tags: "Etiketleri değiştir"
|
||||
choose_new_tags: "Bu konular için yeni etiket seç:"
|
||||
changed_tags: "Seçtğiniz konular için etiketler değiştirildi."
|
||||
none:
|
||||
unread: "Okunmamış konunuz yok."
|
||||
new: "Yeni konunuz yok."
|
||||
|
@ -2081,6 +2084,8 @@ tr_TR:
|
|||
grant_moderation: "moderasyon yetkisi ver"
|
||||
revoke_moderation: "moderasyon yetkisini kaldır"
|
||||
backup_operation: "yedek operasyonu"
|
||||
deleted_tag: "silinmiş etiket"
|
||||
renamed_tag: "yeniden adlandırılmış etiket"
|
||||
screened_emails:
|
||||
title: "Taranmış E-postalar"
|
||||
description: "Biri yeni bir hesap oluşturmaya çalıştığında, aşağıdaki e-posta adresleri kontrol edilecek ve kayıt önlenecek veya başka bir aksiyon alınacak."
|
||||
|
@ -2557,3 +2562,26 @@ tr_TR:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "Tüm etiketler"
|
||||
changed: "Etiketler değişti:"
|
||||
tags: "Etiketler"
|
||||
choose_for_topic: "Bu konu için opsiyonel olarak bir etiket seçin"
|
||||
delete_tag: "Etiketi sil"
|
||||
delete_confirm: "Bu etiketi kaldırmak istediğinize emin misiniz?"
|
||||
rename_tag: "Etiketi yeniden adlandır"
|
||||
rename_instructions: "Etiket için yeni bir ad girin:"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "Gözleniyor"
|
||||
description: "Bu etikette ki her yeni gönderi için bir bildirim alacaksınız. Okunmamış ve yeni gönderilerin sayısı konunun yanında belirecek."
|
||||
tracking:
|
||||
title: "Takip Ediliyor"
|
||||
description: "Bu etikette ki her yeni gönderi takip edilecek. Okunmamış ve yeni gönderilerin sayısı konunun yanında belirecek."
|
||||
regular:
|
||||
title: "Standart"
|
||||
description: "Biri @isim şeklinde sizden bahsederse ya da gönderinize cevap verirse bildirim alacaksınız."
|
||||
muted:
|
||||
title: "Susturuldu"
|
||||
description: "Bu etiket okunmamışlar sekmenizde belirmeyecek, ve hakkında hiç bir bildirim almayacaksınız."
|
||||
|
|
|
@ -1005,6 +1005,9 @@ zh_CN:
|
|||
choose_new_category: "为主题选择新分类:"
|
||||
selected:
|
||||
other: "你已经选择了 <b>{{count}}</b>个主题"
|
||||
change_tags: "更改标签"
|
||||
choose_new_tags: "为这些主题选择新标签:"
|
||||
changed_tags: "这些主题的标签已被修改。"
|
||||
none:
|
||||
unread: "你没有未读主题。"
|
||||
new: "你没有新主题可读。"
|
||||
|
@ -2100,6 +2103,8 @@ zh_CN:
|
|||
grant_moderation: "授予版主权限"
|
||||
revoke_moderation: "撤销版主权限"
|
||||
backup_operation: "备份操作"
|
||||
deleted_tag: "deleted tag"
|
||||
renamed_tag: "renamed tag"
|
||||
screened_emails:
|
||||
title: "被屏蔽的邮件地址"
|
||||
description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。"
|
||||
|
@ -2578,3 +2583,52 @@ zh_CN:
|
|||
<button class="btn btn-primary">Google</button>
|
||||
</form>
|
||||
</p>
|
||||
tagging:
|
||||
all_tags: "全部标签"
|
||||
selector_all_tags: "所有标签"
|
||||
changed: "标签更改:"
|
||||
tags: "标签"
|
||||
choose_for_topic: "为主题选择可选标签"
|
||||
delete_tag: "删除标签"
|
||||
delete_confirm: "你确定要删除该标签?"
|
||||
rename_tag: "重命名标签"
|
||||
rename_instructions: "为标签选择一个新的名字:"
|
||||
sort_by: "排列顺序:"
|
||||
sort_by_count: "数量"
|
||||
sort_by_name: "名字"
|
||||
|
||||
notifications:
|
||||
watching:
|
||||
title: "关注"
|
||||
description: "你能自动关注该标签下的所有主题。一旦与这个标签有关的新主题和新帖子发表,你都会收到通知。未读贴和新帖子的数量将出现在主题列表中每个主题的标题后。"
|
||||
tracking:
|
||||
title: "追踪"
|
||||
description: "你能自动追踪这个标签下的所有新主题。未读贴和新帖子的数量将出现在主题列表中每个主题的标题后。"
|
||||
regular:
|
||||
title: "常规"
|
||||
description: "当有人@你或者回复你的帖子时,你才会收到通知。"
|
||||
muted:
|
||||
title: "防打扰"
|
||||
description: "你不会收到该标签下的新主题的任何通知,也不会在你的未阅选项卡中显示。"
|
||||
|
||||
topics:
|
||||
none:
|
||||
unread: "你没有未读主题。"
|
||||
new: "你没有近期主题。"
|
||||
read: "你还未阅读任何主题。"
|
||||
posted: "你还未在任何主题中回复。"
|
||||
latest: "没有最新主题。"
|
||||
hot: "没有热门主题。"
|
||||
bookmarks: "你还没有加上书签的主题。"
|
||||
top: "没有热门主题。"
|
||||
search: "搜索无结果。"
|
||||
bottom:
|
||||
latest: "没有更多最新主题了。"
|
||||
hot: "没有更多热门主题了。"
|
||||
posted: "没有更多发表的主题了。"
|
||||
read: "没有更多读过的主题了。"
|
||||
new: "没有更多近期主题了。"
|
||||
unread: "没有更多未读主题了。"
|
||||
top: "没有更多热门主题了。"
|
||||
bookmarks: "没有更多加上书签的主题了。"
|
||||
search: "没有更多搜索结果了。"
|
||||
|
|
|
@ -1101,6 +1101,19 @@ de:
|
|||
default_categories_watching: "Liste der standardmäßig beobachteten Kategorien."
|
||||
default_categories_tracking: "Liste der standardmäßig gefolgten Kategorien."
|
||||
default_categories_muted: "Liste der standardmäßig stummgeschalteten Kategorien."
|
||||
tagging_enabled: "Nutzern erlauben Themen zu taggen?"
|
||||
min_trust_to_create_tag: "Minimale Vertrauensstufe um ein Tag zu erstellen."
|
||||
max_tags_per_topic: "Maximale Anzahl von Tags, die einem Thema zugeordnet werden können."
|
||||
max_tag_length: "Maximale Anzahl von Zeichen, die in einem Tag verwendet werden können."
|
||||
max_tag_search_results: "Maximale Anzahl von Ergebnissen, die bei der Suche nach Tags angezeigt werden."
|
||||
show_filter_by_tag: "Dropdown-Liste zum Filtern von Theman nach Tag anzeigen."
|
||||
max_tags_in_filter_list: "Maximale Anzahl von Tags in der Dropdown-Liste. Die meist genutzten Tags werden angezeigt."
|
||||
tags_sort_alphabetically: "Anzeige der Tags in alphabetischer Reihenfolge. Standard ist die Sortierung nach Beliebtheit."
|
||||
tag_style: "Visuelle Darstellung der Tag-Schildchen."
|
||||
staff_tags: "Eine Liste von Tags, die nur von Mitarbeitern verwendet werden können"
|
||||
min_trust_level_to_tag_topics: "Minimal benötigte Vertrauensstufe zum Taggen von Themen"
|
||||
suppress_overlapping_tags_in_list: "Verstecke Tags in Listen, wenn diese mit dem Titel überlappen"
|
||||
remove_muted_tags_from_latest: "Zeige Themen mit stummgeschalteten Tags nicht in der Liste der aktuellen Themen an."
|
||||
errors:
|
||||
invalid_email: "Ungültige E-Mail-Adresse"
|
||||
invalid_username: "Es gibt keinen Benutzer mit diesem Benutzernamen."
|
||||
|
@ -2048,3 +2061,9 @@ de:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
tags:
|
||||
staff_tag_disallowed: "Das Tag \"%{tag}\" kann nur von Mitarbeitern hinzugefügt werden."
|
||||
staff_tag_remove_disallowed: "Das Tag \"%{tag}\" kann nur von Mitarbeitern entfernt werden."
|
||||
rss_by_tag: "Themen getaggt mit %{tag}"
|
||||
rss_description:
|
||||
tag: "Getaggte Themen"
|
||||
|
|
|
@ -1317,6 +1317,20 @@ en:
|
|||
default_categories_tracking: "List of categories that are tracked by default."
|
||||
default_categories_muted: "List of categories that are muted by default."
|
||||
|
||||
tagging_enabled: "Enable tags on 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."
|
||||
max_tag_search_results: "When searching for tags, the maxium number of results to show."
|
||||
show_filter_by_tag: "Show a dropdown to filter a topic list by tag."
|
||||
max_tags_in_filter_list: "Maximum number of tags to show in the filter dropdown. The most used tags will be shown."
|
||||
tags_sort_alphabetically: "Show tags in alphabetical order. Default is to show in order of popularity."
|
||||
tag_style: "Visual style for tag badges."
|
||||
staff_tags: "A list of tags that can only be applied by staff members"
|
||||
min_trust_level_to_tag_topics: "Minimum trust level required to tag topics"
|
||||
suppress_overlapping_tags_in_list: "Hide tags from list views, if they overlap with title"
|
||||
remove_muted_tags_from_latest: "Don't show topics tagged with muted tags in the latest topic list."
|
||||
|
||||
errors:
|
||||
invalid_email: "Invalid email address."
|
||||
invalid_username: "There's no user with that username."
|
||||
|
@ -3006,3 +3020,10 @@ en:
|
|||
|
||||
topic_invite:
|
||||
user_exists: "Sorry, that user has already been invited. You may only invite a user to a topic once."
|
||||
|
||||
tags:
|
||||
staff_tag_disallowed: "The tag \"%{tag}\" may only be applied by staff."
|
||||
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
|
||||
rss_by_tag: "Topics tagged %{tag}"
|
||||
rss_description:
|
||||
tag: "Tagged topics"
|
||||
|
|
|
@ -1110,6 +1110,19 @@ es:
|
|||
default_categories_watching: "Lista de categorías que están vigiladas por defecto."
|
||||
default_categories_tracking: "Lista de categorías que están seguidas por defecto"
|
||||
default_categories_muted: "Lista de categorías que están silenciadas por defecto."
|
||||
tagging_enabled: "¿Permitir a los usuarios etiquetar temas?"
|
||||
min_trust_to_create_tag: "Mínimo nivel de confianza requerido para crear una etiqueta."
|
||||
max_tags_per_topic: "Número máximo de etiquetas que pueden añadirse a un tema."
|
||||
max_tag_length: "Longitud máxima de caracteres que puede tener el nombre de una etiqueta."
|
||||
max_tag_search_results: "Máximo número de resultados a mostrar al buscar etiquetas."
|
||||
show_filter_by_tag: "Mostrar un desplegable para filtrar la lista de temas por etiqueta."
|
||||
max_tags_in_filter_list: "Máximo número de etiquetas a mostrar en el desplegable. Se mostrarán las más usadas."
|
||||
tags_sort_alphabetically: "Mostrar etiquetas en orden alfabético. Por defecto se muestran por popularidad."
|
||||
tag_style: "Estilo visual de los distintivos de etiqueta."
|
||||
staff_tags: "Una lista de etiquetas que sólo podrá ser aplicada por administradores o moderadores"
|
||||
min_trust_level_to_tag_topics: "Mínimo nivel de confianza requerido para etiquetar temas"
|
||||
suppress_overlapping_tags_in_list: "Ocultar etiquetas de la vista de listado si se solapan con el título"
|
||||
remove_muted_tags_from_latest: "No mostrar temas etiquetados con etiquetas silenciadas en la lista de temas recientes."
|
||||
errors:
|
||||
invalid_email: "Dirección de correo electrónico inválida. "
|
||||
invalid_username: "No existe ningún usuario con ese nombre de usuario. "
|
||||
|
@ -2228,3 +2241,9 @@ es:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
tags:
|
||||
staff_tag_disallowed: "La etiqueta \"%{tag}\" sólo puede ser aplicada por los administradores."
|
||||
staff_tag_remove_disallowed: "La etiqueta \"%{tag}\" sólo puede ser eliminada por los administradores."
|
||||
rss_by_tag: "Temas con la etiqueta %{tag}"
|
||||
rss_description:
|
||||
tag: "Temas etiquetados"
|
||||
|
|
|
@ -913,6 +913,10 @@ fa_IR:
|
|||
emoji_set: "میخواهید ایموجی شما چطور باشد؟"
|
||||
enforce_square_emoji: "تحمیل نسبت ابعاد مربع به تمام شکلک ها emojis . "
|
||||
approve_unless_trust_level: "نوشته ها برای کاربران پایین ت از این سطح اعتماد نیاز به تایید دارد. "
|
||||
tagging_enabled: "کاربران اجازه انتخاب برچسب برای موضوعات داشته باشند?"
|
||||
min_trust_to_create_tag: "حداقل سطح اعتماد مورد نیاز برای انتخاب برچسب؟."
|
||||
max_tags_per_topic: "حداکثر تعداد برچسب انتخابی برای موضوع."
|
||||
max_tag_length: "حداقل کراکتر یک نوشته برای برچسب انتخابی."
|
||||
errors:
|
||||
invalid_email: "آدرس ایمیل نامعتبر"
|
||||
invalid_username: "هیچ کاربری با این نام کاربری وجود ندارد."
|
||||
|
@ -1291,3 +1295,6 @@ fa_IR:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
rss_by_tag: "موضوعات برچسب خورد %{tag}"
|
||||
rss_description:
|
||||
tag: "موضوعات برچسب خورد"
|
||||
|
|
|
@ -1112,6 +1112,10 @@ fi:
|
|||
default_categories_watching: "Lista oletuksena tarkkailtavista alueista."
|
||||
default_categories_tracking: "Lista oletuksena seurattavista alueista."
|
||||
default_categories_muted: "Lista oletuksena vaimennetuista alueista."
|
||||
tagging_enabled: "Salli käyttäjien tagata viestejä?"
|
||||
min_trust_to_create_tag: "Alin luottamustaso tagin luomiseen."
|
||||
max_tags_per_topic: "Tagien maksimiäärä per aihe."
|
||||
max_tag_length: "Tagien merkkien maksimimäärä."
|
||||
errors:
|
||||
invalid_email: "Sähköpostiosoite ei kelpaa."
|
||||
invalid_username: "Tällä nimellä ei löydy käyttäjää."
|
||||
|
@ -2270,3 +2274,6 @@ fi:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
rss_by_tag: "Aiheet tagattu %{tag}"
|
||||
rss_description:
|
||||
tag: "Tagatut aiheet"
|
||||
|
|
|
@ -1094,6 +1094,10 @@ fr:
|
|||
default_categories_watching: "Liste de catégories surveillées par défaut."
|
||||
default_categories_tracking: "Liste de catégories suivies par défaut."
|
||||
default_categories_muted: "Liste de catégories silencées par défaut."
|
||||
tagging_enabled: "Autoriser les utilisateurs à mettre des tags sur les sujets ?"
|
||||
min_trust_to_create_tag: "Le niveau de confiance requis pour créer un tag."
|
||||
max_tags_per_topic: "Le nombre maximum de tags qui peuvent être ajouté à un sujet."
|
||||
max_tag_length: "The nombre maximum de caractères qui peuvent être utilisés pour un tag."
|
||||
errors:
|
||||
invalid_email: "Adresse de courriel invalide."
|
||||
invalid_username: "Il n'y a pas d'utilisateur ayant ce pseudo."
|
||||
|
@ -2193,3 +2197,6 @@ fr:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
rss_by_tag: "Sujets portant le tag %{tag}"
|
||||
rss_description:
|
||||
tag: "Sujet tagués"
|
||||
|
|
|
@ -821,6 +821,10 @@ pl_PL:
|
|||
approve_unless_trust_level: "Posty użytkowników poniżej tego poziomu zaufania muszą być zatwierdzane"
|
||||
default_categories_watching: "Lista kategorii obserwowanych domyślnie."
|
||||
default_categories_tracking: "Lista kategorii śledzonych domyślnie."
|
||||
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."
|
||||
errors:
|
||||
invalid_email: "Nieprawidłowy adres email."
|
||||
invalid_username: "Użytkownik o takiej nazwie nie istnieje."
|
||||
|
@ -1216,3 +1220,6 @@ pl_PL:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
rss_by_tag: "Tematy otagowane jako %{tag}"
|
||||
rss_description:
|
||||
tag: "Otagowane tematy"
|
||||
|
|
|
@ -1002,6 +1002,19 @@ pt_BR:
|
|||
emoji_set: "Como você gostaria do seu emoji?"
|
||||
enforce_square_emoji: "Forçar proporção quadrangular para todos emojis."
|
||||
approve_unless_trust_level: "Mensagens para os usuários abaixo deste nível de confiança devem ser aprovados"
|
||||
tagging_enabled: "Permitir que usuários coloquem tags em tópicos?"
|
||||
min_trust_to_create_tag: "O nível de confiança mínimo necessário para criar uma tag."
|
||||
max_tags_per_topic: "O número máximo de tags que podem ser aplicados a um tópico."
|
||||
max_tag_length: "O número máximo de caracteres que pode ser usado em uma tag."
|
||||
max_tag_search_results: "Quando buscando por tags, o número máximo de resultados a exibir."
|
||||
show_filter_by_tag: "Exibir um dropdown para filtrar uma lista de tópicos por uma tag."
|
||||
max_tags_in_filter_list: "Número máximo de tags no filtro do dropdown. As tags mais utilizadas serão exibidas."
|
||||
tags_sort_alphabetically: "Exibir tags em ordem alfabetica. O padrão é exibí-las em ordem de popularidade."
|
||||
tag_style: "Estilo visual para as insígnias de tag."
|
||||
staff_tags: "Uma lista de tags que só podem ser aplicadas por membros da moderação"
|
||||
min_trust_level_to_tag_topics: "Nível de confiança mínimo necessário para aplicar uma tag"
|
||||
suppress_overlapping_tags_in_list: "Esconder tags das visualizações de lista, se elas se sobreporem ao título"
|
||||
remove_muted_tags_from_latest: "Não mostrar tópicos com tags silenciadas na lista de tópicos recentes."
|
||||
errors:
|
||||
invalid_email: "Endereço de email inválido"
|
||||
invalid_username: "Não há nenhum usuário com esse nome de usuário."
|
||||
|
@ -1516,3 +1529,9 @@ pt_BR:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
tags:
|
||||
staff_tag_disallowed: "A tag \"%{tag}\" só pode ser aplicada pela moderação."
|
||||
staff_tag_remove_disallowed: "A tag \"%{tag}\" só pode ser removida pela moderação."
|
||||
rss_by_tag: "Tópicos com a tag %{tag}"
|
||||
rss_description:
|
||||
tag: "Tópicos com tag"
|
||||
|
|
|
@ -984,6 +984,10 @@ tr_TR:
|
|||
default_categories_watching: "Öntanımlı olarak, izlenen kategorilerin listesi."
|
||||
default_categories_tracking: "Öntanımlı olarak, takip edilen kategorilerin listesi."
|
||||
default_categories_muted: "Öntanımlı olarak, sesi kısılan kategorilerin listesi."
|
||||
tagging_enabled: "Kullanıcılar konularına etiket ekleyebilsinler mi?"
|
||||
min_trust_to_create_tag: "Etiket oluşturmak için gereken minumum güven seviyesi."
|
||||
max_tags_per_topic: "Bir konu en fazla kaç adet etiket eklenebilir."
|
||||
max_tag_length: "Bir etiket en fazla kaç karakterde oluşabilir."
|
||||
errors:
|
||||
invalid_email: "Geçersiz e-posta adresi."
|
||||
invalid_username: "Bu kullanıcı adı ile bir kullanıcı bulunmuyor."
|
||||
|
@ -1452,3 +1456,6 @@ tr_TR:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
rss_by_tag: "Konu %{tag} ile etiketlenmiştir."
|
||||
rss_description:
|
||||
tag: "Etiketlenmiş konular"
|
||||
|
|
|
@ -1079,6 +1079,18 @@ zh_CN:
|
|||
default_categories_watching: "分类列表默认跟踪。"
|
||||
default_categories_tracking: "分类列表默认追踪。"
|
||||
default_categories_muted: "分类列表默认不显示。"
|
||||
tagging_enabled: "允许用户为主题设置标签?"
|
||||
min_trust_to_create_tag: "允许创建标签的最小信任等级。"
|
||||
max_tags_per_topic: "一个主题最多允许有多少个标签。"
|
||||
max_tag_length: "一个标签允许的最大字符数。"
|
||||
max_tag_search_results: "当搜索标签时,显示的最多几个结果。"
|
||||
show_filter_by_tag: "显示一个下拉菜单按照标签过滤主题列表。"
|
||||
max_tags_in_filter_list: "过滤下拉菜单中显示的最大标签数。最常用的标签将优先显示。"
|
||||
tags_sort_alphabetically: "按照字母顺序显示标签。默认显示顺序是流行度。"
|
||||
tag_style: "标签的视觉样式。"
|
||||
staff_tags: "只可由志愿设置的标签列表"
|
||||
min_trust_level_to_tag_topics: "给主题加标签的最小信任等级"
|
||||
suppress_overlapping_tags_in_list: "如果在列表视图中标签覆盖了主题,则隐藏标签"
|
||||
errors:
|
||||
invalid_email: "电子邮箱地址无效。"
|
||||
invalid_username: "没有这个用户名的用户。"
|
||||
|
@ -2452,3 +2464,9 @@ zh_CN:
|
|||
activemodel:
|
||||
errors:
|
||||
<<: *errors
|
||||
tags:
|
||||
staff_tag_disallowed: "标签\"%{tag}\"只可以由职员标记。"
|
||||
staff_tag_remove_disallowed: "标签\"%{tag}\"只可以由职员删除。"
|
||||
rss_by_tag: "%{tag}标签的主题"
|
||||
rss_description:
|
||||
tag: "加标签的主题"
|
||||
|
|
|
@ -606,6 +606,29 @@ Discourse::Application.routes.draw do
|
|||
get "manifest.json" => "metadata#manifest", as: :manifest
|
||||
get "opensearch" => "metadata#opensearch", format: :xml
|
||||
|
||||
scope "/tags" do
|
||||
get '/' => 'tags#index'
|
||||
get '/filter/list' => 'tags#index'
|
||||
get '/filter/search' => 'tags#search'
|
||||
get '/check' => 'tags#check_hashtag'
|
||||
constraints(tag_id: /[^\/]+?/, format: /json|rss/) do
|
||||
get '/:tag_id.rss' => 'tags#tag_feed'
|
||||
get '/:tag_id' => 'tags#show', as: 'list_by_tag'
|
||||
get '/c/:category/:tag_id' => 'tags#show'
|
||||
get '/c/:parent_category/:category/:tag_id' => 'tags#show'
|
||||
get '/:tag_id/notifications' => 'tags#notifications'
|
||||
put '/:tag_id/notifications' => 'tags#update_notifications'
|
||||
put '/:tag_id' => 'tags#update'
|
||||
delete '/:tag_id' => 'tags#destroy'
|
||||
|
||||
Discourse.filters.each do |filter|
|
||||
get "/:tag_id/l/#{filter}" => "tags#show_#{filter}"
|
||||
get "/c/:category/:tag_id/l/#{filter}" => "tags#show_#{filter}"
|
||||
get "/c/:parent_category/:category/:tag_id/l/#{filter}" => "tags#show_#{filter}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Discourse.filters.each do |filter|
|
||||
root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}"
|
||||
end
|
||||
|
|
|
@ -1157,3 +1157,57 @@ user_preferences:
|
|||
default_categories_muted:
|
||||
type: category_list
|
||||
default: ''
|
||||
|
||||
tags:
|
||||
tagging_enabled:
|
||||
client: true
|
||||
default: false
|
||||
refresh: true
|
||||
tag_style:
|
||||
client: true
|
||||
type: enum
|
||||
default: 'simple'
|
||||
choices:
|
||||
- simple
|
||||
- bullet
|
||||
- box
|
||||
preview: '<div class="discourse-tags"><span class="discourse-tag {{value}}">tag1</span><span class="discourse-tag {{value}}">tag2</span></div>'
|
||||
max_tags_per_topic:
|
||||
default: 5
|
||||
client: true
|
||||
max_tag_length:
|
||||
default: 20
|
||||
client: true
|
||||
min_trust_to_create_tag:
|
||||
default: 3
|
||||
enum: 'TrustLevelSetting'
|
||||
min_trust_level_to_tag_topics:
|
||||
default: 0
|
||||
enum: 'TrustLevelSetting'
|
||||
client: true
|
||||
max_tag_search_results:
|
||||
client: true
|
||||
default: 5
|
||||
min: 1
|
||||
show_filter_by_tag:
|
||||
client: true
|
||||
default: false
|
||||
refresh: true
|
||||
max_tags_in_filter_list:
|
||||
default: 30
|
||||
min: 1
|
||||
refresh: true
|
||||
tags_sort_alphabetically:
|
||||
client: true
|
||||
default: false
|
||||
refresh: true
|
||||
staff_tags:
|
||||
type: list
|
||||
client: true
|
||||
default: ''
|
||||
suppress_overlapping_tags_in_list:
|
||||
default: false
|
||||
client: true
|
||||
remove_muted_tags_from_latest:
|
||||
default: false
|
||||
|
||||
|
|
116
lib/discourse_tagging.rb
Normal file
116
lib/discourse_tagging.rb
Normal file
|
@ -0,0 +1,116 @@
|
|||
module DiscourseTagging
|
||||
|
||||
TAGS_FIELD_NAME = "tags"
|
||||
TAGS_FILTER_REGEXP = /[<\\\/\>\#\?\&\s]/
|
||||
|
||||
# class Engine < ::Rails::Engine
|
||||
# engine_name "discourse_tagging"
|
||||
# isolate_namespace DiscourseTagging
|
||||
# end
|
||||
|
||||
def self.clean_tag(tag)
|
||||
tag.downcase.strip[0...SiteSetting.max_tag_length].gsub(TAGS_FILTER_REGEXP, '')
|
||||
end
|
||||
|
||||
def self.staff_only_tags(tags)
|
||||
return nil if tags.nil?
|
||||
|
||||
staff_tags = SiteSetting.staff_tags.split("|")
|
||||
|
||||
tag_diff = tags - staff_tags
|
||||
tag_diff = tags - tag_diff
|
||||
|
||||
tag_diff.present? ? tag_diff : nil
|
||||
end
|
||||
|
||||
def self.tags_for_saving(tags, guardian)
|
||||
|
||||
return [] unless guardian.can_tag_topics?
|
||||
|
||||
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
|
||||
# TODO: this is doing a full count, it should just check first or use a cache
|
||||
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)
|
||||
# This insert will run up to SiteSetting.max_tags_per_topic times
|
||||
tags.each do |tag|
|
||||
key_name_sql = ActiveRecord::Base.sql_fragment("('#{notification_key(tag)}')", tag)
|
||||
|
||||
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_name_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
|
||||
|
||||
def self.rename_tag(current_user, old_id, new_id)
|
||||
sql = <<-SQL
|
||||
UPDATE topic_custom_fields AS tcf
|
||||
SET value = :new_id
|
||||
WHERE value = :old_id
|
||||
AND name = :tags_field_name
|
||||
AND NOT EXISTS(SELECT 1
|
||||
FROM topic_custom_fields
|
||||
WHERE value = :new_id AND name = :tags_field_name AND topic_id = tcf.topic_id)
|
||||
SQL
|
||||
|
||||
user_sql = <<-SQL
|
||||
UPDATE user_custom_fields
|
||||
SET name = :new_user_tag_id
|
||||
WHERE name = :old_user_tag_id
|
||||
AND NOT EXISTS(SELECT 1
|
||||
FROM user_custom_fields
|
||||
WHERE name = :new_user_tag_id)
|
||||
SQL
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.exec_sql(sql, new_id: new_id, old_id: old_id, tags_field_name: TAGS_FIELD_NAME)
|
||||
TopicCustomField.delete_all(name: TAGS_FIELD_NAME, value: old_id)
|
||||
ActiveRecord::Base.exec_sql(user_sql, new_user_tag_id: notification_key(new_id),
|
||||
old_user_tag_id: notification_key(old_id))
|
||||
UserCustomField.delete_all(name: notification_key(old_id))
|
||||
StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: old_id, new_value: new_id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.top_tags(limit_arg=nil)
|
||||
# TODO: cache
|
||||
# TODO: need an index for this (name,value)
|
||||
TopicCustomField.where(name: TAGS_FIELD_NAME)
|
||||
.group(:value)
|
||||
.limit(limit_arg || SiteSetting.max_tags_in_filter_list)
|
||||
.order('COUNT(value) DESC')
|
||||
.count
|
||||
.map {|name, count| name}
|
||||
end
|
||||
|
||||
def self.muted_tags(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
|
||||
end
|
||||
end
|
|
@ -276,6 +276,18 @@ class Guardian
|
|||
UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0
|
||||
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
|
||||
|
||||
def is_my_own?(obj)
|
||||
|
|
|
@ -30,6 +30,10 @@ class Plugin::Instance
|
|||
[].tap { |plugins|
|
||||
# also follows symlinks - http://stackoverflow.com/q/357754
|
||||
Dir["#{parent_path}/**/*/**/plugin.rb"].sort.each do |path|
|
||||
|
||||
# tagging is included in core, so don't load it
|
||||
next if path =~ /discourse-tagging/
|
||||
|
||||
source = File.read(path)
|
||||
metadata = Plugin::Metadata.parse(source)
|
||||
plugins << self.new(metadata, path)
|
||||
|
|
|
@ -147,6 +147,7 @@ class PostCreator
|
|||
track_latest_on_category
|
||||
enqueue_jobs
|
||||
BadgeGranter.queue_badge_grant(Badge::Trigger::PostRevision, post: @post)
|
||||
auto_notify_for_tags
|
||||
|
||||
trigger_after_events(@post)
|
||||
|
||||
|
@ -437,6 +438,15 @@ class PostCreator
|
|||
PostJobsEnqueuer.new(@post, @topic, new_topic?, {import_mode: @opts[:import_mode]}).enqueue_jobs
|
||||
end
|
||||
|
||||
def auto_notify_for_tags
|
||||
tags = DiscourseTagging.tags_for_saving(@opts[:tags], @guardian)
|
||||
if tags.present?
|
||||
@topic.custom_fields.update(DiscourseTagging::TAGS_FIELD_NAME => tags)
|
||||
@topic.save
|
||||
DiscourseTagging.auto_notify_for(tags, @topic)
|
||||
end
|
||||
end
|
||||
|
||||
def new_topic?
|
||||
@opts[:topic_id].blank?
|
||||
end
|
||||
|
|
|
@ -72,6 +72,54 @@ class PostRevisor
|
|||
tc.check_result(tc.topic.change_category_to_id(category_id))
|
||||
end
|
||||
|
||||
track_topic_field(:tags_empty_array) do |tc, val|
|
||||
if val.present?
|
||||
unless tc.guardian.is_staff?
|
||||
old_tags = tc.topic.tags || []
|
||||
staff_tags = DiscourseTagging.staff_only_tags(old_tags)
|
||||
if staff_tags.present?
|
||||
tc.topic.errors[:base] << I18n.t("tags.staff_tag_remove_disallowed", tag: staff_tags.join(" "))
|
||||
tc.check_result(false)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
tc.record_change(DiscourseTagging::TAGS_FIELD_NAME, tc.topic.custom_fields[DiscourseTagging::TAGS_FIELD_NAME], nil)
|
||||
tc.topic.custom_fields.delete(DiscourseTagging::TAGS_FIELD_NAME)
|
||||
end
|
||||
end
|
||||
|
||||
track_topic_field(:tags) do |tc, tags|
|
||||
if tags.present? && tc.guardian.can_tag_topics?
|
||||
tags = DiscourseTagging.tags_for_saving(tags, tc.guardian)
|
||||
old_tags = tc.topic.tags || []
|
||||
|
||||
new_tags = tags - old_tags
|
||||
removed_tags = old_tags - tags
|
||||
|
||||
unless tc.guardian.is_staff?
|
||||
staff_tags = DiscourseTagging.staff_only_tags(new_tags)
|
||||
if staff_tags.present?
|
||||
tc.topic.errors[:base] << I18n.t("tags.staff_tag_disallowed", tag: staff_tags.join(" "))
|
||||
tc.check_result(false)
|
||||
next
|
||||
end
|
||||
|
||||
staff_tags = DiscourseTagging.staff_only_tags(removed_tags)
|
||||
if staff_tags.present?
|
||||
tc.topic.errors[:base] << I18n.t("tags.staff_tag_remove_disallowed", tag: staff_tags.join(" "))
|
||||
tc.check_result(false)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
tc.record_change(DiscourseTagging::TAGS_FIELD_NAME, tc.topic.custom_fields[DiscourseTagging::TAGS_FIELD_NAME], tags)
|
||||
tc.topic.custom_fields.update(DiscourseTagging::TAGS_FIELD_NAME => tags)
|
||||
|
||||
DiscourseTagging.auto_notify_for(new_tags, tc.topic) if new_tags.present?
|
||||
end
|
||||
end
|
||||
|
||||
# AVAILABLE OPTIONS:
|
||||
# - revised_at: changes the date of the revision
|
||||
# - force_new_version: bypass ninja-edit window
|
||||
|
|
|
@ -67,6 +67,23 @@ module PrettyText
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
def category_tag_hashtag_lookup(text)
|
||||
tag_postfix = '::tag'
|
||||
is_tag = text =~ /#{tag_postfix}$/
|
||||
|
||||
if !is_tag && category = Category.query_from_hashtag_slug(text)
|
||||
[category.url_with_id, text]
|
||||
elsif is_tag && tag = TopicCustomField.find_by(name: TAGS_FIELD_NAME, value: text.gsub!("#{tag_postfix}", ''))
|
||||
["#{Discourse.base_url}/tags/#{tag.value}", text]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
DiscourseEvent.on(:markdown_context) do |context|
|
||||
context.eval('opts["categoryHashtagLookup"] = function(c){return helpers.category_tag_hashtag_lookup(c);}')
|
||||
end
|
||||
end
|
||||
|
||||
@mutex = Mutex.new
|
||||
|
|
|
@ -313,6 +313,17 @@ class Search
|
|||
end
|
||||
end
|
||||
|
||||
advanced_filter(/tags?:([a-zA-Z0-9,\-_]+)/) do |posts, match|
|
||||
tags = match.split(",")
|
||||
|
||||
posts.where("topics.id IN (
|
||||
SELECT tc.topic_id
|
||||
FROM topic_custom_fields tc
|
||||
WHERE tc.name = '#{DiscourseTagging::TAGS_FIELD_NAME}' AND
|
||||
tc.value in (?)
|
||||
)", tags)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
|
|
|
@ -23,6 +23,12 @@ class TopicCreator
|
|||
# so we fire the validation event after
|
||||
# this allows us to add errors
|
||||
valid = topic.valid?
|
||||
|
||||
# not sure where this should go
|
||||
if !@guardian.is_staff? && staff_only = DiscourseTagging.staff_only_tags(@opts[:tags])
|
||||
topic.errors[:base] << I18n.t("tags.staff_tag_disallowed", tag: staff_only.join(" "))
|
||||
end
|
||||
|
||||
DiscourseEvent.trigger(:after_validate_topic, topic, self)
|
||||
valid &&= topic.errors.empty?
|
||||
|
||||
|
|
|
@ -308,6 +308,7 @@ class TopicQuery
|
|||
result = default_results(options)
|
||||
result = remove_muted_topics(result, @user) unless options && options[:state] == "muted".freeze
|
||||
result = remove_muted_categories(result, @user, exclude: options[:category])
|
||||
result = remove_muted_tags(result, @user, options)
|
||||
|
||||
# plugins can remove topics here:
|
||||
self.class.results_filter_callbacks.each do |filter_callback|
|
||||
|
@ -334,6 +335,7 @@ class TopicQuery
|
|||
result = TopicQuery.new_filter(default_results(options.reverse_merge(:unordered => true)), @user.user_option.treat_as_new_topic_start_date)
|
||||
result = remove_muted_topics(result, @user)
|
||||
result = remove_muted_categories(result, @user, exclude: options[:category])
|
||||
result = remove_muted_tags(result, @user, options)
|
||||
|
||||
self.class.results_filter_callbacks.each do |filter_callback|
|
||||
result = filter_callback.call(:new, result, @user, options)
|
||||
|
@ -562,6 +564,36 @@ class TopicQuery
|
|||
|
||||
list
|
||||
end
|
||||
def remove_muted_tags(list, user, opts=nil)
|
||||
if user.nil? || !SiteSetting.tagging_enabled || !SiteSetting.remove_muted_tags_from_latest
|
||||
list
|
||||
else
|
||||
muted_tags = DiscourseTagging.muted_tags(user)
|
||||
if muted_tags.empty?
|
||||
list
|
||||
else
|
||||
showing_tag = if opts[:filter]
|
||||
f = opts[:filter].split('/')
|
||||
f[0] == 'tags' ? f[1] : nil
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
if muted_tags.include?(showing_tag)
|
||||
list # if viewing the topic list for a muted tag, show all the topics
|
||||
else
|
||||
arr = muted_tags.map{ |z| "'#{z}'" }.join(',')
|
||||
list.where("EXISTS (
|
||||
SELECT 1
|
||||
FROM topic_custom_fields tcf
|
||||
WHERE tcf.name = 'tags'
|
||||
AND tcf.value NOT IN (#{arr})
|
||||
AND tcf.topic_id = topics.id
|
||||
) OR NOT EXISTS (select 1 from topic_custom_fields tcf where tcf.name = 'tags' and tcf.topic_id = topics.id)")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new_messages(params)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ class TopicsBulkAction
|
|||
def self.operations
|
||||
@operations ||= %w(change_category close archive change_notification_level
|
||||
reset_read dismiss_posts delete unlist archive_messages
|
||||
move_messages_to_inbox)
|
||||
move_messages_to_inbox change_tags)
|
||||
end
|
||||
|
||||
def self.register_operation(name, &block)
|
||||
|
@ -130,6 +130,23 @@ class TopicsBulkAction
|
|||
end
|
||||
end
|
||||
|
||||
def change_tags
|
||||
tags = @operation[:tags]
|
||||
tags = DiscourseTagging.tags_for_saving(tags, guardian) if tags.present?
|
||||
|
||||
topics.each do |t|
|
||||
if guardian.can_edit?(t)
|
||||
if tags.present?
|
||||
t.custom_fields.update(DiscourseTagging::TAGS_FIELD_NAME => tags)
|
||||
t.save
|
||||
DiscourseTagging.auto_notify_for(tags, t)
|
||||
else
|
||||
t.custom_fields.delete(DiscourseTagging::TAGS_FIELD_NAME)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def guardian
|
||||
@guardian ||= Guardian.new(@user)
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue