Initial commit as a clone of discourse-tagging
This commit is contained in:
commit
174e6d6ecc
|
@ -0,0 +1,7 @@
|
||||||
|
.bundle/
|
||||||
|
log/*.log
|
||||||
|
pkg/
|
||||||
|
auto_generated
|
||||||
|
Gemfile.lock
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"predef":["Ember",
|
||||||
|
"jQuery",
|
||||||
|
"$",
|
||||||
|
"RSVP",
|
||||||
|
"Discourse",
|
||||||
|
"PreloadStore",
|
||||||
|
"Handlebars",
|
||||||
|
"I18n",
|
||||||
|
"bootbox",
|
||||||
|
"module",
|
||||||
|
"moduleFor",
|
||||||
|
"moduleForComponent",
|
||||||
|
"Pretender",
|
||||||
|
"sandbox",
|
||||||
|
"integration",
|
||||||
|
"controllerFor",
|
||||||
|
"test",
|
||||||
|
"ok",
|
||||||
|
"not",
|
||||||
|
"expect",
|
||||||
|
"equal",
|
||||||
|
"blank",
|
||||||
|
"present",
|
||||||
|
"visit",
|
||||||
|
"andThen",
|
||||||
|
"click",
|
||||||
|
"currentPath",
|
||||||
|
"currentRouteName",
|
||||||
|
"currentURL",
|
||||||
|
"fillIn",
|
||||||
|
"keyEvent",
|
||||||
|
"triggerEvent",
|
||||||
|
"count",
|
||||||
|
"exists",
|
||||||
|
"asyncTestDiscourse",
|
||||||
|
"fixture",
|
||||||
|
"find",
|
||||||
|
"sinon",
|
||||||
|
"moment",
|
||||||
|
"start",
|
||||||
|
"_",
|
||||||
|
"alert",
|
||||||
|
"containsInstance",
|
||||||
|
"parseHTML",
|
||||||
|
"deepEqual",
|
||||||
|
"notEqual",
|
||||||
|
"require",
|
||||||
|
"requirejs",
|
||||||
|
"hasModule",
|
||||||
|
"Blob",
|
||||||
|
"File"],
|
||||||
|
"node" : false,
|
||||||
|
"browser" : true,
|
||||||
|
"boss" : true,
|
||||||
|
"curly": false,
|
||||||
|
"debug": false,
|
||||||
|
"devel": false,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"evil": true,
|
||||||
|
"forin": false,
|
||||||
|
"immed": false,
|
||||||
|
"laxbreak": false,
|
||||||
|
"newcap": true,
|
||||||
|
"noarg": true,
|
||||||
|
"noempty": false,
|
||||||
|
"nonew": false,
|
||||||
|
"nomen": false,
|
||||||
|
"onevar": false,
|
||||||
|
"plusplus": false,
|
||||||
|
"regexp": false,
|
||||||
|
"undef": true,
|
||||||
|
"unused": true,
|
||||||
|
"sub": true,
|
||||||
|
"strict": false,
|
||||||
|
"white": false,
|
||||||
|
"eqnull": true,
|
||||||
|
"quotmark": false,
|
||||||
|
"lastsemic": true,
|
||||||
|
"esnext": true
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Civilized Discourse Construction Kit
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,17 @@
|
||||||
|
### discourse-data-explorer
|
||||||
|
|
||||||
|
This plugin allows admins to run SQL queries against the live Discourse database,
|
||||||
|
including parameterized queries and formatting for several common column types.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Follow our [Install a Plugin](https://meta.discourse.org/t/install-a-plugin/19157)
|
||||||
|
howto, using `git clone https://github.com/discourse/discourse-data-explorer.git`
|
||||||
|
as the plugin command.
|
||||||
|
|
||||||
|
Once you've installed it, review the settings under admin and then enable
|
||||||
|
`data_explorer_enabled`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{#if canEditTags}}
|
||||||
|
{{tag-chooser tags=model.tags tabIndex="4"}}
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{#if canEditTags}}
|
||||||
|
{{tag-chooser tags=buffered.tags}}
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{#if tags_changes}}
|
||||||
|
<div class='row'>
|
||||||
|
{{i18n "tagging.changed"}}
|
||||||
|
{{#each t in previousTagChanges}}
|
||||||
|
{{discourse-tag tagId=t}}
|
||||||
|
{{/each}}
|
||||||
|
→
|
||||||
|
|
||||||
|
{{#each t in currentTagChanges}}
|
||||||
|
{{discourse-tag tagId=t}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<li>
|
||||||
|
{{#link-to 'tags'}}{{i18n "tagging.tags"}}{{/link-to}}
|
||||||
|
</li>
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{#each t in topic.tags}}
|
||||||
|
{{discourse-tag tagId=t}}
|
||||||
|
{{/each}}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import RESTAdapter from 'discourse/adapters/rest';
|
||||||
|
|
||||||
|
export default RESTAdapter.extend({
|
||||||
|
pathFor(type, id) {
|
||||||
|
return "/tags/" + id + "/notifications";
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
tagName: 'a',
|
||||||
|
classNameBindings: [':discourse-tag'],
|
||||||
|
attributeBindings: ['href', 'style'],
|
||||||
|
|
||||||
|
href: function() {
|
||||||
|
return "/tags/" + this.get('tagId');
|
||||||
|
}.property('tagId'),
|
||||||
|
|
||||||
|
style: function() {
|
||||||
|
const count = parseFloat(this.get('count')),
|
||||||
|
minCount = parseFloat(this.get('minCount')),
|
||||||
|
maxCount = parseFloat(this.get('maxCount'));
|
||||||
|
|
||||||
|
if (count && maxCount && minCount) {
|
||||||
|
let ratio = (count - minCount) / maxCount;
|
||||||
|
if (ratio) {
|
||||||
|
ratio = ratio + 1.0;
|
||||||
|
return "font-size: " + ratio + "em";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.property('count', 'scaleTo'),
|
||||||
|
|
||||||
|
render(buffer) {
|
||||||
|
buffer.push(Handlebars.Utils.escapeExpression(this.get('tagId')));
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Discourse.URL.routeTo(this.get('href'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,88 @@
|
||||||
|
function formatTag(t) {
|
||||||
|
const ret = "<a href class='discourse-tag'>" + Handlebars.Utils.escapeExpression(t.id) + "</a>";
|
||||||
|
return (t.count) ? ret + " <span class='discourse-tag-count'>x" + t.count + "</span>" : ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ember.TextField.extend({
|
||||||
|
classNameBindings: [':tag-chooser'],
|
||||||
|
attributeBindings: ['tabIndex'],
|
||||||
|
|
||||||
|
_setupTags: function() {
|
||||||
|
const tags = this.get('tags') || [];
|
||||||
|
this.set('value', tags.join(", "));
|
||||||
|
}.on('init'),
|
||||||
|
|
||||||
|
_valueChanged: function() {
|
||||||
|
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||||
|
this.set('tags', tags);
|
||||||
|
}.observes('value'),
|
||||||
|
|
||||||
|
_initializeTags: function() {
|
||||||
|
const site = this.site,
|
||||||
|
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||||
|
|
||||||
|
this.$().select2({
|
||||||
|
tags: true,
|
||||||
|
placeholder: I18n.t('tagging.choose_for_topic'),
|
||||||
|
maximumInputLength: this.siteSettings.max_tag_length,
|
||||||
|
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
|
||||||
|
initSelection(element, callback) {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
function splitVal(string, separator) {
|
||||||
|
var val, i, l;
|
||||||
|
if (string === null || string.length < 1) return [];
|
||||||
|
val = string.split(separator);
|
||||||
|
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(splitVal(element.val(), ",")).each(function () {
|
||||||
|
data.push({
|
||||||
|
id: this,
|
||||||
|
text: this
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
},
|
||||||
|
createSearchChoice: function(term, data) {
|
||||||
|
term = term.replace(filterRegexp, '').trim();
|
||||||
|
|
||||||
|
// No empty terms, make sure the user has permission to create the tag
|
||||||
|
if (!term.length || !site.get('can_create_tag')) { return; }
|
||||||
|
|
||||||
|
if ($(data).filter(function() {
|
||||||
|
return this.text.localeCompare(term) === 0;
|
||||||
|
}).length === 0) {
|
||||||
|
return { id: term, text: term };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createSearchChoicePosition: function(list, item) {
|
||||||
|
// Search term goes on the bottom
|
||||||
|
list.push(item);
|
||||||
|
},
|
||||||
|
formatSelectionCssClass: function () { return "discourse-tag"; },
|
||||||
|
formatResult: formatTag,
|
||||||
|
// formatSelection: formatTag,
|
||||||
|
multiple: true,
|
||||||
|
ajax: {
|
||||||
|
quietMillis: 200,
|
||||||
|
cache: true,
|
||||||
|
url: "/tags/filter/search",
|
||||||
|
dataType: 'json',
|
||||||
|
data: function (term) {
|
||||||
|
return { q: term };
|
||||||
|
},
|
||||||
|
results: function (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}.on('didInsertElement'),
|
||||||
|
|
||||||
|
_destroyTags: function() {
|
||||||
|
this.$().select2('destroy');
|
||||||
|
}.on('willDestroyElement')
|
||||||
|
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
tag: null,
|
||||||
|
list: null,
|
||||||
|
|
||||||
|
loadMoreTopics() {
|
||||||
|
return this.get('list').loadMore();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
changeTagNotification(id) {
|
||||||
|
const tagNotification = this.get('tagNotification');
|
||||||
|
tagNotification.update({ notification_level: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
import ComposerController from 'discourse/controllers/composer';
|
||||||
|
import HistoryController from 'discourse/controllers/history';
|
||||||
|
import TopicController from 'discourse/controllers/topic';
|
||||||
|
import { needsSecondRowIf } from 'discourse/components/header-extra-info';
|
||||||
|
|
||||||
|
// Work around a quirk of custom fields -- an array of one element
|
||||||
|
// is returned as just that element. We should fix this properly
|
||||||
|
// in custom fields and remove this.
|
||||||
|
function customTagArray(fieldName) {
|
||||||
|
return function() {
|
||||||
|
var val = this.get(fieldName);
|
||||||
|
if (!val) { return val; }
|
||||||
|
if (!Array.isArray(val)) { val = [val]; }
|
||||||
|
return val;
|
||||||
|
}.property(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'extend-for-tagging',
|
||||||
|
initialize() {
|
||||||
|
Discourse.Composer.serializeOnCreate('tags');
|
||||||
|
Discourse.Composer.serializeToTopic('tags', 'topic.tags');
|
||||||
|
|
||||||
|
TopicController.reopen({
|
||||||
|
canEditTags: Ember.computed.not('isPrivateMessage')
|
||||||
|
});
|
||||||
|
|
||||||
|
HistoryController.reopen({
|
||||||
|
previousTagChanges: customTagArray('tags_changes.previous'),
|
||||||
|
currentTagChanges: customTagArray('tags_changes.current')
|
||||||
|
});
|
||||||
|
|
||||||
|
ComposerController.reopen({
|
||||||
|
canEditTags: function() {
|
||||||
|
return !this.site.mobileView &&
|
||||||
|
this.get('model.canEditTitle') &&
|
||||||
|
!this.get('model.creatingPrivateMessage');
|
||||||
|
}.property('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show a second row in the header if there are any tags on the topic
|
||||||
|
needsSecondRowIf('topic.tags.length', tagsLength => parseInt(tagsLength) > 0);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return Discourse.ajax("/tags/filter/cloud.json");
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
|
||||||
|
model(tag) {
|
||||||
|
tag.tag_id = Handlebars.Utils.escapeExpression(tag.tag_id);
|
||||||
|
|
||||||
|
if (this.get('currentUser')) {
|
||||||
|
// If logged in, we should get the tag's user settings
|
||||||
|
const self = this;
|
||||||
|
return this.store.find('tagNotification', tag.tag_id).then(function(tn) {
|
||||||
|
self.set('tagNotification', tn);
|
||||||
|
return tag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
},
|
||||||
|
|
||||||
|
afterModel(tag) {
|
||||||
|
const self = this;
|
||||||
|
return Discourse.TopicList.list('tags/' + tag.tag_id).then(function(list) {
|
||||||
|
self.set('list', list);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
controller.setProperties({
|
||||||
|
tag: model,
|
||||||
|
list: this.get('list'),
|
||||||
|
tagNotification: this.get('tagNotification')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default function() {
|
||||||
|
this.resource('tags', function() {
|
||||||
|
this.route('show', {path: ':tag_id'});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<h2>{{i18n "tagging.all_tags"}}</h2>
|
||||||
|
|
||||||
|
<div class='tag-cloud'>
|
||||||
|
{{#each tag in cloud}}
|
||||||
|
{{discourse-tag tagId=tag.id
|
||||||
|
count=tag.count
|
||||||
|
maxCount=model.max_count
|
||||||
|
minCount=model.min_count}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{#if tagNotification}}
|
||||||
|
{{tag-notifications-button tag=tag.tag_id
|
||||||
|
action="changeTagNotification"
|
||||||
|
notificationLevel=tagNotification.notification_level}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<h2>{{{i18n "tagging.topics_tagged" tag=tag.tag_id}}}</h2>
|
||||||
|
|
||||||
|
{{topic-list topics=list.topics}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import DiscoveryTopicsView from "discourse/views/discovery-topics";
|
||||||
|
|
||||||
|
export default DiscoveryTopicsView;
|
|
@ -0,0 +1,68 @@
|
||||||
|
|
||||||
|
.topic-title-outlet.choose-tags {
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud {
|
||||||
|
.discourse-tag {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-info-wrapper {
|
||||||
|
.discourse-tag {
|
||||||
|
-webkit-animation: fadein .7s;
|
||||||
|
animation: fadein .7s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.add-tags .select2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discourse-tag-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-result-label .discourse-tag {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discourse-tag {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
font-size: 0.857em;
|
||||||
|
|
||||||
|
/* !important is needed in the select2 widget to overwrite default styles */
|
||||||
|
color: #999 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
line-height: 1.4em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tags {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chooser {
|
||||||
|
width: 500px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-notification-menu {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tag-notification-menu .dropdown-menu {
|
||||||
|
right: 0;
|
||||||
|
top: 30px;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
# This file contains content for the client portion of Discourse, sent out
|
||||||
|
# to the Javascript app.
|
||||||
|
#
|
||||||
|
# To work with us on translations, see:
|
||||||
|
# https://www.transifex.com/projects/p/discourse-org/
|
||||||
|
#
|
||||||
|
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||||
|
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||||
|
#
|
||||||
|
# tx push -s
|
||||||
|
#
|
||||||
|
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||||
|
#
|
||||||
|
# To validate this YAML file after you change it, please paste it into
|
||||||
|
# http://yamllint.com/
|
||||||
|
|
||||||
|
en:
|
||||||
|
js:
|
||||||
|
tagging:
|
||||||
|
all_tags: "All Tags"
|
||||||
|
changed: "tags changed:"
|
||||||
|
tags: "Tags"
|
||||||
|
choose_for_topic: "choose optional tags for this topic"
|
||||||
|
topics_tagged: "Topics tagged <span class='discourse-tag'>{{tag}}</span>"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
watching:
|
||||||
|
title: "Watching"
|
||||||
|
description: "You will automatically watch all new topics in this tag. You will be notified of all new posts and topics, plus the count of unread and new posts will also appear next to the topic."
|
||||||
|
tracking:
|
||||||
|
title: "Tracking"
|
||||||
|
description: "You will automatically track all new topics in this tag. A count of unread and new posts will appear next to the topic."
|
||||||
|
regular:
|
||||||
|
title: "Regular"
|
||||||
|
description: "You will be notified if someone mentions your @name or replies to your post."
|
||||||
|
muted:
|
||||||
|
title: "Muted"
|
||||||
|
description: "You will not be notified of anything about new topics in this tag, and they will not appear on your unread tab."
|
|
@ -0,0 +1,25 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
# This file contains content for the client portion of Discourse, sent out
|
||||||
|
# to the Javascript app.
|
||||||
|
#
|
||||||
|
# To work with us on translations, see:
|
||||||
|
# https://www.transifex.com/projects/p/discourse-org/
|
||||||
|
#
|
||||||
|
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||||
|
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||||
|
#
|
||||||
|
# tx push -s
|
||||||
|
#
|
||||||
|
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||||
|
#
|
||||||
|
# To validate this YAML file after you change it, please paste it into
|
||||||
|
# http://yamllint.com/
|
||||||
|
|
||||||
|
pl_PL:
|
||||||
|
js:
|
||||||
|
tagging:
|
||||||
|
all_tags: "Wszystkie tagi"
|
||||||
|
changed: "zmienione tagi:"
|
||||||
|
tags: "Tagi"
|
||||||
|
choose_for_topic: "wybierz opcjonalne tagi dla tego tematu"
|
||||||
|
topics_tagged: "Tematy otagowane jako <span class='discourse-tag'>{{tag}}</span>"
|
|
@ -0,0 +1,24 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
# This file contains content for the client portion of Discourse, sent out
|
||||||
|
# to the Javascript app.
|
||||||
|
#
|
||||||
|
# To work with us on translations, see:
|
||||||
|
# https://www.transifex.com/projects/p/discourse-org/
|
||||||
|
#
|
||||||
|
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||||
|
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||||
|
#
|
||||||
|
# tx push -s
|
||||||
|
#
|
||||||
|
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||||
|
#
|
||||||
|
# To validate this YAML file after you change it, please paste it into
|
||||||
|
# http://yamllint.com/
|
||||||
|
|
||||||
|
ru:
|
||||||
|
js:
|
||||||
|
tagging:
|
||||||
|
all_tags: "Все теги"
|
||||||
|
tags: "Теги"
|
||||||
|
choose_for_topic: "выберите теги для темы"
|
||||||
|
topics_tagged: "Темы с тегом <span class='discourse-tag'>{{tag}}</span>"
|
|
@ -0,0 +1,25 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
# This file contains content for the client portion of Discourse, sent out
|
||||||
|
# to the Javascript app.
|
||||||
|
#
|
||||||
|
# To work with us on translations, see:
|
||||||
|
# https://www.transifex.com/projects/p/discourse-org/
|
||||||
|
#
|
||||||
|
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||||
|
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||||
|
#
|
||||||
|
# tx push -s
|
||||||
|
#
|
||||||
|
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||||
|
#
|
||||||
|
# To validate this YAML file after you change it, please paste it into
|
||||||
|
# http://yamllint.com/
|
||||||
|
|
||||||
|
zh_CN:
|
||||||
|
js:
|
||||||
|
tagging:
|
||||||
|
all_tags: "全部标签"
|
||||||
|
changed: "标签更改:"
|
||||||
|
tags: "标签"
|
||||||
|
choose_for_topic: "选择主题的标签"
|
||||||
|
topics_tagged: "主题标签: <span class='discourse-tag'>{{tag}}</span>"
|
|
@ -0,0 +1,9 @@
|
||||||
|
en:
|
||||||
|
site_settings:
|
||||||
|
tagging_enabled: "Allow users to tag topics?"
|
||||||
|
min_trust_to_create_tag: "The minimum trust level required to create a tag."
|
||||||
|
max_tags_per_topic: "The maximum tags that can be applied to a topic."
|
||||||
|
max_tag_length: "The maximum amount of characters that can be used in a tag."
|
||||||
|
rss_by_tag: "Topics tagged %{tag}"
|
||||||
|
rss_description:
|
||||||
|
tag: "Tagged topics"
|
|
@ -0,0 +1,9 @@
|
||||||
|
pl_PL:
|
||||||
|
site_settings:
|
||||||
|
tagging_enabled: "Pozwolić użytkownikom na tagowanie tematów?"
|
||||||
|
min_trust_to_create_tag: "Minimalny poziom zaufania dla tworzenia nowych tagów."
|
||||||
|
max_tags_per_topic: "Maksymalna ilość tagów przypisanych do tematu."
|
||||||
|
max_tag_length: "Maksymalna ilość znaków per tag."
|
||||||
|
rss_by_tag: "Tematy otagowane jako %{tag}"
|
||||||
|
rss_description:
|
||||||
|
tag: "Otagowane tematy"
|
|
@ -0,0 +1,4 @@
|
||||||
|
ru:
|
||||||
|
site_settings:
|
||||||
|
min_trust_to_create_tag: "Минимальный уровень доверия для создания тегов."
|
||||||
|
max_tags_per_topic: "Максимальное количество тегов для темы."
|
|
@ -0,0 +1,6 @@
|
||||||
|
zh_CN:
|
||||||
|
site_settings:
|
||||||
|
tagging_enabled: "允许用户为主题设置标签?"
|
||||||
|
min_trust_to_create_tag: "允许创建标签的最小信任等级"
|
||||||
|
max_tags_per_topic: "一个主题最多允许被创建多少个标签"
|
||||||
|
max_tag_length: "一个标签允许的最大字符数"
|
|
@ -0,0 +1,3 @@
|
||||||
|
plugins:
|
||||||
|
data_explorer_enabled:
|
||||||
|
default: false
|
|
@ -0,0 +1,222 @@
|
||||||
|
# name: discourse-data-explorer
|
||||||
|
# about: Interface for running analysis SQL queries on the live database
|
||||||
|
# version: 0.1
|
||||||
|
# authors: Riking
|
||||||
|
# url: https://github.com/discourse/discourse-data-explorer
|
||||||
|
|
||||||
|
enabled_site_setting :data_explorer_enabled
|
||||||
|
register_asset 'stylesheets/tagging.scss'
|
||||||
|
|
||||||
|
after_initialize do
|
||||||
|
|
||||||
|
TAGS_FIELD_NAME = "tags"
|
||||||
|
TAGS_FILTER_REGEXP = /[<\\\/\>\.\#\?\&\s]/
|
||||||
|
|
||||||
|
module ::DataExplorer
|
||||||
|
class Engine < ::Rails::Engine
|
||||||
|
engine_name "data_explorer"
|
||||||
|
isolate_namespace DataExplorer
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.clean_tag(tag)
|
||||||
|
tag.downcase.strip[0...SiteSetting.max_tag_length].gsub(TAGS_FILTER_REGEXP, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.tags_for_saving(tags, guardian)
|
||||||
|
return unless tags
|
||||||
|
|
||||||
|
tags.map! {|t| clean_tag(t) }
|
||||||
|
tags.delete_if {|t| t.blank? }
|
||||||
|
tags.uniq!
|
||||||
|
|
||||||
|
# If the user can't create tags, remove any tags that don't already exist
|
||||||
|
unless guardian.can_create_tag?
|
||||||
|
tag_count = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tags).group(:value).count
|
||||||
|
tags.delete_if {|t| !tag_count.has_key?(t) }
|
||||||
|
end
|
||||||
|
|
||||||
|
return tags[0...SiteSetting.max_tags_per_topic]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.notification_key(tag_id)
|
||||||
|
"tags_notification:#{tag_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.auto_notify_for(tags, topic)
|
||||||
|
|
||||||
|
key_names = tags.map {|t| notification_key(t) }
|
||||||
|
key_names_sql = ActiveRecord::Base.sql_fragment("(#{tags.map { "'%s'" }.join(', ')})", *key_names)
|
||||||
|
|
||||||
|
sql = <<-SQL
|
||||||
|
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
|
||||||
|
SELECT ucf.user_id,
|
||||||
|
#{topic.id.to_i},
|
||||||
|
CAST(ucf.value AS INTEGER),
|
||||||
|
#{TopicUser.notification_reasons[:plugin_changed]}
|
||||||
|
FROM user_custom_fields AS ucf
|
||||||
|
WHERE ucf.name IN #{key_names_sql}
|
||||||
|
AND NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = #{topic.id.to_i} AND user_id = ucf.user_id)
|
||||||
|
AND CAST(ucf.value AS INTEGER) <> #{TopicUser.notification_levels[:regular]}
|
||||||
|
SQL
|
||||||
|
|
||||||
|
ActiveRecord::Base.exec_sql(sql)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require_dependency 'application_controller'
|
||||||
|
require_dependency 'topic_list_responder'
|
||||||
|
class DataExplorer::TagsController < ::ApplicationController
|
||||||
|
include ::TopicListResponder
|
||||||
|
|
||||||
|
requires_plugin 'discourse-tagging'
|
||||||
|
skip_before_filter :check_xhr, only: [:tag_feed, :show]
|
||||||
|
before_filter :ensure_logged_in, only: [:notifications, :update_notifications]
|
||||||
|
|
||||||
|
def cloud
|
||||||
|
cloud = self.class.tags_by_count(guardian, limit: 300).count
|
||||||
|
result, max_count, min_count = [], 0, nil
|
||||||
|
cloud.each do |t, c|
|
||||||
|
result << { id: t, count: c }
|
||||||
|
max_count = c if c > max_count
|
||||||
|
min_count = c if min_count.nil? || c < min_count
|
||||||
|
end
|
||||||
|
|
||||||
|
result.sort_by! {|r| r[:id]}
|
||||||
|
|
||||||
|
render json: { cloud: result, max_count: max_count, min_count: min_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
tag_id = ::DiscourseTagging.clean_tag(params[:tag_id])
|
||||||
|
topics_tagged = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tag_id).pluck(:topic_id)
|
||||||
|
|
||||||
|
page = params[:page].to_i
|
||||||
|
|
||||||
|
query = TopicQuery.new(current_user, page: page)
|
||||||
|
latest_results = query.latest_results.where(id: topics_tagged)
|
||||||
|
@list = query.create_list(:by_tag, {}, latest_results)
|
||||||
|
@list.more_topics_url = list_by_tag_path(tag_id: tag_id, page: page + 1)
|
||||||
|
@rss = "tag"
|
||||||
|
|
||||||
|
respond_with_list(@list)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_feed
|
||||||
|
discourse_expires_in 1.minute
|
||||||
|
|
||||||
|
tag_id = ::DiscourseTagging.clean_tag(params[:tag_id])
|
||||||
|
@link = "#{Discourse.base_url}/tags/#{tag_id}"
|
||||||
|
@description = I18n.t("rss_by_tag", tag: tag_id)
|
||||||
|
@title = "#{SiteSetting.title} - #{@description}"
|
||||||
|
@atom_link = "#{Discourse.base_url}/tags/#{tag_id}.rss"
|
||||||
|
|
||||||
|
query = TopicQuery.new(current_user)
|
||||||
|
topics_tagged = TopicCustomField.where(name: TAGS_FIELD_NAME, value: tag_id).pluck(:topic_id)
|
||||||
|
latest_results = query.latest_results.where(id: topics_tagged)
|
||||||
|
@topic_list = query.create_list(:by_tag, {}, latest_results)
|
||||||
|
|
||||||
|
render 'list/list', formats: [:rss]
|
||||||
|
end
|
||||||
|
|
||||||
|
def search
|
||||||
|
tags = self.class.tags_by_count(guardian)
|
||||||
|
term = params[:q]
|
||||||
|
if term.present?
|
||||||
|
term.gsub!(/[^a-z0-9]*/, '')
|
||||||
|
tags = tags.where('value like ?', "%#{term}%")
|
||||||
|
end
|
||||||
|
|
||||||
|
tags = tags.count(:value).map {|t, c| { id: t, text: t, count: c } }
|
||||||
|
|
||||||
|
render json: { results: tags }
|
||||||
|
end
|
||||||
|
|
||||||
|
def notifications
|
||||||
|
level = current_user.custom_fields[::DiscourseTagging.notification_key(params[:tag_id])] || 1
|
||||||
|
render json: { tag_notifications: { id: params[:tag_id], notification_level: level.to_i } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_notifications
|
||||||
|
level = params[:tag_notifications][:notification_level].to_i
|
||||||
|
|
||||||
|
current_user.custom_fields[::DiscourseTagging.notification_key(params[:tag_id])] = level
|
||||||
|
current_user.save_custom_fields
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
|
||||||
|
def self.tags_by_count(guardian, opts=nil)
|
||||||
|
opts = opts || {}
|
||||||
|
result = TopicCustomField.where(name: TAGS_FIELD_NAME)
|
||||||
|
.joins(:topic)
|
||||||
|
.group(:value)
|
||||||
|
.limit(opts[:limit] || 5)
|
||||||
|
.order('COUNT(topic_custom_fields.value) DESC')
|
||||||
|
|
||||||
|
guardian.filter_allowed_categories(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DataExplorer::Engine.routes.draw do
|
||||||
|
get '/' => 'tags#cloud'
|
||||||
|
get '/filter/cloud' => 'tags#cloud'
|
||||||
|
get '/filter/search' => 'tags#search'
|
||||||
|
get '/:tag_id.rss' => 'tags#tag_feed'
|
||||||
|
get '/:tag_id' => 'tags#show', as: 'list_by_tag'
|
||||||
|
get '/:tag_id/notifications' => 'tags#notifications'
|
||||||
|
put '/:tag_id/notifications' => 'tags#update_notifications'
|
||||||
|
end
|
||||||
|
|
||||||
|
Discourse::Application.routes.append do
|
||||||
|
mount ::DataExplorer::Engine, at: "/tags"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add a `tags` reader to the Topic model for easy reading of tags
|
||||||
|
add_to_class(:topic, :tags) do
|
||||||
|
result = custom_fields[TAGS_FIELD_NAME]
|
||||||
|
return [result].flatten if result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Save the tags when the topic is saved
|
||||||
|
PostRevisor.track_topic_field(:tags_empty_array) do |tc, val|
|
||||||
|
if val.present?
|
||||||
|
tc.record_change(TAGS_FIELD_NAME, tc.topic.custom_fields[TAGS_FIELD_NAME], nil)
|
||||||
|
tc.topic.custom_fields.delete(TAGS_FIELD_NAME)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PostRevisor.track_topic_field(:tags) do |tc, tags|
|
||||||
|
if tags.present?
|
||||||
|
tags = ::DataExplorer.tags_for_saving(tags, tc.guardian)
|
||||||
|
|
||||||
|
new_tags = tags - (tc.topic.tags || [])
|
||||||
|
tc.record_change(TAGS_FIELD_NAME, tc.topic.custom_fields[TAGS_FIELD_NAME], tags)
|
||||||
|
tc.topic.custom_fields.update(TAGS_FIELD_NAME => tags)
|
||||||
|
|
||||||
|
::DataExplorer.auto_notify_for(new_tags, tc.topic) if new_tags.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on(:topic_created) do |topic, params, user|
|
||||||
|
tags = ::DataExplorer.tags_for_saving(params[:tags], Guardian.new(user))
|
||||||
|
if tags.present?
|
||||||
|
topic.custom_fields.update(TAGS_FIELD_NAME => tags)
|
||||||
|
topic.save
|
||||||
|
::DataExplorer.auto_notify_for(tags, topic)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_class(:guardian, :can_create_tag?) do
|
||||||
|
user && user.has_trust_level?(SiteSetting.min_trust_to_create_tag.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return tag related stuff in JSON output
|
||||||
|
TopicViewSerializer.attributes_from_topic(:tags)
|
||||||
|
add_to_serializer(:site, :can_create_tag) { scope.can_create_tag? }
|
||||||
|
add_to_serializer(:site, :tags_filter_regexp) { TAGS_FILTER_REGEXP.source }
|
||||||
|
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in New Issue