Merge branch 'master' into beta

This commit is contained in:
Neil Lalonde 2014-11-06 15:53:53 -05:00
commit c87673c651
172 changed files with 1687 additions and 917 deletions

View File

@ -6,31 +6,31 @@ PATH
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.1.6)
actionpack (= 4.1.6)
actionview (= 4.1.6)
actionmailer (4.1.7)
actionpack (= 4.1.7)
actionview (= 4.1.7)
mail (~> 2.5, >= 2.5.4)
actionpack (4.1.6)
actionview (= 4.1.6)
activesupport (= 4.1.6)
actionpack (4.1.7)
actionview (= 4.1.7)
activesupport (= 4.1.7)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionpack-action_caching (1.1.1)
actionpack (>= 4.0.0, < 5.0)
actionview (4.1.6)
activesupport (= 4.1.6)
actionview (4.1.7)
activesupport (= 4.1.7)
builder (~> 3.1)
erubis (~> 2.7.0)
active_model_serializers (0.8.2)
activemodel (>= 3.0)
activemodel (4.1.6)
activesupport (= 4.1.6)
activemodel (4.1.7)
activesupport (= 4.1.7)
builder (~> 3.1)
activerecord (4.1.6)
activemodel (= 4.1.6)
activesupport (= 4.1.6)
activerecord (4.1.7)
activemodel (= 4.1.7)
activesupport (= 4.1.7)
arel (~> 5.0.0)
activesupport (4.1.6)
activesupport (4.1.7)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
@ -171,7 +171,7 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.0)
minitest (5.4.1)
minitest (5.4.2)
mocha (1.1.0)
metaclass (~> 0.0.1)
mock_redis (0.13.2)
@ -255,21 +255,21 @@ GEM
rack
rack-test (0.6.2)
rack (>= 1.0)
rails (4.1.6)
actionmailer (= 4.1.6)
actionpack (= 4.1.6)
actionview (= 4.1.6)
activemodel (= 4.1.6)
activerecord (= 4.1.6)
activesupport (= 4.1.6)
rails (4.1.7)
actionmailer (= 4.1.7)
actionpack (= 4.1.7)
actionview (= 4.1.7)
activemodel (= 4.1.7)
activerecord (= 4.1.7)
activesupport (= 4.1.7)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.6)
railties (= 4.1.7)
sprockets-rails (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
railties (4.1.6)
actionpack (= 4.1.6)
activesupport (= 4.1.6)
railties (4.1.7)
actionpack (= 4.1.7)
activesupport (= 4.1.7)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)

View File

@ -2,17 +2,27 @@ export default Ember.ObjectController.extend({
viewMode: 'table',
viewingTable: Em.computed.equal('viewMode', 'table'),
viewingBarChart: Em.computed.equal('viewMode', 'barChart'),
startDate: null,
endDate: null,
refreshing: false,
actions: {
// Changes the current view mode to 'table'
refreshReport: function() {
var self = this;
this.set('refreshing', true);
Discourse.Report.find(this.get('type'), this.get('startDate'), this.get('endDate')).then(function(r) {
self.set('model', r);
}).finally(function() {
self.set('refreshing', false);
});
},
viewAsTable: function() {
this.set('viewMode', 'table');
},
// Changes the current view mode to 'barChart'
viewAsBarChart: function() {
this.set('viewMode', 'barChart');
}
}
});

View File

@ -90,11 +90,11 @@ export default Ember.ArrayController.extend(Discourse.Presence, {
@method refreshUsers
**/
refreshUsers: function() {
refreshUsers: function(showEmails) {
var adminUsersListController = this;
adminUsersListController.set('loading', true);
Discourse.AdminUser.findAll(this.get('query'), { filter: this.get('username') }).then(function (result) {
Discourse.AdminUser.findAll(this.get('query'), { filter: this.get('username'), show_emails: showEmails }).then(function (result) {
adminUsersListController.set('content', result);
adminUsersListController.set('loading', false);
});
@ -140,6 +140,10 @@ export default Ember.ArrayController.extend(Discourse.Presence, {
bootbox.alert(message);
controller.refreshUsers();
});
},
showEmails: function() {
this.refreshUsers(true);
}
});

View File

@ -140,9 +140,12 @@ Discourse.Report = Discourse.Model.extend({
});
Discourse.Report.reopenClass({
find: function(type) {
var model = Discourse.Report.create({type: type});
Discourse.ajax("/admin/reports/" + type).then(function (json) {
find: function(type, startDate, endDate) {
return Discourse.ajax("/admin/reports/" + type, {data: {
start_date: startDate,
end_date: endDate
}}).then(function (json) {
// Add a percent field to each tuple
var maxY = 0;
json.report.data.forEach(function (row) {
@ -153,9 +156,9 @@ Discourse.Report.reopenClass({
row.percentage = Math.round((row.y / maxY) * 100);
});
}
var model = Discourse.Report.create({type: type});
model.setProperties(json.report);
model.set('loaded', true);
return model;
});
return(model);
}
});

View File

@ -9,5 +9,13 @@
Discourse.AdminReportsRoute = Discourse.Route.extend({
model: function(params) {
return Discourse.Report.find(params.type);
},
setupController: function(controller, model) {
controller.setProperties({
model: model,
startDate: moment(model.get('start_date')).format('YYYY-MM-DD'),
endDate: moment(model.get('end_date')).format('YYYY-MM-DD')
});
}
});

View File

@ -19,7 +19,7 @@
</div>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if showHtml}}
{{{html_content}}}

View File

@ -9,7 +9,7 @@
<div class="admin-container">
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if length}}
<table class='admin-flags'>
@ -160,7 +160,7 @@
</tbody>
</table>
{{#if view.loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{else}}

View File

@ -1,7 +1,7 @@
<p>{{i18n admin.logs.screened_emails.description}}</p>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if model.length}}

View File

@ -4,7 +4,7 @@
<br/>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if model.length}}

View File

@ -1,7 +1,7 @@
<p>{{i18n admin.logs.screened_urls.description}}</p>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if model.length}}

View File

@ -45,7 +45,7 @@
{{#if loading}}
<br/>
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if model.length}}
{{view "staff-action-logs-list" content=controller}}

View File

