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