@ -1,14 +1,28 @@
{{#if loaded}}
<h3>{{title}}</h3>
<h3>{{title}}</h3>
<button class='btn'
{{action "viewAsTable"}}
{{bind-attr disabled="viewingTable"}}>{{i18n admin.dashboard.reports.view_table}}</button>
<div>
{{i18n admin.dashboard.reports.start_date}} {{input type="date" value=startDate}}
{{i18n admin.dashboard.reports.end_date}} {{input type="date" value=endDate}}
<button {{action refreshReport}} class='btn btn-primary'>{{i18n admin.dashboard.reports.refresh_report}}</button>
</div>
<button class='btn'
{{action "viewAsBarChart"}}
{{bind-attr disabled="viewingBarChart"}}>{{i18n admin.dashboard.reports.view_chart}}</button>
<div class='view-options'>
{{#if viewingTable}}
{{i18n admin.dashboard.reports.view_table}}
{{else}}
<a href {{action "viewAsTable"}}>{{i18n admin.dashboard.reports.view_table}}</a>
{{/if}}
|
{{#if viewingBarChart}}
{{i18n admin.dashboard.reports.view_chart}}
{{else}}
<a href {{action "viewAsBarChart"}}>{{i18n admin.dashboard.reports.view_chart}}</a>
{{/if}}
</div>
{{#if refreshing}}
{{loading-spinner}}
{{else}}
<table class='table report'>
<tr>
<th>{{xaxis}}</th>
@ -31,7 +45,4 @@
</tr>
{{/each}}
</table>
{{else}}
{{i18n loading}}
{{/if}}

View File

@ -7,7 +7,7 @@
</div>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
<div class='admin-container user-badges'>
<h2>{{i18n admin.badges.grant_badge}}</h2>

View File

@ -28,12 +28,17 @@
</div>
{{/if}}
<h2>{{title}}</h2>
<br/>
<div class="admin-title">
<div class="pull-left">
<h2>{{title}}</h2>
</div>
<div class="pull-right">
<button {{action "showEmails"}} class="btn">{{i18n admin.users.show_emails}}</button>
</div>
</div>
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{else}}
{{#if model.length}}
<table class='table'>
@ -43,6 +48,7 @@
{{/if}}
<th>&nbsp;</th>
<th>{{i18n username}}</th>
<th>{{i18n email}}</th>
<th>{{i18n admin.users.last_emailed}}</th>
<th>{{i18n last_seen}}</th>
<th>{{i18n admin.user.topics_entered}}</th>
@ -53,7 +59,6 @@
<th>{{i18n admin.users.approved}}</th>
{{/if}}
<th>&nbsp;</th>
</tr>
{{#each model}}
@ -67,6 +72,7 @@
{{/if}}
<td>{{#link-to 'adminUser' this}}{{avatar this imageSize="small"}}{{/link-to}}</td>
<td>{{#link-to 'adminUser' this}}{{unbound username}}{{/link-to}}</td>
<td>{{{unbound email}}}</td>
<td>{{{unbound last_emailed_age}}}</td>
<td>{{{unbound last_seen_age}}}</td>
<td>{{{unbound topics_entered}}}</td>

View File

@ -1,12 +0,0 @@
/**
Displays a list of groups that a user belongs to.
@class Discourse.GroupsListComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
export default Em.Component.extend({
classNames: ['groups']
});

View File

@ -1,14 +1,9 @@
export default Ember.Component.extend({
tagName: 'a',
attributeBindings: ['href'],
attributeBindings: ['href','data-user-card'],
classNames: ['trigger-user-card'],
href: Em.computed.alias('post.usernameUrl'),
click: function(e) {
this.appEvents.trigger('poster:expand', $(e.target));
this.sendAction('action', this.get('post'));
return false;
},
href: Em.computed.oneWay('post.usernameUrl'),
"data-user-card": Em.computed.oneWay('post.username'),
render: function(buffer) {
var avatar = Handlebars.helpers.avatar(this.get('post'), {hash: {imageSize: 'large'}});

View File

@ -26,7 +26,7 @@ var PosterNameComponent = Em.Component.extend({
linkClass += ' ' + primaryGroupName;
}
// Main link
buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true'>" + username + "</a>");
buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true' data-user-card='" + username + "'>" + username + "</a>");
// Add a glyph if we have one
var glyph = this.posterGlyph(post);
@ -38,7 +38,7 @@ var PosterNameComponent = Em.Component.extend({
// Are we showing full names?
if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) {
name = Handlebars.Utils.escapeExpression(name);
buffer.push("<span class='full-name'><a href='" + url + "' data-auto-route='true'>" + name + "</a></span>");
buffer.push("<span class='full-name'><a href='" + url + "' data-auto-route='true' data-user-card='" + username + "'>" + name + "</a></span>");
}
// User titles
@ -59,20 +59,6 @@ var PosterNameComponent = Em.Component.extend({
}
},
click: function(e) {
var $target = $(e.target),
href = $target.attr('href'),
url = this.get('post.usernameUrl');
if (!Em.isEmpty(href) && href !== url) {
return true;
} else {
this.appEvents.trigger('poster:expand', $target);
this.sendAction('expandAction', this.get('post'));
}
return false;
},
/**
Overwrite this to give a user a custom font awesome glyph.

View File

@ -0,0 +1,9 @@
export default Ember.Component.extend({
_parse: function() {
this.$().ellipsis();
}.on('didInsertElement'),
render: function(buffer) {
buffer.push(this.get('text'));
}
});

View File

@ -40,13 +40,11 @@ export default Ember.Component.extend({
var renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
var title = I18n.t("topic_statuses." + key + ".help");
var title = Handlebars.Utils.escapeExpression(I18n.t("topic_statuses." + key + ".help"));
var startTag = actionable ? "a href='#'" : "span";
var endTag = actionable ? "a" : "span";
buffer.push("<" + startTag +
" title='" + title +"' class='topic-status'><i class='fa fa-" + name + "'></i></" + endTag + ">");
buffer.push("<" + startTag + " title='" + title + "' class='topic-status'><i class='fa fa-" + name + "'></i></" + endTag + ">");
};
// Allow a plugin to add a custom icon to a topic

View File

@ -85,7 +85,7 @@ var controllerOpts = {
if (selected.length > 0) {
promise = Discourse.Topic.bulkOperation(selected, operation);
} else {
promise = Discourse.Topic.bulkOperationByFilter(this.get('filter'), operation);
promise = Discourse.Topic.bulkOperationByFilter('unread', operation, this.get('category.id'));
}
promise.then(function(result) {
if (result && result.topic_ids) {
@ -105,8 +105,12 @@ var controllerOpts = {
return Discourse.TopicTrackingState.current();
}.property(),
isFilterPage: function(filter, filterType) {
return filter.match(new RegExp(filterType + '$', 'gi')) ? true : false;
},
showDismissRead: function() {
return this.get('filter') === 'unread' && this.get('topics.length') > 0;
return this.isFilterPage(this.get('filter'), 'unread') && this.get('topics.length') > 0;
}.property('filter', 'topics.length'),
showResetNew: function() {
@ -114,8 +118,8 @@ var controllerOpts = {
}.property('filter', 'topics.length'),
showDismissAtTop: function() {
return (this.get('filter') === 'new' ||
this.get('filter') === 'unread') &&
return (this.isFilterPage(this.get('filter'), 'new') ||
this.isFilterPage(this.get('filter'), 'unread')) &&
this.get('topics.length') >= 30;
}.property('filter', 'topics.length'),

View File

@ -18,8 +18,12 @@ export default ObjectController.extend({
}.property('name_key'),
formattedName: function(){
return this.get('name').replace("{{username}}", this.get('controllers.flag.username'));
}.property('name'),
if (this.get("is_custom_flag")) {
return this.get('name').replace("{{username}}", this.get('controllers.flag.username'));
} else {
return I18n.t("flagging.formatted_name." + this.get('name_key'));
}
}.property('name', 'name_key', 'is_custom_flag'),
selected: function() {
return this.get('model') === this.get('controllers.flag.selected');

View File

@ -1,4 +1,5 @@
import ObjectController from 'discourse/controllers/object';
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
export default ObjectController.extend(Discourse.SelectedPostsCount, {
multiSelect: false,
@ -8,6 +9,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
selectedPosts: null,
selectedReplies: null,
queryParams: ['filter', 'username_filters', 'show_deleted'],
searchHighlight: null,
maxTitleLength: Discourse.computed.setting('max_topic_title_length'),
@ -509,7 +511,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
}.property('isPrivateMessage'),
loadingHTML: function() {
return "<div class='spinner'></div>";
return spinnerHTML;
}.property(),
recoverTopic: function() {

View File

@ -7,6 +7,8 @@ export default ObjectController.extend({
username: null,
participant: null,
avatar: null,
userLoading: null,
cardTarget: null,
postStream: Em.computed.alias('controllers.topic.postStream'),
enoughPostsForFiltering: Em.computed.gte('participant.post_count', 2),
@ -28,7 +30,12 @@ export default ObjectController.extend({
showMoreBadges: Em.computed.gt('moreBadgesCount', 0),
show: function(username, uploadedAvatarId) {
hasCardBadgeImage: function() {
var img = this.get('user.card_badge.image');
return img && img.indexOf('fa-') !== 0;
}.property('user.card_badge.image'),
show: function(username, target) {
// XSS protection (should be encapsulated)
username = username.replace(/[^A-Za-z0-9_]/g, "");
var url = "/users/" + username;
@ -42,17 +49,17 @@ export default ObjectController.extend({
var currentUsername = this.get('username'),
wasVisible = this.get('visible');
if (uploadedAvatarId) {
this.set('avatar', {username: username, uploaded_avatar_id: uploadedAvatarId});
} else {
this.set('avatar', null);
this.set('avatar', null);
this.set('username', username);
// If we click the avatar again, close it (unless its diff element on the screen).
if (target === this.get('cardTarget') && wasVisible) {
this.setProperties({ visible: false, username: null, avatar: null, cardTarget: null });
return;
}
this.setProperties({visible: true, username: username});
// If we click the avatar again, close it.
if (username === currentUsername && wasVisible) {
this.setProperties({ visible: false, username: null, avatar: null });
if (username === currentUsername && this.get('userLoading') === username) {
// debounce
return;
}
@ -66,14 +73,19 @@ export default ObjectController.extend({
var self = this;
self.set('user', null);
self.set('userLoading', username);
self.set('cardTarget', target);
Discourse.User.findByUsername(username).then(function (user) {
self.set('user', user);
self.set('avatar', user);
self.setProperties({ user: user, avatar: user, visible: true});
}).finally(function(){
self.set('userLoading', null);
});
},
close: function() {
this.set('visible', false);
this.set('cardTarget', null);
},
actions: {

View File

@ -0,0 +1,7 @@
var spinnerHTML = "<div class='spinner'><div class='spinner-container container1'><div class='circle1'></div><div class='circle2'></div><div class='circle3'></div><div class='circle4'></div></div><div class='spinner-container container2'><div class='circle1'></div><div class='circle2'></div><div class='circle3'></div><div class='circle4'></div></div><div class='spinner-container container3'><div class='circle1'></div><div class='circle2'></div><div class='circle3'></div><div class='circle4'></div></div></div>";
Handlebars.registerHelper('loading-spinner', function() {
return new Handlebars.SafeString(spinnerHTML);
});
export { spinnerHTML };

View File

@ -331,7 +331,7 @@ export default function(options) {
}
// ESC
if (e.which === keys.escape) {
if (e.which === keys.esc) {
if (completeStart !== null) {
closeAutocomplete();
return false;

View File

@ -19,7 +19,13 @@ Discourse.PageTracker = Ember.Object.extend(Ember.Evented, {
router.on('didTransition', function() {
this.send('refreshTitle');
self.trigger('change', this.get('url'), Discourse.get('_docTitle'));
var url = this.get('url');
// Refreshing the title is debounced, so we need to trigger this in the
// next runloop to have the correct title.
Em.run.next(function() {
self.trigger('change', url, Discourse.get('_docTitle'));
});
});
this.set('started', true);
}

View File

@ -269,7 +269,7 @@ Discourse.Utilities = {
@param {String} path The path
**/
isAnImage: function(path) {
return (/\.(png|jpg|jpeg|gif|bmp|tif|tiff)$/i).test(path);
return (/\.(png|jpg|jpeg|gif|bmp|tif|tiff|svg|webp)$/i).test(path);
},
/**
@ -279,7 +279,7 @@ Discourse.Utilities = {
**/
allowsAttachments: function() {
return Discourse.Utilities.authorizesAllExtensions() ||
!(/((png|jpg|jpeg|gif|bmp|tif|tiff)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
!(/((png|jpg|jpeg|gif|bmp|tif|tiff|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
},
displayErrorForUpload: function(data) {

View File

@ -130,7 +130,7 @@ Discourse.PostStream = Em.Object.extend({
hasNoFilters: function() {
var streamFilters = this.get('streamFilters');
return !(streamFilters && ((streamFilters.filter === 'summary') || streamFilters.userFilters));
return !(streamFilters && ((streamFilters.filter === 'summary') || streamFilters.username_filters));
}.property('streamFilters.[]', 'topic.posts_count', 'posts.length'),
/**

View File

@ -471,10 +471,12 @@ Discourse.Topic.reopenClass({
});
},
bulkOperationByFilter: function(filter, operation) {
bulkOperationByFilter: function(filter, operation, categoryId) {
var data = { filter: filter, operation: operation };
if (categoryId) data['category_id'] = categoryId;
return Discourse.ajax("/topics/bulk", {
type: 'PUT',
data: { filter: filter, operation: operation }
data: data
});
},

View File

@ -19,11 +19,6 @@ var ApplicationRoute = Discourse.Route.extend({
});
},
expandUser: function(user) {
this.controllerFor('user-card').show(user.get('username'), user.get('uploaded_avatar_id'));
return true;
},
error: function(err, transition) {
if (err.status === 404) {
// 404

View File

@ -38,18 +38,6 @@ Discourse.TopicRoute = Discourse.Route.extend({
this.controllerFor("topic-admin-menu").send("show");
},
// Modals that can pop up within a topic
expandPostUser: function(post) {
this.controllerFor('user-card').show(post.get('username'), post.get('uploaded_avatar_id'));
},
expandPostUsername: function(username) {
username = username.replace(/^@/, '');
if (!Em.isEmpty(username)) {
this.controllerFor('user-card').show(username);
}
},
showFlags: function(post) {
Discourse.Route.showModal(this, 'flag', post);
this.controllerFor('flag').setProperties({ selected: null });

View File

@ -36,11 +36,11 @@
{{/each}}
</div>
{{#if canLoadMore}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{else}}
{{#unless userBadgesLoaded}}
<div class='spinner'></div>
{{loading-spinner}}
{{/unless}}
{{/if}}
</div>

View File

@ -56,5 +56,5 @@
</div>
{{/if}}
{{else}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}

View File

@ -1,6 +0,0 @@
{{#if groups}}
{{i18n groups.title count=groups.length}}:
{{#each groups}}
{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}
{{/each}}
{{/if}}

View File

@ -1,4 +1,4 @@
<div class='spinner'></div>
{{loading-spinner}}
<div class='contents'>

View File

@ -11,7 +11,7 @@
</div>
<div {{bind-attr class="loadingSpinner::hidden"}}>
<div class='spinner'></div>
{{loading-spinner}}
</div>

View File

@ -72,7 +72,7 @@
<footer class='topic-list-bottom'>
{{#if loadingMore}}
<div class='topics-loading'></div>
{{loading-spinner}}
{{/if}}
{{#if allLoaded}}
{{#if showDismissRead}}

View File

@ -2,13 +2,13 @@
<div class='topic-avatar'>
<div class='contents'>
<div>
{{poster-avatar action="expandPostUser" post=this classNames="main-avatar"}}
{{poster-avatar post=this classNames="main-avatar"}}
</div>
</div>
</div>
<div class='topic-body'>
<div class="topic-meta-data">
{{poster-name post=this expandAction="expandPostUser"}}
{{poster-name post=this}}
{{#if view.parentView.previousPost}}<a href='{{unbound url}}' class="post-info arrow" title="{{i18n topic.jump_reply_up}}"><i class='fa fa-arrow-up'></i></a>{{/if}}
<div class='post-info post-date'>{{age-with-tooltip created_at}}</div>
</div>

View File

@ -53,5 +53,5 @@
</div>
{{/if}}
{{else}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}

View File

@ -25,7 +25,7 @@
<footer class='topic-list-bottom'>
{{#if loadingMore}}
<div class='topics-loading'></div>
{{loading-spinner}}
{{/if}}
{{#if allLoaded}}
{{#if showDismissRead}}

View File

@ -20,7 +20,7 @@
<div class='topic-avatar'>
<div class="contents">
{{#unless userDeleted}}
{{poster-avatar action="expandPostUser" post=this classNames="main-avatar"}}
{{poster-avatar post=this classNames="main-avatar"}}
{{else}}
<i class="fa fa-trash-o deleted-user-avatar"></i>
{{/unless}}
@ -30,7 +30,7 @@
<div class='topic-body'>
<div class='topic-meta-data'>
{{poster-name post=this expandAction="expandPostUser"}}
{{poster-name post=this}}
<div class='post-info'>
<a class='post-date' {{bind-attr href="shareUrl" data-share-url="shareUrl" data-post-number="post_number"}}>{{age-with-tooltip created_at}}</a>
</div>

View File

@ -8,7 +8,7 @@
<a href="#" class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n show_help}}</a>
</div>
{{#if loading}}
<div class='searching'></div>
<div class='searching'>{{loading-spinner}}</div>
{{else}}
<div class="results">
{{#unless noResults}}

View File

@ -73,7 +73,7 @@
{{render 'topic-progress'}}
{{#if postStream.loadingAbove}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{#unless postStream.loadingFilter}}
@ -90,7 +90,7 @@
{{/unless}}
{{#if postStream.loadingBelow}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
</div>
<div id='topic-bottom'></div>
@ -137,13 +137,13 @@
{{/if}}
</div>
{{#if retrying}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{/if}}
</div>
{{else}}
<div class='container'>
<div class='spinner'></div>
{{loading-spinner}}
</div>
{{/if}}
{{/if}}

View File

@ -1,72 +1,73 @@
{{#if username}}
<div class="card-content">
<div class="card-content">
{{#link-to 'user' user}}{{bound-avatar avatar "huge"}}{{/link-to}}
<div class="names">
<span>
<h1 {{bind-attr class="staff new_user"}}>
{{#link-to 'user' user}}{{username}}{{/link-to}}
</h1>
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{#if showName}}
<h2>{{#link-to 'user' user}}{{name}}{{/link-to}}</h2>
{{/if}}
</span>
<h1 {{bind-attr class="staff new_user"}}>
{{#link-to 'user' user}}{{username}}{{{user.statusIcon}}}{{/link-to}}
</h1>
{{#if user.name}}
<h2>{{user.name}}</h2>
{{/if}}
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{#if showName}}
<h2>{{#link-to 'user' user}}{{name}}{{/link-to}}</h2>
{{/if}}
</span>
</div>
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li><a class='btn btn-primary' {{action "composePrivateMessage" user}}>{{fa-icon "envelope"}}{{i18n user.private_message}}</a></li>
{{/if}}
{{#if showFilter}}
<li><a class='btn' {{action "togglePosts" user}}>{{fa-icon "filter"}}{{i18n topic.filter_to username="username" post_count="participant.post_count"}}</a></li>
{{/if}}
{{#if hasUserFilters}}
<li><a class='btn' {{action "cancelFilter"}}>{{fa-icon "times"}}{{i18n topic.filters.cancel}}</a></li>
{{/if}}
</ul>
{{#if isSuspended}}
<div class='suspended'>
{{fa-icon "ban"}}
<b>{{i18n user.suspended_notice date="user.suspendedTillDate"}}</b><br/>
<b>{{i18n user.suspended_reason}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_cooked}}</div>{{/if}}
{{/if}}
{{#if user.card_badge}}
{{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}}
{{icon-or-image user.card_badge.image title=user.card_badge.name}}
{{/link-to}}
{{/if}}
{{#if user}}
{{#if isSuspended}}
<div class='suspended'>
{{fa-icon "ban"}}
<b>{{i18n user.suspended_notice date="user.suspendedTillDate"}}</b><br/>
<b>{{i18n user.suspended_reason}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{{user.bio_cooked}}}</div>{{/if}}
{{/if}}
{{#if showBadges}}
<div class="badge-section">
{{#each user.featured_user_badges}}
{{user-badge badge=badge}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}}
{{i18n badges.more_badges count=moreBadgesCount}}
{{/link-to}}
{{/if}}
<div class="metadata">
<h3><span class='desc'>{{i18n last_post}}</span> {{format-date path="user.last_posted_at" leaveAgo="true"}} </h3>
<h3><span class='desc'>{{i18n joined}}</span> {{format-date path="user.created_at" leaveAgo="true"}}</h3>
</div>
{{/if}}
{{/if}}
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li><a class='btn btn-primary' {{action "composePrivateMessage" user}}>{{fa-icon "envelope"}}{{i18n user.private_message}}</a></li>
{{/if}}
<li>{{#link-to 'user' user class="btn"}}{{fa-icon "user"}}{{i18n user.profile}}{{/link-to}}</li>
{{#if showFilter}}
<li><a class='btn' {{action "togglePosts" user}}>{{fa-icon "filter"}}{{i18n topic.filter_to username="username" post_count="participant.post_count"}}</a></li>
{{/if}}
{{#if hasUserFilters}}
<li><a class='btn' {{action "cancelFilter"}}>{{fa-icon "times"}}{{i18n topic.filters.cancel}}</a></li>
{{/if}}
</ul>
{{#if user.card_badge}}
{{#link-to 'badges.show' user.card_badge class="card-badge"}}
{{icon-or-image user.card_badge.image}}
{{#if showBadges}}
<div class="badge-section">
{{#each user.featured_user_badges}}
{{user-badge badge=badge}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}}
{{i18n badges.more_badges count=moreBadgesCount}}
{{/link-to}}
{{/if}}
</div>
{{else}}
<p class='loading'>{{i18n loading}}</p>
</div>
{{/if}}
{{/if}}
</div>

View File

@ -70,7 +70,7 @@
{{/each}}
</table>
{{#if invitesLoading}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{else}}

View File

@ -23,7 +23,7 @@
{{/each}}
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}
{{#unless canLoadMore}}
<div class='end-of-stream'></div>

View File

@ -27,5 +27,5 @@
</div>
{{/each}}
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}

View File

@ -2,7 +2,7 @@
<div {{bind-attr class=":item hidden deleted moderator_action"}}>
<div class='clearfix info'>
<a href="{{unbound userUrl}}" data-user-card="{{unbound username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{format-date path="created_at" leaveAgo="true"}}</span>
<span class='time'>{{format-date path="created_at"}}</span>
<span class="title">
<a href="{{unbound postUrl}}">{{unbound title}}</a>
</span>
@ -26,5 +26,5 @@
</div>
{{/grouped-each}}
{{#if loading}}
<div class='spinner'></div>
{{loading-spinner}}
{{/if}}

View File

@ -94,11 +94,11 @@
{{#if websiteName}}
<i class="fa fa-globe"></i>
{{#if linkWebsite}}
<a {{bind-attr href="website"}} rel="nofollow" target="_blank">{{websiteName}}</a>
{{else}}
<span {{bind-attr title="website"}}>{{websiteName}}</span>
{{/if}}
{{#if linkWebsite}}
<a {{bind-attr href="website"}} rel="nofollow" target="_blank">{{websiteName}}</a>
{{else}}
<span {{bind-attr title="website"}}>{{websiteName}}</span>
{{/if}}
{{/if}}
</h3>
@ -150,6 +150,14 @@
{{/if}}
</dd>
{{/if}}
{{#if custom_groups}}
<dt>{{i18n groups.title count=custom_groups.length}}</dt>
<dd class='groups'>
{{#each custom_groups}}
<span>{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}</span>
{{/each}}
</dd>
{{/if}}
</dl>
{{plugin-outlet "user-profile-secondary"}}
</div>

View File

@ -70,35 +70,6 @@ export default Discourse.View.extend(AddCategoryClass, Discourse.Scrolling, {
}.on('willDestroyElement'),
debounceLoadSuggested: Discourse.debounce(function(){
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
var incoming = this.get('topicTrackingState.newIncoming'),
suggested = this.get('topic.details.suggested_topics'),
topicId = this.get('topic.id');
if(suggested) {
var existing = _.invoke(suggested, 'get', 'id'),
lookup = _.chain(incoming)
.last(Discourse.SiteSettings.suggested_topics)
.reverse()
.union(existing)
.uniq()
.without(topicId)
.first(Discourse.SiteSettings.suggested_topics)
.value();
Discourse.TopicList.loadTopics(lookup, "").then(function(topics){
suggested.clear();
suggested.pushObjects(topics);
});
}
}, 1000),
hasNewSuggested: function(){
this.debounceLoadSuggested();
}.observes('topicTrackingState.incomingCount'),
gotFocus: function(){
if (Discourse.get('hasFocus')){
this.scrolled();

View File

@ -6,7 +6,7 @@ var clickOutsideEventName = "mousedown.outside-user-card",
export default Discourse.View.extend(CleansUp, {
elementId: 'user-card',
classNameBindings: ['controller.visible::hidden', 'controller.showBadges'],
classNameBindings: ['controller.visible::hidden', 'controller.showBadges', 'controller.hasCardBadgeImage'],
allowBackgrounds: Discourse.computed.setting('allow_profile_backgrounds'),
addBackground: function() {
@ -30,8 +30,11 @@ export default Discourse.View.extend(CleansUp, {
$('html').off(clickOutsideEventName).on(clickOutsideEventName, function(e) {
if (self.get('controller.visible')) {
var $target = $(e.target);
if ($target.closest('.trigger-user-card').length > 0) { return; }
if (self.$().has(e.target).length !== 0) { return; }
if ($target.closest('[data-user-card]').data('userCard') ||
$target.closest('a.mention').length > 0 ||
$target.closest('#user-card').length > 0) {
return;
}
self.get('controller').close();
}
@ -39,19 +42,22 @@ export default Discourse.View.extend(CleansUp, {
return true;
});
var expand = function(username, $target){
self._posterExpand($target);
self.get('controller').show(username, $target[0]);
return false;
};
$('#main-outlet').on(clickDataExpand, '[data-user-card]', function(e) {
var $target = $(e.currentTarget);
self._posterExpand($target);
self.get('controller').show($target.data('user-card'));
return false;
var username = $target.data('user-card');
return expand(username, $target);
});
$('#main-outlet').on(clickMention, 'a.mention', function(e) {
var $target = $(e.target);
self._posterExpand($target);
var username = $target.text().replace(/^@/, '');
self.get('controller').show(username);
return false;
return expand(username, $target);
});
}.on('didInsertElement'),
@ -67,9 +73,11 @@ export default Discourse.View.extend(CleansUp, {
var overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 5;
position.left -= (width/2) - 10;
position.top += target.height() + 8;
}
position.top -= $('#main-outlet').offset().top;
self.$().css(position);
}
}

View File

@ -45,6 +45,7 @@
//= require ./discourse/components/visible
//= require ./discourse/helpers/user-avatar
//= require ./discourse/helpers/cold-age-class
//= require ./discourse/helpers/loading-spinner
//= require ./discourse/dialects/dialect
//= require_tree ./discourse/dialects

View File

@ -40,4 +40,5 @@
//= require ember-cloaking
//= require break_string
//= require buffered-proxy
//= require jquery.autoellipsis-1.0.10.min.js
//= require_tree ./discourse/ember

View File

@ -39,6 +39,9 @@ td.flaggers td {
}
}
.view-options {
float: right;
}
table.report {
margin-top: 20px;
tr {
@ -89,6 +92,10 @@ td.flaggers td {
margin-top: 20px;
}
.admin-title {
height: 45px;
}
.admin-controls {
background-color: dark-light-diff($primary, $secondary, 90%, -75%);
padding: 10px 10px 3px 0;

View File

@ -149,17 +149,6 @@
}
}
.topics-loading {
width: 20px;
margin: 0 auto;
padding: 25px 15px;
background: {
image: image-url("spinner_96.gif");
repeat: no-repeat;
size: 50px;
};
}
.list-controls {
.home {
background-color: scale-color-diff();

View File

@ -15,6 +15,10 @@ big {
font-size: 28px;
}
small {
font-size: 10px;
}
blockquote {
background-color: scale-color-diff();
border-left: 5px solid darken(scale-color-diff(), 10%);
@ -143,28 +147,6 @@ body {
}
}
.spinner {
width: 20px;
margin: 0 auto;
padding: 25px 15px;
background: {
image: image-url("spinner_96.gif");
repeat: no-repeat;
size: 50px;
};
-webkit-animation: spinner .25s;
animation: spinner .25s;
margin-top: 10px;
}
//loading spinner fade-in
@-webkit-keyframes spinner {
from {opacity: 0}
to {opacity: 1}
}@keyframes fade {
from {opacity: 0}
to {opacity: 1}
}
.avatar-wrapper {
background-color: $secondary;
@ -193,3 +175,115 @@ body {
.fade.in {
opacity: 1;
}
.spinner {
margin: 20px auto 0 auto;
width: 35px;
height: 35px;
position: relative;
.container1 > div, .container2 > div, .container3 > div {
width: 6px;
height: 6px;
background-color: $primary;
border-radius: 100%;
position: absolute;
-webkit-animation: bouncedelay 1.2s infinite ease-in-out;
animation: bouncedelay 1.2s infinite ease-in-out;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.spinner-container {
position: absolute;
width: 100%;
height: 100%;
}
.container2 {
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}
.container3 {
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
.circle1 { top: 0; left: 0; }
.circle2 { top: 0; right: 0; }
.circle3 { right: 0; bottom: 0; }
.circle4 { left: 0; bottom: 0; }
.container2 .circle1 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.container3 .circle1 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
.container1 .circle2 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.container2 .circle2 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.container3 .circle2 {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.container1 .circle3 {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.container2 .circle3 {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.container3 .circle3 {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.container1 .circle4 {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.container2 .circle4 {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.container3 .circle4 {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}

View File

@ -270,18 +270,17 @@
}
.searching {
display: block;
position: absolute;
top: 17px;
right: 15px;
width: 22px;
height: 22px;
background: {
image: image-url("spinner_96.gif");
repeat: no-repeat;
position: 0px 50%;
size: 18px;
};
top: 0;
right: 0;
.spinner {
width: 16px;
height: 16px;
}
.container1 > div, .container2 > div, .container3 > div {
width: 4px;
height: 4px;
}
}
// I am ghetto using this to display "Show More".. be warned
.no-results {
@ -366,5 +365,3 @@
#search-help table td {
padding-right: 10px;
}

View File

@ -33,7 +33,6 @@ body {
.boxed {
height: 100%;
@include border-radius-all(5px);
&.white {
background-color: $secondary;
}

View File

@ -68,7 +68,7 @@
background-color: $secondary;
}
&.highlighted {
background-color: scale-color($tertiary, $lightness: 85%);
background-color: dark-light-diff($tertiary, $secondary, 90%, -41%);
}
}
button.bulk-select {

View File

@ -242,7 +242,7 @@ nav.post-controls {
.topic-body {
background: dark-light-diff($primary, $secondary, 90%, -65%) !important;
width: 86%;
width: 87%;
padding-left: 1%;
padding-right: 1%;
border-top: none;
@ -253,7 +253,7 @@ nav.post-controls {
.topic-avatar {
width: 45px;
margin: 0 10px 0 15px;
margin: 0 5px 0 15px;
border: none;
}
@ -614,11 +614,11 @@ iframe {
position: relative;
}
a.mention {
padding: 2px 4px;
color: $primary;
background: scale-color-diff();
}
a.mention {
padding: 2px 4px;
color: $primary;
background: scale-color-diff();
}
.modal-body {

View File

@ -11,11 +11,13 @@
color: $secondary;
background-size: cover;
background-position: center center;
min-height: 175px;
.card-content {
padding: 12px;
padding: 12px 12px 0 12px;
background: rgba($primary, .85);
margin-top: 100px;
margin-top: 80px;
&:after {
content: '';
display: block;
@ -24,6 +26,8 @@
}
&.no-bg {
min-height: 50px;
.card-content {
margin-top: 0;
}
@ -49,6 +53,10 @@
a {
color: $secondary;
}
i {
font-size: 18px;
color: $secondary;
}
}
h2 {
@ -69,11 +77,11 @@
font-size: 13px;
font-weight: normal;
margin-top: 0;
color: scale-color($primary, $lightness: 50%);
color: dark-light-diff($secondary, $primary, 25%, -25%);
overflow: hidden;
text-overflow: ellipsis;
a {
color: $secondary;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
}
.groups {
@ -88,11 +96,19 @@
}
.metadata {
position: absolute;
right: 20px;
top: 10px;
max-width: 180px;
text-align: right;
width: 100%;
clear: both;
padding-top: 5px;
h3 {
display: inline;
margin-right: 5px;
.desc, a {
color: dark-light-diff($secondary, $primary, 50%, -50%);
}
}
div {display: inline; color: scale-color($primary, $lightness: 50%);
.group-link {color: scale-color($primary, $lightness: 50%);}
}
}
.bottom {
@ -100,12 +116,14 @@
padding-top: 10px;
}
&.has-card-badge-image .bio {
width: 75%;
}
.bio {
max-height: 55px;
overflow: auto;
float: left;
margin: 10px 0;
width: 70%;
padding: 15px 0 0 0;
clear: left;
a {
color: $secondary;
text-decoration: underline;
@ -113,23 +131,26 @@
img {
max-width: 100%;
}
a.mention {
background-color: dark-light-diff($secondary, $primary, 50%, -60%);
}
.overflow {
max-height: 60px;
overflow: hidden;
}
}
img.avatar {
float: left;
padding-right: 10px;
margin-top: -60px;
margin-top: -53px;
}
p {
margin: 0 0 5px 0;
}
p.loading {
margin-top: 45px;
color: $primary;
}
.btn {
margin: 0 0 7px 0;
box-sizing: border-box;
@ -137,9 +158,8 @@
.usercard-controls {
list-style-type: none;
float: right;
margin: 0;
position: absolute;
right: 12px;
a {width: 100%; min-width: 150px;}
}
@ -156,24 +176,27 @@
height: 60px;
position: relative;
width: 45%;
margin-top: 11px;
span {
position: absolute;
bottom: 0;
display: block;
width: 250px;
}
}
.badge-section {
margin-top: 10px;
float: left;
width: 390px;
width: 500px;
padding-bottom: 10px;
margin-top: 5px;
.user-badge {
background: transparent;
color: $secondary;
color: dark-light-diff($primary, $secondary, 50%, -50%);
border-color: dark-light-diff($primary, $secondary, 50%, -50%);
}
h3 {
@ -186,20 +209,24 @@
.more-user-badges {
@extend .user-badge;
padding: 3px 8px;
font-size: 13px;
}
}
.suspended {
color: $danger;
margin-bottom: 10px;
margin-bottom: 5px;
clear: left;
padding-top: 15px;
}
.card-badge {
img {
max-width: 100px;
max-width: 120px;
}
float: right;
margin-right: 5px;
position: absolute;
right: 12px;
bottom: 12px;
font-size: 30px;
i {color: $secondary;}
}
}

View File

@ -198,17 +198,19 @@
dl dd {
display: inline;
margin: 0 15px 0 5px;
margin: 0 10px 0 0;
padding: 0;
}
dl dt {
display: inline-block;
margin: 0 5px 0 0;
padding: 0;
}
dl {
padding: 10px 15px;
margin: 0;
padding: 8px 10px;
}
dd {
@ -217,6 +219,15 @@
color: $primary;
}
dd.groups {
span:after {
content: ','
}
span:last-of-type:after {
content:''
}
}
dt {
color: dark-light-diff($secondary, $primary, 50%, -40%);
margin: 0;
@ -271,24 +282,28 @@
width: 100%;
position: relative;
float: left;
color: dark-light-diff($secondary, $primary, 75%, 0%);
color: $secondary;
h1 {font-weight: bold;}
.primary-textual {
padding: 3px;
a[href] {
color: dark-light-diff($secondary, $primary, 75%, -10%);
color: $secondary;
}
}
.bio {
color: dark-light-diff($secondary, $primary, 75%, 0%);
color: $secondary;
max-height: 300px;
overflow: auto;
max-width: 750px;
a.mention {
background-color: dark-light-diff($secondary, $primary, 20%, -50%);
}
a[href] {
color: dark-light-diff($secondary, $primary, 75%, 0%);
text-decoration: underline;
@ -304,6 +319,7 @@
.controls {
padding: 0 0 12px 0;
float: right;
width: 180px;
ul {list-style-type: none;}
a {
padding: 5px 10px;
@ -321,6 +337,7 @@
.about.collapsed-info {
.controls {
margin-top: 0;
width: auto;
ul {
li {display: inline;}
a {

View File

@ -10,9 +10,8 @@ body {
.boxed {
height: 100%;
@include border-radius-all(5px);
.contents {
padding: 10px 10px 10px 10px;
padding: 10px;
}
&.white {
background-color: $secondary;

View File

@ -50,6 +50,12 @@
// Search
.searching {
position: absolute;
top: 0;
right: 25px;
}
input[type='text'] {
width: 265px;
font-size: 16px;
@ -75,7 +81,6 @@
font-size: 14px;
}
.d-dropdown .no-results .show-help {
display: none;
.d-dropdown#search-dropdown .heading {
padding: 0;
}

View File

@ -163,3 +163,16 @@
float: none !important;
}
}
#search-help {
max-width: 300px;
}
#search-help h2 {
margin: 0;
font-size: 12px;
}
#search-help p {
margin: 5px;
}

View File

@ -364,10 +364,6 @@ iframe {
display: none;
}
.topic-meta-data {
margin-bottom: 10px;
}
.open>.dropdown-menu {
display: block;
}
@ -470,12 +466,13 @@ span.highlighted {
}
.topic-meta-data {
margin-left: 60px;
white-space: nowrap;
position: absolute;
width: 100%;
width: 80%;
left: 0px;
.names {
margin: 5px 0 0 65px;
margin: 5px 0 0 5px;
line-height: 17px;
span {
display: block;

View File

@ -151,12 +151,11 @@
.user-navigation {
width: 100%;
margin-right: 1.8018%;
margin-top: 20px;
h3 {
color: $primary;
margin: 20px 0 10px 0;
margin: 10px;
}
}
@ -241,7 +240,6 @@
.btn { padding: 3px 12px; }
dl dd {
display: inline;
margin: 0 15px 0 5px;
padding: 0;
}
@ -344,18 +342,16 @@
}
.controls {
padding: 0 0 12px 0;
float: right;
ul {list-style-type: none;}
width: 55%;
float:right;
ul {
list-style-type: none;
margin: 0;
}
a {
padding: 5px 10px;
width: 140px;
margin-bottom: 10px;
}
.right {
float: right;
margin-left: 5px;
}
}
}
@ -411,6 +407,7 @@
.user-stream {
padding: 0 10px;
.end-of-stream {
width: auto;
}
@ -445,8 +442,7 @@
.time, .delete-info {
display: block;
float: right;
color: $primary;
margin-right: 8px;
color: lighten($primary, 40%);
font-size: 11px;
}
.delete-info i {

View File

@ -4,11 +4,13 @@ class Admin::ImpersonateController < Admin::AdminController
params.require(:username_or_email)
user = User.find_by_username_or_email(params[:username_or_email])
raise Discourse::NotFound if user.blank?
guardian.ensure_can_impersonate!(user)
# log impersonate
StaffActionLogger.new(current_user).log_impersonate(user)
# Log on as the user
log_on_user(user)

View File

@ -3,12 +3,17 @@ require_dependency 'report'
class Admin::ReportsController < Admin::AdminController
def show
report_type = params[:type]
raise Discourse::NotFound.new unless report_type =~ /^[a-z0-9\_]+$/
report = Report.find(report_type)
start_date = 1.month.ago
start_date = Time.parse(params[:start_date]) if params[:start_date].present?
end_date = start_date + 1.month
end_date = Time.parse(params[:end_date]) if params[:end_date].present?
report = Report.find(report_type, {start_date: start_date, end_date: end_date})
raise Discourse::NotFound.new if report.blank?
render_json_dump(report: report)

View File

@ -25,8 +25,14 @@ class Admin::UsersController < Admin::AdminController
:revoke_api_key]
def index
query = ::AdminUserIndexQuery.new(params)
render_serialized(query.find_users, AdminUserSerializer)
users = ::AdminUserIndexQuery.new(params).find_users
if params[:show_emails] == "true"
guardian.can_see_emails = true
StaffActionLogger.new(current_user).log_show_emails(users)
end
render_serialized(users, AdminUserSerializer)
end
def show

View File

@ -99,7 +99,7 @@ class CategoriesController < ApplicationController
category_id = params[:category_id].to_i
notification_level = params[:notification_level].to_i
CategoryUser.set_notification_level_for_category(current_user, notification_level , category_id)
CategoryUser.set_notification_level_for_category(current_user, notification_level, category_id)
render json: success_json
end

View File

@ -364,7 +364,9 @@ class TopicsController < ApplicationController
topic_ids = params[:topic_ids].map {|t| t.to_i}
elsif params[:filter] == 'unread'
tq = TopicQuery.new(current_user)
topic_ids = TopicQuery.unread_filter(tq.joined_topic_user).listable_topics.pluck(:id)
topics = TopicQuery.unread_filter(tq.joined_topic_user).listable_topics
topics = topics.where('category_id = ?', params[:category_id]) if params[:category_id]
topic_ids = topics.pluck(:id)
else
raise ActionController::ParameterMissing.new(:topic_ids)
end
@ -439,7 +441,7 @@ class TopicsController < ApplicationController
end
def perform_show_response
topic_view_serializer = TopicViewSerializer.new(@topic_view, scope: guardian, root: false)
topic_view_serializer = TopicViewSerializer.new(@topic_view, scope: guardian, root: false, include_raw: !!params[:include_raw])
respond_to do |format|
format.html do

View File

@ -245,7 +245,7 @@ class UsersController < ApplicationController
activation.finish
# save user email in session, to show on account-created page
session["user_created_email"] = user.email
session["user_created_message"] = activation.message
render json: {
success: true,
@ -364,6 +364,7 @@ class UsersController < ApplicationController
end
def account_created
@message = session['user_created_message']
expires_now
render layout: 'no_js'
end

View File

@ -1,13 +1,5 @@
module UserNotificationsHelper
def self.sanitize_options
return @sanitize_options if @sanitize_options
@sanitize_options = Sanitize::Config::RELAXED.deep_dup
@sanitize_options[:elements] << 'aside' << 'div'
@sanitize_options[:attributes][:all] << 'class'
@sanitize_options
end
def indent(text, by=2)
spacer = " " * by
result = ""
@ -57,21 +49,15 @@ module UserNotificationsHelper
end
def email_excerpt(html, posts_count)
# If there's only one post, include the whole thing.
if posts_count == 1
raw Sanitize.clean(html, UserNotificationsHelper.sanitize_options)
else
# Otherwise, try just the first paragraph.
para = first_paragraph_from(html)
raw Sanitize.clean(para.to_s, UserNotificationsHelper.sanitize_options)
end
# only include 1st paragraph when more than 1 posts
html = first_paragraph_from(html).to_s if posts_count > 1
raw format_for_email(html)
end
def cooked_post_for_email(post)
PrettyText.format_for_email(post.cooked).html_safe
def format_for_email(html)
PrettyText.format_for_email(html).html_safe
end
def email_category(category, opts=nil)
opts = opts || {}

View File

@ -26,7 +26,7 @@ module Jobs
user_data.each do |user|
user_array = Array.new
group_names = get_group_names(user).join(';')
user_array.push(user['id']).push(user['name']).push(user['username']).push(user['email'])
user_array = get_user_fields(user)
user_array.push(group_names) if group_names != ''
data.push(user_array)
end
@ -51,6 +51,22 @@ module Jobs
return group_names
end
def get_user_fields(user)
csv_user_attrs = ['id','name','username','email','created_at']
csv_user_stats_attr = ['topics_entered','posts_read_count','time_read','topic_count','post_count','likes_given','likes_received']
user_array = []
csv_user_attrs.each do |user_attr|
user_array.push(user.attributes[user_attr])
end
csv_user_stats_attr.each do |user_stat_attr|
user_array.push(user.user_stat.attributes[user_stat_attr])
end
return user_array
end
def set_file_path
@file_name = "export_#{SecureRandom.hex(4)}.csv"
# ensure directory exists

View File

@ -250,9 +250,7 @@ class UserNotifications < ActionMailer::Base
)
template = "user_notifications.user_#{notification_type}"
if post.topic.private_message?
template << "_pm"
end
template << "_pm" if post.topic.private_message?
email_opts = {
topic_title: title,

View File

@ -16,19 +16,17 @@ class CategoryUser < ActiveRecord::Base
end
def self.auto_track_new_topic(topic)
apply_default_to_topic(
topic,
TopicUser.notification_levels[:tracking],
TopicUser.notification_reasons[:auto_track_category]
)
apply_default_to_topic(topic,
TopicUser.notification_levels[:tracking],
TopicUser.notification_reasons[:auto_track_category]
)
end
def self.auto_watch_new_topic(topic)
apply_default_to_topic(
topic,
TopicUser.notification_levels[:watching],
TopicUser.notification_reasons[:auto_watch_category]
)
apply_default_to_topic(topic,
TopicUser.notification_levels[:watching],
TopicUser.notification_reasons[:auto_watch_category]
)
end
def self.batch_set(user, level, category_ids)
@ -49,8 +47,6 @@ class CategoryUser < ActiveRecord::Base
def self.set_notification_level_for_category(user, level, category_id)
record = CategoryUser.where(user: user, category_id: category_id).first
# oder CategoryUser.where(user: user, category_id: category_id).destroy_all
# und danach mir create anlegen.
if record.present?
record.notification_level = level
@ -62,23 +58,21 @@ class CategoryUser < ActiveRecord::Base
def self.apply_default_to_topic(topic, level, reason)
# Can not afford to slow down creation of topics when a pile of users are watching new topics, reverting to SQL for max perf here
sql = <<SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT user_id, :topic_id, :level, :reason
FROM category_users
WHERE notification_level = :level AND
category_id = :category_id AND
NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = :topic_id AND user_id = category_users.user_id)
SQL
sql = <<-SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT user_id, :topic_id, :level, :reason
FROM category_users
WHERE notification_level = :level
AND category_id = :category_id
AND NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = :topic_id AND user_id = category_users.user_id)
SQL
exec_sql(
sql,
topic_id: topic.id,
category_id: topic.category_id,
level: level,
reason: reason
)
exec_sql(sql,
topic_id: topic.id,
category_id: topic.category_id,
level: level,
reason: reason
)
end
private_class_method :apply_default_to_topic

View File

@ -13,8 +13,8 @@ class EmailLog < ActiveRecord::Base
User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present? and !skipped
end
def self.count_per_day(sinceDaysAgo = 30)
where('created_at > ? and skipped = false', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
def self.count_per_day(start_date, end_date)
where('created_at >= ? and created_at < ? AND skipped = false', start_date, end_date).group('date(created_at)').order('date(created_at)').count
end
def self.for(reply_key)
@ -22,8 +22,7 @@ class EmailLog < ActiveRecord::Base
end
def self.last_sent_email_address
where(email_type: 'signup').order('created_at DESC')
.first.try(:to_address)
where(email_type: 'signup').order('created_at DESC').first.try(:to_address)
end
end

View File

@ -31,9 +31,15 @@ class OptimizedImage < ActiveRecord::Base
extension = File.extname(original_path)
temp_file = Tempfile.new(["discourse-thumbnail", extension])
temp_path = temp_file.path
original_path += "[0]" unless opts[:allow_animation]
if resize(original_path, temp_path, width, height)
if extension =~ /\.svg$/i
FileUtils.cp(original_path, temp_path)
resized = true
else
resized = resize(original_path, temp_path, width, height, opts[:allow_animation])
end
if resized
thumbnail = OptimizedImage.create!(
upload_id: upload.id,
sha1: Digest::SHA1.file(temp_path).hexdigest,
@ -72,7 +78,8 @@ class OptimizedImage < ActiveRecord::Base
end
end
def self.resize(from, to, width, height)
def self.resize(from, to, width, height, allow_animation=false)
from << "[0]" unless allow_animation
# NOTE: ORDER is important!
instructions = %W{
#{from}

View File

@ -460,8 +460,8 @@ class Post < ActiveRecord::Base
Jobs.enqueue(:process_post, args)
end
def self.public_posts_count_per_day(since_days_ago=30)
public_posts.where('posts.created_at > ?', since_days_ago.days.ago).group('date(posts.created_at)').order('date(posts.created_at)').count
def self.public_posts_count_per_day(start_date, end_date)
public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date).group('date(posts.created_at)').order('date(posts.created_at)').count
end
def self.private_messages_count_per_day(since_days_ago, topic_subtype)

View File

@ -2,39 +2,51 @@ require_dependency 'topic_subtype'
class Report
attr_accessor :type, :data, :total, :prev30Days
attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date
def self.default_days
30
end
def initialize(type)
@type = type
@data = nil
@total = nil
@prev30Days = nil
@start_date ||= 1.month.ago
@end_date ||= Time.now
end
def as_json(options = nil)
{
type: self.type,
title: I18n.t("reports.#{self.type}.title"),
xaxis: I18n.t("reports.#{self.type}.xaxis"),
yaxis: I18n.t("reports.#{self.type}.yaxis"),
data: self.data,
total: self.total,
type: type,
title: I18n.t("reports.#{type}.title"),
xaxis: I18n.t("reports.#{type}.xaxis"),
yaxis: I18n.t("reports.#{type}.yaxis"),
data: data,
total: total,
start_date: start_date,
end_date: end_date,
prev30Days: self.prev30Days
}
end
def self.find(type, opts={})
def self.find(type, opts=nil)
opts ||= {}
report_method = :"report_#{type}"
return nil unless respond_to?(report_method)
# Load the report
report = Report.new(type)
report.start_date = opts[:start_date] if opts[:start_date]
report.end_date = opts[:end_date] if opts[:end_date]
send(report_method, report)
report
end
def self.report_visits(report)
basic_report_about report, UserVisit, :by_day
basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date
end
def self.report_signups(report)
@ -42,15 +54,15 @@ class Report
end
def self.report_topics(report)
basic_report_about report, Topic, :listable_count_per_day
basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date
report.total = Topic.listable_topics.count
report.prev30Days = Topic.listable_topics.where('created_at > ? and created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = Topic.listable_topics.where('created_at > ? and created_at < ?', report.start_date - 30.days, report.start_date).count
end
def self.report_posts(report)
basic_report_about report, Post, :public_posts_count_per_day
basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date
report.total = Post.public_posts.count
report.prev30Days = Post.public_posts.where('posts.created_at > ? and posts.created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = Post.public_posts.where('posts.created_at > ? and posts.created_at < ?', report.start_date - 30.days, report.start_date).count
end
def self.report_emails(report)
@ -58,20 +70,20 @@ class Report
end
def self.report_about(report, subject_class, report_method = :count_per_day)
basic_report_about report, subject_class, report_method
basic_report_about report, subject_class, report_method, report.start_date, report.end_date
add_counts(report, subject_class)
end
def self.basic_report_about(report, subject_class, report_method, *args)
report.data = []
subject_class.send(report_method, 30, *args).each do |date, count|
subject_class.send(report_method, *args).each do |date, count|
report.data << {x: date, y: count}
end
end
def self.add_counts(report, subject_class)
report.total = subject_class.count
report.prev30Days = subject_class.where('created_at > ? and created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = subject_class.where('created_at > ? and created_at < ?', report.start_date - 30.days, report.start_date).count
end
def self.report_users_by_trust_level(report)
@ -82,10 +94,10 @@ class Report
end
def self.report_starred(report)
basic_report_about report, Topic, :starred_counts_per_day
basic_report_about report, Topic, :starred_counts_per_day, default_days
query = TopicUser.where(starred: true)
report.total = query.count
report.prev30Days = query.where('starred_at > ? and starred_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = query.where('starred_at > ? and starred_at < ?', report.start_date - 30.days, report.start_date).count
end
# Post action counts:
@ -102,7 +114,7 @@ class Report
end
flagsQuery = PostAction.where(post_action_type_id: PostActionType.flag_types.values)
report.total = flagsQuery.count
report.prev30Days = flagsQuery.where('created_at > ? and created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = flagsQuery.where('created_at > ? and created_at < ?', report.start_date - 30.days, report.start_date).count
end
def self.report_likes(report)
@ -120,15 +132,15 @@ class Report
end
query = PostAction.unscoped.where(post_action_type_id: post_action_type)
report.total = query.count
report.prev30Days = query.where('created_at > ? and created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = query.where('created_at > ? and created_at < ?', report.start_date - 30.days, report.start_date).count
end
# Private messages counts:
def self.private_messages_report(report, topic_subtype)
basic_report_about report, Post, :private_messages_count_per_day, topic_subtype
basic_report_about report, Post, :private_messages_count_per_day, default_days, topic_subtype
report.total = Post.private_posts.with_topic_subtype(topic_subtype).count
report.prev30Days = Post.private_posts.with_topic_subtype(topic_subtype).where('posts.created_at > ? and posts.created_at < ?', 60.days.ago, 30.days.ago).count
report.prev30Days = Post.private_posts.with_topic_subtype(topic_subtype).where('posts.created_at > ? and posts.created_at < ?', report.start_date - 30.days, report.start_date).count
end
def self.report_user_to_user_private_messages(report)

View File

@ -345,8 +345,8 @@ class Topic < ActiveRecord::Base
custom_fields[key.to_s]
end
def self.listable_count_per_day(sinceDaysAgo=30)
listable_topics.where('created_at > ?', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
def self.listable_count_per_day(start_date, end_date)
listable_topics.where('created_at >= ? and created_at < ?', start_date, end_date).group('date(created_at)').order('date(created_at)').count
end
def private_message?
@ -421,9 +421,9 @@ class Topic < ActiveRecord::Base
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
END,
seen_post_count = CASE
WHEN seen_post_count > :highest THEN :highest
ELSE seen_post_count
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
END
WHERE topic_id = :topic_id",
highest: highest_post_number,

View File

@ -152,8 +152,8 @@ class TopicUser < ActiveRecord::Base
threshold: SiteSetting.auto_track_topics_after
}
# In case anyone seens "seen_post_count" and gets confused, like I do.
# seen_post_count represents the highest_post_number of the topic when
# In case anyone seens "highest_seen_post_number" and gets confused, like I do.
# highest_seen_post_number represents the highest_post_number of the topic when
# the user visited it. It may be out of alignment with last_read, meaning
# ... user visited the topic but did not read the posts
#
@ -161,7 +161,7 @@ class TopicUser < ActiveRecord::Base
rows = exec_sql("UPDATE topic_users
SET
last_read_post_number = GREATEST(:post_number, tu.last_read_post_number),
seen_post_count = t.highest_post_number,
highest_seen_post_number = t.highest_post_number,
total_msecs_viewed = LEAST(tu.total_msecs_viewed + :msecs,86400000),
notification_level =
case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) >
@ -210,7 +210,7 @@ class TopicUser < ActiveRecord::Base
TopicTrackingState.publish_read(topic_id, post_number, user.id, args[:new_status])
user.update_posts_read!(post_number)
exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, seen_post_count, last_visited_at, first_visited_at, notification_level)
exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, highest_seen_post_number, last_visited_at, first_visited_at, notification_level)
SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, :new_status
FROM topics AS ft
JOIN users u on u.id = :user_id
@ -241,7 +241,7 @@ class TopicUser < ActiveRecord::Base
UPDATE topic_users t
SET
last_read_post_number = LEAST(GREATEST(last_read, last_read_post_number), max_post_number),
seen_post_count = LEAST(max_post_number,GREATEST(t.seen_post_count, last_read))
highest_seen_post_number = LEAST(max_post_number,GREATEST(t.highest_seen_post_number, last_read))
FROM (
SELECT topic_id, user_id, MAX(post_number) last_read
FROM post_timings
@ -259,7 +259,7 @@ X.topic_id = t.topic_id AND
X.user_id = t.user_id AND
(
last_read_post_number <> LEAST(GREATEST(last_read, last_read_post_number), max_post_number) OR
seen_post_count <> LEAST(max_post_number,GREATEST(t.seen_post_count, last_read))
highest_seen_post_number <> LEAST(max_post_number,GREATEST(t.highest_seen_post_number, last_read))
)
SQL
@ -281,7 +281,7 @@ end
# starred :boolean default(FALSE), not null
# posted :boolean default(FALSE), not null
# last_read_post_number :integer
# seen_post_count :integer
# highest_seen_post_number :integer
# starred_at :datetime
# last_visited_at :datetime
# first_visited_at :datetime

View File

@ -75,13 +75,24 @@ class Upload < ActiveRecord::Base
# deal with width & height for images
if FileHelper.is_image?(filename)
begin
# fix orientation first
Upload.fix_image_orientation(file.path)
# retrieve image info
image_info = FastImage.new(file, raise_on_failure: true)
# compute image aspect ratio
upload.width, upload.height = ImageSizer.resize(*image_info.size)
# make sure we're at the beginning of the file (FastImage moves the pointer)
if filename =~ /\.svg$/i
svg = Nokogiri::XML(file).at_css("svg")
width, height = svg["width"].to_i, svg["height"].to_i
if width == 0 || height == 0
upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
else
upload.width, upload.height = ImageSizer.resize(width, height)
end
else
# fix orientation first
Upload.fix_image_orientation(file.path)
# retrieve image info
image_info = FastImage.new(file, raise_on_failure: true)
# compute image aspect ratio
upload.width, upload.height = ImageSizer.resize(*image_info.size)
end
# make sure we're at the beginning of the file
# (FastImage and Nokogiri move the pointer)
file.rewind
rescue FastImage::ImageFetchFailure
upload.errors.add(:base, I18n.t("upload.images.fetch_failure"))

View File

@ -531,8 +531,8 @@ class User < ActiveRecord::Base
.limit(3)
end
def self.count_by_signup_date(sinceDaysAgo=30)
where('created_at > ?', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
def self.count_by_signup_date(start_date, end_date)
where('created_at >= ? and created_at < ?', start_date, end_date).group('date(created_at)').order('date(created_at)').count
end

View File

@ -32,7 +32,8 @@ class UserHistory < ActiveRecord::Base
:auto_trust_level_change,
:check_email,
:delete_post,
:delete_topic)
:delete_topic,
:impersonate)
end
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
@ -48,7 +49,8 @@ class UserHistory < ActiveRecord::Base
:revoke_badge,
:check_email,
:delete_post,
:delete_topic]
:delete_topic,
:impersonate]
end
def self.staff_action_ids

View File

@ -1,8 +1,8 @@
class UserVisit < ActiveRecord::Base
# A count of visits in the last month by day
def self.by_day(sinceDaysAgo=30)
where("visited_at >= ?", sinceDaysAgo.days.ago).group(:visited_at).order(:visited_at).count
def self.by_day(start_date, end_date)
where("visited_at >= ? and visited_at < ?", start_date, end_date).group(:visited_at).order(:visited_at).count
end
def self.ensure_consistency!

View File

@ -39,7 +39,7 @@ class AdminUserSerializer < BasicUserSerializer
def include_email?
# staff members can always see their email
scope.is_staff? && object.id == scope.user.id
(scope.is_staff? && object.id == scope.user.id) || scope.can_see_emails?
end
alias_method :include_associated_accounts?, :include_email?

View File

@ -21,6 +21,7 @@ module PostStreamSerializerMixin
object.posts.each_with_index do |p, idx|
highest_number_in_posts = p.post_number if p.post_number > highest_number_in_posts
ps = PostSerializer.new(p, scope: scope, root: false)
ps.add_raw = true if @options[:include_raw]
ps.topic_view = object
p.topic = object.topic

View File

@ -142,10 +142,32 @@ class StaffActionLogger
}))
end
def log_show_emails(users)
values = []
users.each do |user|
values << "(#{@admin.id}, #{UserHistory.actions[:check_email]}, #{user.id}, current_timestamp, current_timestamp)"
end
# bulk insert
UserHistory.exec_sql <<-SQL
INSERT INTO user_histories (acting_user_id, action, target_user_id, created_at, updated_at)
VALUES #{values.join(",")}
SQL
end
def log_impersonate(user, opts={})
raise Discourse::InvalidParameters.new("user is nil") unless user
UserHistory.create(params(opts).merge({
action: UserHistory.actions[:impersonate],
target_user_id: user.id
}))
end
private
def params(opts)
{acting_user_id: @admin.id, context: opts[:context]}
{ acting_user_id: @admin.id, context: opts[:context] }
end
end

View File

@ -10,7 +10,7 @@
</td>
</tr>
<tr>
<td class='body'><%= cooked_post_for_email(post) %></td>
<td class='body'><%= format_for_email(post.cooked) %></td>
</tr>
</tbody>
</table>

View File

@ -10,10 +10,7 @@
<%= raw(@markdown_linker.create(t.title, t.relative_url)) %>
<%- if t.best_post.present? %>
<%= raw(t.best_post.excerpt(1000,
strip_links: true,
text_entities: true,
markdown_images: true)) %>
<%= raw(t.best_post.excerpt(1000, strip_links: true, text_entities: true, markdown_images: true)) %>
--------------------------------------------------------------------------------
<%- end %>

View File

@ -2,6 +2,6 @@
(function() {
setTimeout(function() {
window.location.href = '/';
}, 5000);
}, 2000);
})();
</script>

View File

@ -1,5 +1,3 @@
<div id='simple-container'>
<% if session["user_created_email"] %>
<span style="font-size: 16px; line-height: 24px;"><%= t('login.activate_email', email: session["user_created_email"]).html_safe %></span>
<% end %>
<span style="font-size: 16px; line-height: 24px;"><%= @message.html_safe %></span>
</div>

View File

@ -359,7 +359,6 @@ cs:
frequency: "Budeme vás informovat emailem pouze pokud jste se na našem webu dlouho neukázali a pokud jste obsah, o kterém vás chceme informovat, doposud neviděli."
name:
title: "Jméno"
instructions: "Vaše celé jméno."
too_short: "Vaše jméno je příliš krátké."
ok: "Vaše jméno vypadá dobře"
username:
@ -387,6 +386,8 @@ cs:
created: "Účet vytvořen"
log_out: "Odhlásit se"
location: "Lokace"
card_badge:
title: "User Card Badge"
website: "Webová stránka"
email_settings: "Emailová upozornění"
email_digests:
@ -589,6 +590,7 @@ cs:
drafts_offline: "koncepty offline"
min_length:
need_more_for_title: "ještě {{n}} znaků nadpisu tématu"
need_more_for_reply: "ještě {{n}} znaků"
error:
title_missing: "Název musí být vyplněn"
title_too_short: "Název musí být dlouhý alespoň {{min}} znaků"
@ -612,6 +614,7 @@ cs:
view_new_post: "Zobrazit váš nový příspěvek."
saving: "Ukládám..."
saved: "Uloženo!"
saved_draft: "Máte rozepsaný příspěvek. Klikněte pro obnovení."
uploading: "Nahrávám..."
show_preview: 'zobrazit náhled &raquo;'
hide_preview: '&laquo; skrýt náhled'
@ -906,7 +909,6 @@ cs:
one: "Je zobrazen pouze 1 příspěvek"
few: "Jsou zobrazeny pouze {{count}} příspěvky"
other: "Je zobrazeno pouze {{count}} příspěvků"
cancel: "Zobrazí znovu všechny příspěvky v tomto tématu."
split_topic:
title: "Rozdělit téma"
action: "do nového téma"
@ -1375,8 +1377,6 @@ cs:
7_days_ago: "Týden"
30_days_ago: "Měsíc"
all: "Celkem"
view_table: "Zobrazit jako tabulku"
view_chart: "Zobrazit jako sloupcový graf"
commits:
latest_changes: "Poslední změny:"
by: "od"
@ -1883,6 +1883,7 @@ cs:
star: '<b>f</b> Star topic'
share_topic: '<b>shift s</b> Sdílet téma'
share_post: '<b>s</b> Share post'
reply_as_new_topic: '<b>t</b> Odpovědět v propojeném tématu'
reply_topic: '<b>shift r</b> Odpovědět na téma'
reply_post: '<b>r</b> Odpovědět na příspěvek'
quote_post: '<b>q</b> Citovat příspěvek'
@ -1910,6 +1911,7 @@ cs:
few: "1 udělen"
other: "%{count} uděleno"
select_badge_for_title: Vyberte odznak, který chcete použít jako svůj titul
none: "<none>"
badge_grouping:
other:
name: Ostatní

View File

@ -824,7 +824,6 @@ da:
n_posts:
one: "1 indlæg"
other: "{{count}} indlæg"
cancel: "Se alle indlæg i emnet."
split_topic:
title: "Flyt til nyt emne"
action: "flyt til nyt emne"
@ -1263,8 +1262,6 @@ da:
7_days_ago: "7 dage siden"
30_days_ago: "30 dage siden"
all: "Alle"
view_table: "Vis som tabel"
view_chart: "Vis som søjlediagram"
commits:
latest_changes: "Seneste ændringer: opdatér ofte!"
by: "af"

View File

@ -277,6 +277,7 @@ de:
admin_tooltip: "Dieser Benutzer ist ein Administrator"
suspended_notice: "Dieser Benutzer ist bis zum {{date}} gesperrt."
suspended_reason: "Grund: "
github_profile: "Github"
mailing_list_mode: "Erhalte bei jedem neuen Beitrag eine E-Mail (sofern du das Thema oder die Kategorie nicht stummgeschaltet hast)"
watched_categories: "Beobachtet"
watched_categories_instructions: "Du wirst in diesen Kategorien automatisch alle neuen Themen beobachten. Du wirst über alle neuen Beiträge und Themen benachrichtigt werden. Außerdem wird die Anzahl der ungelesenen und neuen Beiträge neben dem Eintrag in der Themenliste angezeigt."
@ -343,7 +344,7 @@ de:
frequency: "Wir werden dir nur E-Mails senden, wenn wir dich kürzlich nicht gesehen haben und wenn du die betroffenen Inhalte noch nicht gesehen hast."
name:
title: "Name"
instructions: "Dein vollständiger Name."
instructions: "Ihr vollständiger Name (optional)."
too_short: "Dein Name ist zu kurz."
ok: "Dein Name schaut gut aus."
username:
@ -511,8 +512,7 @@ de:
created: 'Erstellt'
created_lowercase: 'erstellt'
trust_level: 'Vertrauensstufe'
search_hint: 'Benutzername oder IP-Adresse'
search_hint_admin: 'Benutzername, E-Mail- oder IP-Adresse'
search_hint: 'Benutzername, E-Mail- oder IP-Adresse'
create_account:
title: "Neues Benutzerkonto erstellen"
failed: "Etwas ist fehlgeschlagen. Vielleicht ist diese E-Mail-Adresse bereits registriert. Versuche den 'Passwort vergessen'-Link."
@ -712,6 +712,8 @@ de:
hot: "Es gibt keine beliebten Themen."
category: "Es gibt keine Themen in {{category}}."
top: "Es gibt keine Top-Themen."
educate:
new: '<p>Hier werden neue Themen angezeigt.</p><p>Standardmäßig werden jene Themen als neu angesehen und mit dem <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">neu</span> Indikator versehen, die in den letzten 2 Tagen erstellt wurden.</p><p>Du kannst das in deinen <a href="%{userPrefsUrl}">Einstellungen</a> ändern.</p>'
bottom:
latest: "Das waren die aktuellen Themen."
hot: "Das waren alle beliebten Themen."
@ -881,7 +883,7 @@ de:
n_posts:
one: "1 Beitrag"
other: "{{count}} Beiträge"
cancel: "Zeige in diesem Thema wieder alle Beiträge an."
cancel: "Filter entfernen"
split_topic:
title: "In neues Thema verschieben"
action: "in ein neues Thema verschieben"
@ -1181,6 +1183,9 @@ de:
submit_tooltip: "Private Meldung abschicken"
take_action_tooltip: "Den Meldungsschwellenwert sofort erreichen, anstatt auf weitere Meldungen aus der Community zu warten."
cant: "Entschuldige, Du kannst diesen Beitrag augenblicklich nicht melden."
formatted_name:
inappropriate: "Es ist unangemessen"
spam: "Es ist Spam"
custom_placeholder_notify_user: "Weshalb erfordert der Beitrag, dass du den Benutzer direkt und privat kontaktieren möchtest? Sei spezifisch, konstruktiv und immer freundlich."
custom_placeholder_notify_moderators: "Warum soll ein Moderator sich diesen Beitrag ansehen? Bitte lass uns wissen, was genau Dich beunruhigt, und wenn möglich dafür relevante Links."
custom_message:
@ -1239,6 +1244,7 @@ de:
history: "Verlauf"
changed_by: "von {{author}}"
raw_email:
title: "Unverarbeitete E-Mail"
not_available: "Nicht verfügbar!"
categories_list: "Liste der Kategorien"
filters:
@ -1346,8 +1352,11 @@ de:
7_days_ago: "vor 7 Tagen"
30_days_ago: "vor 30 Tagen"
all: "alle"
view_table: "Als Tabelle anzeigen"
view_chart: "Als Balkendiagramm anzeigen"
view_table: "Tabelle"
view_chart: "Säulendiagramm"
refresh_report: "Bericht aktualisieren"
start_date: "Startdatum"
end_date: "Enddatum"
commits:
latest_changes: "Letzte Änderungen: bitte häufig updaten!"
by: "von"
@ -1684,6 +1693,7 @@ de:
last_emailed: "Letzte E-Mail"
not_found: "Entschuldige, dieser Benutzername existiert im System nicht."
active: "Aktiv"
show_emails: "E-Mails anzeigen"
nav:
new: "Neu"
active: "Aktiv"

View File

@ -310,6 +310,7 @@ en:
admin_tooltip: "This user is an admin"
suspended_notice: "This user is suspended until {{date}}."
suspended_reason: "Reason: "
github_profile: "Github"
mailing_list_mode: "Receive an email for every new post (unless you mute the topic or category)"
watched_categories: "Watched"
watched_categories_instructions: "You will automatically watch all new topics in these categories. 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's listing."
@ -380,7 +381,7 @@ en:
email:
title: "Email"
instructions: "Never shown to the public."
ok: "Looks good. We will email you to confirm."
ok: "We will email you to confirm."
invalid: "Please enter a valid email address."
authenticated: "Your email has been authenticated by {{provider}}."
frequency: "We'll only email you if we haven't seen you recently and you haven't already seen the thing we're emailing you about."
@ -1003,7 +1004,7 @@ en:
n_posts:
one: "1 post"
other: "{{count}} posts"
cancel: "Show all posts in this topic again."
cancel: "Remove filter"
split_topic:
title: "Move to New Topic"
@ -1323,6 +1324,10 @@ en:
submit_tooltip: "Submit the private flag"
take_action_tooltip: "Reach the flag threshold immediately, rather than waiting for more community flags"
cant: "Sorry, you can't flag this post at this time."
formatted_name:
off_topic: "It's Off-Topic"
inappropriate: "It's Inappropriate"
spam: "It's Spam"
custom_placeholder_notify_user: "Why does this post require you to speak to this user directly and privately? Be specific, be constructive, and always be kind."
custom_placeholder_notify_moderators: "Why does this post require moderator attention? Let us know specifically what you are concerned about, and provide relevant links where possible."
custom_message:
@ -1507,8 +1512,11 @@ en:
7_days_ago: "7 Days Ago"
30_days_ago: "30 Days Ago"
all: "All"
view_table: "View as Table"
view_chart: "View as Bar Chart"
view_table: "table"
view_chart: "bar chart"
refresh_report: "Refresh Report"
start_date: "Start Date"
end_date: "End Date"
commits:
latest_changes: "Latest changes: please update often!"
@ -1825,6 +1833,7 @@ en:
check_email: "check email"
delete_topic: "delete topic"
delete_post: "delete post"
impersonate: "impersonate"
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."
@ -1861,6 +1870,7 @@ en:
last_emailed: "Last Emailed"
not_found: "Sorry, that username doesn't exist in our system."
active: "Active"
show_emails: "Show Emails"
nav:
new: "New"
active: "Active"

View File

@ -343,7 +343,6 @@ es:
frequency: "Sólo te enviaremos e-mails si no te hemos visto recientemente y todavía no has visto lo que te estamos enviando."
name:
title: "Nombre"
instructions: "Tu nombre completo."
too_short: "Tu nombre es demasiado corto."
ok: "Tu nombre es válido."
username:
@ -511,8 +510,6 @@ es:
created: 'Creado'
created_lowercase: 'creado'
trust_level: 'Nivel de Confianza'
search_hint: 'nombre de usuario o dirección IP'
search_hint_admin: 'nombre de usuario, e-mail o dirección IP'
create_account:
title: "Crear nueva cuenta"
failed: "Algo ha salido mal, tal vez este e-mail ya fue registrado, intenta con el enlace 'olvidé la contraseña'"
@ -887,7 +884,6 @@ es:
n_posts:
one: "1 post"
other: "{{count}} posts"
cancel: "Mostrar de nuevo todos los posts de este tema."
split_topic:
title: "Mover a un tema nuevo"
action: "mover a un tema nuevo"
@ -1353,8 +1349,6 @@ es:
7_days_ago: "Hace 7 días"
30_days_ago: "Hace 30 días"
all: "Todo"
view_table: "Ver como tabla"
view_chart: "Ver como gráfico de tablas"
commits:
latest_changes: "Cambios recientes: ¡actualiza a menudo!"
by: "por"

View File

@ -104,6 +104,7 @@ fi:
admin_title: "Ylläpito"
flags_title: "Liput"
show_more: "näytä lisää"
show_help: "ohje"
links: "Linkit"
links_lowercase: "linkit"
faq: "UKK"
@ -138,6 +139,10 @@ fi:
stat:
all_time: "Kaikki"
last_7_days: "Viimeiset 7 päivää"
like_count: "Tykkäyksiä"
topic_count: "Ketjuja"
post_count: "Viestejä"
user_count: "Käyttäjiä"
bookmarks:
not_logged_in: "pahoittelut, sinun täytyy kirjautua sisään voidaksesi lisätä viestin kirjanmerkin"
created: "olet lisännyt tämän viestin kirjainmerkkeihisi"
@ -325,6 +330,10 @@ fi:
image_is_not_a_square: "Varoitus: olemme rajanneet kuvaasti; se ei ole neliön muotoinen."
change_profile_background:
title: "Profiilin taustakuva"
instructions: "Profiilin taustakuvan leveys on 850 pikseliä."
change_card_background:
title: "Käyttäjäkortin taustakuva"
instructions: "Taustakuvan leveys on 590 pikseliä."
email:
title: "Sähköposti"
instructions: "Ei tule julkiseksi."
@ -334,7 +343,7 @@ fi:
frequency: "Lähetämme sähköpostia vain aiheista, joita et ole nähnyt, ja kun emme ole nähneet sinua viime aikoina."
name:
title: "Nimi"
instructions: "Koko nimesi."
instructions: "Koko nimesi (valinnainen)"
too_short: "Nimesi on liian lyhyt."
ok: "Nimesi vaikuttaa hyvältä."
username:
@ -362,6 +371,8 @@ fi:
created: "Liittynyt"
log_out: "Kirjaudu ulos"
location: "Sijainti"
card_badge:
title: "Käyttäjäkortin tunnus"
website: "Nettisivu"
email_settings: "Sähköposti"
email_digests:
@ -500,6 +511,7 @@ fi:
created: 'Luotu'
created_lowercase: 'luotu'
trust_level: 'Luottamustaso'
search_hint: 'käyttäjätunnus, sähköposti tai IP-osoite'
create_account:
title: "Luo uusi tunnus"
failed: "Jotain meni pieleen. Ehkäpä tämä sähköpostiosoite on jo rekisteröity, kokeile salasana unohtui -linkkiä."
@ -585,6 +597,7 @@ fi:
view_new_post: "Katsele uutta viestiäsi."
saving: "Tallennetaan..."
saved: "Tallennettu!"
saved_draft: "Viestiluonnos kesken. Klikkaa tähän jatkaaksesi."
uploading: "Lähettää..."
show_preview: 'näytä esikatselu &raquo;'
hide_preview: '&laquo; piilota esikatselu'
@ -873,7 +886,6 @@ fi:
n_posts:
one: "1 viesti"
other: "{{count}} viestiä"
cancel: "Näytä tämän ketjun kaikki viestit."
split_topic:
title: "Siirrä uuteen ketjuun"
action: "siirrä uuteen ketjuun"
@ -1231,6 +1243,7 @@ fi:
history: "Historia"
changed_by: "käyttäjältä {{author}}"
raw_email:
title: "Alkuperäinen sähköposti"
not_available: "Ei käytettävissä!"
categories_list: "Lista alueista"
filters:
@ -1338,8 +1351,6 @@ fi:
7_days_ago: "7 päivää sitten"
30_days_ago: "30 päivää sitten"
all: "Kaikki"
view_table: "Näytä taulukkona"
view_chart: "Katsele pylväsdiagrammina"
commits:
latest_changes: "Viimeisimmät muutokset: päivitä usein!"
by: "käyttäjältä"
@ -1858,7 +1869,7 @@ fi:
confirm: 'Vahvistus'
site_text:
none: "Valitse sisällön tyyppi aloittaaksesi muokkaamisen."
title: 'Tekstin sisältö'
title: 'Tekstit'
site_settings:
show_overriden: 'Näytä vain muokatut'
title: 'Asetukset'
@ -1911,11 +1922,14 @@ fi:
grant: Myönnä
no_user_badges: "%{name} ei ole saanut yhtään arvomerkkiä."
no_badges: Myönnettäviä arvomerkkejä ei ole.
none_selected: "Valitse arvomerkki aloittaaksesi"
allow_title: Salli arvomerkin käyttäminen tittelinä
multiple_grant: Voidaan myöntää useita kertoja
listable: Näytä arvomerkki julkisella arvomerkkisivulla
enabled: Ota arvomerkki käyttöön
icon: Ikoni
image: Kuva
icon_help: "Käytä joko Font Awesome -luokkaa tai kuvan URL-osoitetta"
query: Arvomerkkien haku tietokannasta (SQL)
target_posts: Tietokantakyselyn kohdeviestit
auto_revoke: Aja kumoamis-ajo päivittäin
@ -1948,6 +1962,8 @@ fi:
with_time: <span class="username">%{username}</span> <span class="time">%{time}</span>
lightbox:
download: "lataa"
search_help:
title: 'Etsi ohjetta'
keyboard_shortcuts_help:
title: 'Näppäinoikotiet'
jump_to:
@ -2009,6 +2025,7 @@ fi:
one: "1 myönnetty"
other: "%{count} myönnettyä"
select_badge_for_title: Valitse tittelisi arvomerkeistä
none: "<ei mitään>"
badge_grouping:
getting_started:
name: Ensiaskeleet
@ -2090,3 +2107,12 @@ fi:
reader:
name: Lukija
description: Luki kaikki viestit ketjusta, jossa on yli 100 viestiä
google_search: |
<h2>Etsi Googlella</h2>
<p>
<form action='//google.com/search' id='google-search' onsubmit="document.getElementById('google-query').value = 'site:' + window.location.host + ' ' + document.getElementById('user-query').value; return true;">
<input type="text" id='user-query' value="">
<input type='hidden' id='google-query' name="q">
<button class="btn btn-primary">Google</button>
</form>
</p>

Some files were not shown because too many files have changed in this diff Show More