Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2016-03-28 15:16:51 +11:00
commit 4da9a434fd
69 changed files with 852 additions and 714 deletions

3
.gitignore vendored
View File

@ -58,6 +58,9 @@ log/
# Ignore Eclipse .buildpath file # Ignore Eclipse .buildpath file
/.buildpath /.buildpath
# Ignore byebug history
/.byebug_history
# Ignore RubyMine settings # Ignore RubyMine settings
/.idea /.idea

View File

@ -1,11 +1,21 @@
import IncomingEmail from 'admin/models/incoming-email'; import IncomingEmail from 'admin/models/incoming-email';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
loadMore() { loading: false,
return IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
.then(incoming => { actions: {
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(incoming); loadMore() {
}); if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set('loading', true);
IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
.then(incoming => {
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(incoming);
}).finally(() => {
this.set('loading', false);
});
}
} }
}); });

View File

@ -1,11 +1,20 @@
import EmailLog from 'admin/models/email-log'; import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
loadMore() { loading: false,
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
.then(logs => { actions: {
if (logs.length < 50) { this.get("model").set("allLoaded", true); } loadMore() {
this.get("model").addObjects(logs); if (this.get("loading") || this.get("model.allLoaded")) { return; }
});
this.set('loading', true);
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
.then(logs => {
if (logs.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(logs);
}).finally(() => {
this.set('loading', false);
});
}
} }
}); });

View File

@ -1,55 +1,57 @@
<table class='table email-list'> {{#load-more selector=".email-list tr" action="loadMore"}}
<thead> <table class='table email-list'>
<tr> <thead>
<th>{{i18n 'admin.email.time'}}</th> <tr>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th> <th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th> <th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th> <th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
</tr> </tr>
</thead>
<tr class="filters"> {{#each email in model}}
<td>{{i18n 'admin.email.logs.filters.title'}}</td> <tr>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td> <td class="time">{{format-date email.created_at}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td> <td class="username">
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td> <div>
</tr> {{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{#each email in model}} {{avatar email.user imageSize="tiny"}}
<tr> {{email.from_address}}
<td class="time">{{format-date email.created_at}}</td> {{/link-to}}
<td class="username"> {{else}}
<div> &mdash;
{{#if email.user}} {{/if}}
{{#link-to 'adminUser' email.user}} </div>
{{avatar email.user imageSize="tiny"}} </td>
{{email.from_address}} <td class="addresses">
{{/link-to}} {{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>
{{#if email.post_url}}
<a href="{{email.post_url}}">{{email.subject}}</a>
{{else}} {{else}}
&mdash; {{email.subject}}
{{/if}} {{/if}}
</div> </td>
</td> </tr>
<td class="addresses"> {{else}}
{{#each to in email.to_addresses}} <tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p> {{/each}}
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>
{{#if email.post_url}}
<a href="{{email.post_url}}">{{email.subject}}</a>
{{else}}
{{email.subject}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table> </table>
{{/load-more}}
{{conditional-loading-spinner condition=view.loading}} {{conditional-loading-spinner condition=loading}}

View File

@ -1,54 +1,56 @@
<table class='table email-list'> {{#load-more selector=".email-list tr" action="loadMore"}}
<thead> <table class='table email-list'>
<tr> <thead>
<th>{{i18n 'admin.email.time'}}</th> <tr>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th> <th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th> <th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th> <th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.error'}}</th> <th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
<th>{{i18n 'admin.email.incoming_emails.error'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
</tr> </tr>
</thead>
<tr class="filters"> {{#each email in model}}
<td>{{i18n 'admin.email.logs.filters.title'}}</td> <tr>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td> <td class="time">{{format-date email.created_at}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td> <td class="username">
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td> <div>
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td> {{#if email.user}}
</tr> {{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>{{email.subject}}</td>
<td class="error">
<a {{action "showIncomingEmail" email.id}}>{{email.error}}</a>
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
{{#each email in model}} </table>
<tr> {{/load-more}}
<td class="time">{{format-date email.created_at}}</td>
<td class="username">
<div>
{{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>{{email.subject}}</td>
<td class="error">
<a {{action "showIncomingEmail" email.id}}>{{email.error}}</a>
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table> {{conditional-loading-spinner condition=loading}}
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,47 +1,49 @@
<table class='table email-list'> {{#load-more selector=".email-list tr" action="loadMore"}}
<thead> <table class='table email-list'>
<tr> <thead>
<th>{{i18n 'admin.email.sent_at'}}</th> <tr>
<th>{{i18n 'admin.email.user'}}</th> <th>{{i18n 'admin.email.sent_at'}}</th>
<th>{{i18n 'admin.email.to_address'}}</th> <th>{{i18n 'admin.email.user'}}</th>
<th>{{i18n 'admin.email.email_type'}}</th> <th>{{i18n 'admin.email.to_address'}}</th>
<th>{{i18n 'admin.email.reply_key'}}</th> <th>{{i18n 'admin.email.email_type'}}</th>
<th>{{i18n 'admin.email.reply_key'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
<td>{{text-field value=filter.reply_key placeholderKey="admin.email.logs.filters.reply_key_placeholder"}}</td>
</tr> </tr>
</thead>
<tr class="filters"> {{#each l in model}}
<td>{{i18n 'admin.email.logs.filters.title'}}</td> <tr>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td> <td>{{format-date l.created_at}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td> <td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td> {{#if l.user}}
<td>{{text-field value=filter.reply_key placeholderKey="admin.email.logs.filters.reply_key_placeholder"}}</td> {{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
</tr> {{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>
{{#if l.post_url}}
<a href="{{l.post_url}}">{{l.reply_key}}</a>
{{else}}
{{l.reply_key}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
{{#each l in model}} </table>
<tr> {{/load-more}}
<td>{{format-date l.created_at}}</td>
<td>
{{#if l.user}}
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>
{{#if l.post_url}}
<a href="{{l.post_url}}">{{l.reply_key}}</a>
{{else}}
{{l.reply_key}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
</table> {{conditional-loading-spinner condition=loading}}
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,47 +1,49 @@
<table class='table email-list'> {{#load-more selector=".email-list tr" action="loadMore"}}
<thead> <table class='table email-list'>
<tr> <thead>
<th>{{i18n 'admin.email.time'}}</th> <tr>
<th>{{i18n 'admin.email.user'}}</th> <th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.to_address'}}</th> <th>{{i18n 'admin.email.user'}}</th>
<th>{{i18n 'admin.email.email_type'}}</th> <th>{{i18n 'admin.email.to_address'}}</th>
<th>{{i18n 'admin.email.skipped_reason'}}</th> <th>{{i18n 'admin.email.email_type'}}</th>
<th>{{i18n 'admin.email.skipped_reason'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
</tr> </tr>
</thead>
<tr class="filters"> {{#each l in model}}
<td>{{i18n 'admin.email.logs.filters.title'}}</td> <tr>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td> <td>{{format-date l.created_at}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td> <td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td> {{#if l.user}}
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td> {{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
</tr> {{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>
{{#if l.post_url}}
<a href="{{l.post_url}}">{{l.skipped_reason}}</a>
{{else}}
{{l.skipped_reason}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
{{#each l in model}} </table>
<tr> {{/load-more}}
<td>{{format-date l.created_at}}</td>
<td>
{{#if l.user}}
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>
{{#if l.post_url}}
<a href="{{l.post_url}}">{{l.skipped_reason}}</a>
{{else}}
{{l.skipped_reason}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
</table> {{conditional-loading-spinner condition=loading}}
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,14 +0,0 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -1,14 +0,0 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -1,5 +0,0 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-received"
});

View File

@ -1,5 +0,0 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-rejected"
});

View File

@ -1,5 +0,0 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-sent"
});

View File

@ -1,5 +0,0 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-skipped"
});

View File

@ -0,0 +1,23 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
size: 'medium',
classNameBindings: [':badge-card', 'size'],
@computed('count', 'badge.grant_count')
displayCount(count, grantCount) {
const c = parseInt(count || grantCount || 0);
if (c > 1) {
return c;
}
},
@computed('size')
summary(size) {
if (size === 'large') {
return Discourse.Emoji.unescape(this.get('badge.long_description') || '');
}
return this.get('badge.displayDescriptionHtml');
}
});

View File

@ -0,0 +1,16 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
tagName: 'span',
classNameBindings: [':check-display', 'status'],
@computed('checked')
status(checked) {
return checked ? 'status-checked' : 'status-unchecked';
},
render(buffer) {
const icon = this.get('checked') ? 'check' : 'times';
buffer.push(`<i class='fa fa-${icon}'></i>`);
}
});

View File

@ -27,9 +27,9 @@ export default Ember.Component.extend(StringBuffer, {
if (notices.length > 0) { if (notices.length > 0) {
buffer.push(_.map(notices, n => { buffer.push(_.map(notices, n => {
var html = `<div class='row'><div class='alert alert-info ${n[1]}'>${n[0]}`; var html = `<div class='row'><div class='alert alert-info ${n[1]}'>`;
if (n[2]) html += n[2]; if (n[2]) html += n[2];
html += '</div></div>'; html += `${n[0]}</div></div>`;
return html; return html;
}).join("")); }).join(""));
} }

View File

@ -0,0 +1,16 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.Component.extend(LoadMore, {
_viaComponent: true,
init() {
this._super();
this.set('eyelineSelector', this.get('selector'));
},
actions: {
loadMore() {
this.sendAction();
}
}
});

View File

@ -0,0 +1,23 @@
import { url } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
function normalize(name) {
return name.replace(/[\-\_ \.]/g, '').toLowerCase();
}
export default Ember.Component.extend({
classNameBindings: [':user-info', 'size'],
size: 'small',
userPath: url('user.username', '/users/%@'),
// TODO: In later ember releases `hasBlock` works without this
hasBlock: Ember.computed.alias('template'),
@computed('user.name', 'user.username')
name(name, username) {
if (name && normalize(username) !== normalize(name)) {
return name;
}
}
});

View File

@ -1,15 +0,0 @@
import { url } from 'discourse/lib/computed';
export default Ember.Component.extend({
classNames: ['user-small'],
userPath: url('user.username', '/users/%@'),
name: function() {
const name = this.get('user.name');
if (name && this.get('user.username') !== name) {
return name;
}
}.property('user.name')
});

View File

@ -1,4 +1,5 @@
import UserBadge from 'discourse/models/user-badge'; import UserBadge from 'discourse/models/user-badge';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
queryParams: ['username'], queryParams: ['username'],
@ -6,67 +7,43 @@ export default Ember.Controller.extend({
userBadges: null, userBadges: null,
needs: ["application"], needs: ["application"],
user: function(){ @computed('username')
if (this.get("username")) { user(username) {
if (username) {
return this.get('userBadges')[0].get('user'); return this.get('userBadges')[0].get('user');
} }
}.property("username"), },
grantCount: function() { @computed('username', 'model.grant_count', 'userBadges.grant_count')
if (this.get("username")) { grantCount(username, modelCount, userCount) {
return this.get('userBadges.grant_count'); return username ? userCount : modelCount;
} else { },
return this.get('model.grant_count');
}
}.property('username', 'model', 'userBadges'),
actions: { actions: {
loadMore() { loadMore() {
const self = this;
const userBadges = this.get('userBadges'); const userBadges = this.get('userBadges');
UserBadge.findByBadgeId(this.get('model.id'), { UserBadge.findByBadgeId(this.get('model.id'), {
offset: userBadges.length, offset: userBadges.length,
username: this.get('username'), username: this.get('username'),
}).then(function(result) { }).then(result => {
userBadges.pushObjects(result); userBadges.pushObjects(result);
if(userBadges.length === 0){ if (userBadges.length === 0){
self.set('noMoreBadges', true); this.set('noMoreBadges', true);
} }
}); });
} }
}, },
layoutClass: function(){ @computed('noMoreBadges', 'grantCount', 'userBadges.length')
var user = this.get("user") ? " single-user" : ""; canLoadMore(noMoreBadges, grantCount, userBadgeLength) {
var ub = this.get("userBadges"); if (noMoreBadges) { return false; }
if(ub && ub[0] && ub[0].post_id){ return grantCount > (userBadgeLength || 0);
return "user-badge-with-posts" + user; },
} else {
return "user-badge-no-posts" + user;
}
}.property("userBadges"),
canLoadMore: function() { @observes('canLoadMore')
if (this.get('noMoreBadges')) { return false; } _showFooter() {
if (this.get('userBadges')) {
return this.get('grantCount') > this.get('userBadges.length');
} else {
return false;
}
}.property('noMoreBadges', 'model.grant_count', 'userBadges.length'),
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("canLoadMore")); this.set("controllers.application.showFooter", !this.get("canLoadMore"));
}.observes("canLoadMore"), }
longDescription: function(){
return Discourse.Emoji.unescape(this.get('model.long_description'));
}.property('model.long_description'),
showLongDescription: function(){
return this.get('model.long_description');
}.property('userBadges')
}); });

View File

@ -169,11 +169,6 @@ Discourse.BBCode.replaceBBCodeParamsRaw("email", function(param, contents) {
return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents); return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents);
}); });
Discourse.BBCode.register('size', function(contents, params) {
return ['span', {'class': "bbcode-size-" + (parseInt(params, 10) || 1)}].concat(contents);
});
Discourse.Markdown.whiteListTag('span', 'class', /^bbcode-size-\d+$/);
// Handles `[code] ... [/code]` blocks // Handles `[code] ... [/code]` blocks
Discourse.Dialect.replaceBlock({ Discourse.Dialect.replaceBlock({
start: /(\[code\])([\s\S]*)/igm, start: /(\[code\])([\s\S]*)/igm,

View File

@ -25,3 +25,5 @@ registerUnbound('format-date', function(val, params) {
return new Handlebars.SafeString(autoUpdatingRelativeAge(date, {format: format, title: title, leaveAgo: leaveAgo})); return new Handlebars.SafeString(autoUpdatingRelativeAge(date, {format: format, title: title, leaveAgo: leaveAgo}));
} }
}); });

View File

@ -0,0 +1,6 @@
import { relativeAge } from 'discourse/lib/formatter';
export default function(dt, params) {
dt = params.data.view.getStream(dt).value();
return relativeAge(new Date(dt));
}

View File

@ -2,12 +2,20 @@ import Eyeline from 'discourse/lib/eyeline';
import Scrolling from 'discourse/mixins/scrolling'; import Scrolling from 'discourse/mixins/scrolling';
import { on } from 'ember-addons/ember-computed-decorators'; import { on } from 'ember-addons/ember-computed-decorators';
// Provides the ability to load more items for a view which is scrolled to the bottom. // Provides the ability to load more items for a view which is scrolled to the bottom.
export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
init() {
this._super();
if (!this._viaComponent) {
console.warn('Using `LoadMore` as a view mixin is deprecated. Use `{{load-more}}` instead');
}
},
scrolled() { scrolled() {
const eyeline = this.get('eyeline'); const eyeline = this.get('eyeline');
if (eyeline) { eyeline.update(); } return eyeline && eyeline.update();
}, },
loadMoreUnlessFull() { loadMoreUnlessFull() {

View File

@ -33,13 +33,6 @@ const Badge = RestModel.extend({
return I18n.t(i18nKey, {defaultValue: this.get('name')}); return I18n.t(i18nKey, {defaultValue: this.get('name')});
}.property('name', 'i18nNameKey'), }.property('name', 'i18nNameKey'),
/**
The i18n translated description for this badge. Returns the null if no
translation exists.
@property translatedDescription
@type {String}
**/
translatedDescription: function() { translatedDescription: function() {
const i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description"; const i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description";
let translation = I18n.t(i18nKey); let translation = I18n.t(i18nKey);

View File

@ -6,13 +6,8 @@ const UserBadge = Discourse.Model.extend({
return "/t/-/" + this.get('topic_id') + "/" + this.get('post_number'); return "/t/-/" + this.get('topic_id') + "/" + this.get('post_number');
} }
}.property(), // avoid the extra bindings for now }.property(), // avoid the extra bindings for now
/**
Revoke this badge.
@method revoke revoke() {
@returns {Promise} a promise that resolves when the badge has been revoked.
**/
revoke: function() {
return Discourse.ajax("/user_badges/" + this.get('id'), { return Discourse.ajax("/user_badges/" + this.get('id'), {
type: "DELETE" type: "DELETE"
}); });

View File

@ -15,10 +15,7 @@ export default Discourse.Route.extend({
}, },
serialize(model) { serialize(model) {
return { return model.getProperties('id', 'slug');
id: model.get("id"),
slug: model.get("slug")
};
}, },
model(params) { model(params) {
@ -29,13 +26,12 @@ export default Discourse.Route.extend({
} }
}, },
afterModel(model,transition) { afterModel(model, transition) {
const username = transition.queryParams && transition.queryParams.username; const username = transition.queryParams && transition.queryParams.username;
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => { return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
this.userBadges = userBadges; this.userBadges = userBadges;
}); });
}, },
titleToken() { titleToken() {

View File

@ -22,8 +22,8 @@
<section class='about admins'> <section class='about admins'>
<h3>{{i18n 'about.our_admins'}}</h3> <h3>{{i18n 'about.our_admins'}}</h3>
{{#each a in model.admins}} {{#each model.admins as |a|}}
{{user-small user=a}} {{user-info user=a}}
{{/each}} {{/each}}
<div class='clearfix'></div> <div class='clearfix'></div>
@ -35,8 +35,8 @@
<h3>{{i18n 'about.our_moderators'}}</h3> <h3>{{i18n 'about.our_moderators'}}</h3>
<div class='users'> <div class='users'>
{{#each m in model.moderators}} {{#each model.moderators as |m|}}
{{user-small user=m}} {{user-info user=m}}
{{/each}} {{/each}}
</div> </div>
<div class='clearfix'></div> <div class='clearfix'></div>

View File

@ -1,21 +1,19 @@
<div class='container badges'> <div class='container badges'>
<h1>{{i18n 'badges.title'}}</h1> <h1>{{i18n 'badges.title'}}</h1>
<table class='badges-listing'> <div class='badge-groups'>
<tbody> {{#each bg in badgeGroups}}
{{#each bg in badgeGroups}} <div class='badge-grouping'>
<tr class='title'> <div class='title'>
<td colspan=4><h3>{{bg.badgeGrouping.displayName}}</h3></td> <h3>{{bg.badgeGrouping.displayName}}</h3>
</tr> </div>
{{#each b in bg.badges}}
<tr> {{#each bg.badges as |b|}}
<td class='granted'>{{#if b.has_badge}}<i class='fa fa-check'></i>{{/if}}</td> {{#link-to 'badges.show' b.id b.slug}}
<td class='badge'>{{user-badge badge=b}}</td> {{badge-card badge=b}}
<td class='description'>{{{b.displayDescriptionHtml}}}</td> {{/link-to}}
<td class='grant-count'><span title="{{i18n 'badges.granted' count=b.grant_count}}">{{b.grant_count}}</span></td>
</tr>
{{/each}} {{/each}}
{{/each}} </div>
</tbody> {{/each}}
</table> </div>
</div> </div>

View File

@ -5,66 +5,40 @@
{{model.displayName}} {{model.displayName}}
</h1> </h1>
{{#if showLongDescription}} <div class='show-badge-details'>
<div class='long-description banner'> {{badge-card badge=model size="large"}}
{{{longDescription}}} <div class='badge-grant-info'>
</div> <div>
{{/if}} <div class='grant-info-item'>
{{check-mark checked=model.allow_title}} {{i18n 'badges.allow_title'}}
<div class='badges-listing'> </div>
<div class='row'> <div class='grant-info-item'>
{{#unless user}} {{check-mark checked=model.multiple_grant}} {{i18n 'badges.multiple_grant'}}
<div class='grant-count'>{{i18n 'badges.granted' count=grantCount}}</div> </div>
{{/unless}}
<div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
</div> </div>
</div> </div>
</div> </div>
{{#if user}}
<div class='badge-user-info'>
{{#link-to 'user' user}}
{{avatar user imageSize="extra_large"}}
<div class="details clearfix">
<div class='username'>{{user.username}}</div>
</div>
{{/link-to}}
<div class='earned'>
{{i18n 'badges.earned_n_times' count=grantCount}}
</div>
</div>
{{/if}}
{{#if userBadges}} {{#if userBadges}}
<div class={{unbound layoutClass}}> <div class="user-badges">
{{#each ub in userBadges}} {{#load-more selector=".badge-info" action="loadMore"}}
<div class="badge-user"> {{#each userBadges as |ub|}}
{{#if user}} {{#user-info user=ub.user size="medium" class="badge-info" date=ub.granted_at}}
{{format-date ub.granted_at}} <div class="granted-on">{{i18n 'badges.granted_on' date=(inline-date ub.granted_at)}}</div>
{{else}} {{#if ub.post_number}}
{{#link-to 'user' ub.user classNames="badge-info"}} <a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a>
{{avatar ub.user imageSize="large"}} {{/if}}
<div class="details"> {{/user-info}}
<span class="username">{{ub.user.username}}</span> {{/each}}
{{format-date ub.granted_at}} {{/load-more}}
</div>
{{/link-to}}
{{/if}}
{{#if ub.post_number}}
<a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a>
{{/if}}
</div>
{{/each}}
{{#unless canLoadMore}} {{#unless canLoadMore}}
{{#if user}} {{#if user}}
<a class='load-more' href='{{model.url}}'>{{i18n 'badges.more_with_badge'}}</a> <div class='clearfix'>
<a class='btn' href='{{model.url}}'>{{i18n 'badges.others_count' count=model.grant_count}}</a>
</div>
{{/if}} {{/if}}
{{/unless}} {{/unless}}
</div> </div>
{{conditional-loading-spinner condition=canLoadMore}} {{conditional-loading-spinner condition=canLoadMore}}

View File

@ -0,0 +1,17 @@
{{#if displayCount}}
<span class='grant-count' title={{i18n 'badges.granted' count=displayCount}}>{{displayCount}}</span>
{{/if}}
{{#if badge.has_badge}}
<span class='check-display status-checked'>{{fa-icon "check"}}</span>
{{/if}}
<div class='badge-contents'>
<div class='badge-icon {{badge.badgeTypeClassName}}'>
{{icon-or-image badge.icon}}
</div>
<div class='badge-info'>
<div class='badge-info-item'>
<h3>{{badge.displayName}}</h3>
<div class='badge-summary'>{{{summary}}}</div>
</div>
</div>
</div>

View File

@ -8,4 +8,11 @@
<span class="name">{{unbound name}}</span> <span class="name">{{unbound name}}</span>
</div> </div>
<div class="title">{{unbound user.title}}</div> <div class="title">{{unbound user.title}}</div>
{{#if hasBlock}}
<div class='details'>
{{yield}}
</div>
{{/if}}
</div> </div>

View File

@ -18,7 +18,7 @@
{{#each model.members as |m|}} {{#each model.members as |m|}}
<tr> <tr>
<td class='avatar'> <td class='avatar'>
{{user-small user=m}} {{user-info user=m}}
{{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}} {{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}}
</td> </td>
<td> <td>

View File

@ -13,7 +13,7 @@
{{#each ic in model itemController="directory-item"}} {{#each ic in model itemController="directory-item"}}
<div class="user {{if ic.me 'me'}}"> <div class="user {{if ic.me 'me'}}">
{{#with ic.model as |it|}} {{#with ic.model as |it|}}
{{user-small user=it.user}} {{user-info user=it.user}}
{{user-stat value=it.likes_received label="directory.likes_received" icon="heart"}} {{user-stat value=it.likes_received label="directory.likes_received" icon="heart"}}
{{user-stat value=it.likes_given label="directory.likes_given" icon="heart"}} {{user-stat value=it.likes_given label="directory.likes_given" icon="heart"}}
{{user-stat value=it.topic_count label="directory.topic_count"}} {{user-stat value=it.topic_count label="directory.topic_count"}}

View File

@ -1,5 +1,7 @@
<section class='user-content user-badges-list'> <section class='user-content user-badges-list'>
{{#each ub in controller}} {{#each controller as |ub|}}
{{user-badge badge=ub.badge count=ub.count user=user}} {{#link-to 'badges.show' ub.badge.id ub.badge.slug (query-params username=user.username_lower)}}
{{badge-card badge=ub.badge count=ub.count}}
{{/link-to}}
{{/each}} {{/each}}
</section> </section>

View File

@ -278,7 +278,7 @@
{{plugin-outlet "user-custom-controls"}} {{plugin-outlet "user-custom-controls"}}
<div class="control-group"> <div class="control-group save-button">
<div class="controls"> <div class="controls">
{{partial 'user/preferences/save-button'}} {{partial 'user/preferences/save-button'}}
</div> </div>

View File

@ -28,7 +28,7 @@
{{#each ic in model itemController="directory-item"}} {{#each ic in model itemController="directory-item"}}
<tr class="{{if ic.me 'me'}}"> <tr class="{{if ic.me 'me'}}">
{{#with ic.model as |it|}} {{#with ic.model as |it|}}
<td>{{user-small user=it.user}}</td> <td>{{user-info user=it.user}}</td>
<td>{{number it.likes_received}}</td> <td>{{number it.likes_received}}</td>
<td>{{number it.likes_given}}</td> <td>{{number it.likes_given}}</td>
<td>{{number it.topic_count}}</td> <td>{{number it.topic_count}}</td>

View File

@ -1,11 +0,0 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
eyelineSelector: '.badge-user',
tickOrX: function(field){
var icon = this.get('controller.model.' + field) ? "fa-check" : "fa-times";
return "<i class='fa " + icon + "'></i>";
},
allowTitle: function() { return this.tickOrX("allow_title"); }.property(),
multipleGrant: function() { return this.tickOrX("multiple_grant"); }.property()
});

View File

@ -13,10 +13,4 @@ span {
&.bbcode-s { &.bbcode-s {
text-decoration: line-through; text-decoration: line-through;
} }
// Font sizes
@for $i from 4 through 40 {
&.bbcode-size-#{$i} {
font-size: #{$i}px;
}
}
} }

View File

@ -1,6 +1,10 @@
.directory { .directory {
margin-bottom: 100px; margin-bottom: 100px;
.user-info {
margin-bottom: 0;
}
.period-chooser { .period-chooser {
float: left; float: left;
} }

View File

@ -32,84 +32,6 @@
} }
} }
/* User badge listing. */
.user-badges-list {
text-align: center;
.user-badge {
max-width: 80px;
text-align: center;
vertical-align: top;
margin: 10px;
border: none;
.fa {
display: block;
font-size: 3.571em;
margin-bottom: 5px;
}
img {
display: block;
margin: auto auto 4px;
width: 55px;
height: 55px;
}
.count {
display: block;
font-size: 0.8em;
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
}
}
}
/* Badge listing in /badges. */
.badges-listing {
margin: 20px 0;
tr {
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
td {
padding: 10px 0;
}
}
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
width: 90%;
padding: 10px;
display: table;
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
.row {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
.user-badge {
font-size: $base-font-size;
}
.grant-count {
font-size: 120%;
}
.badge, .grant-count {
white-space: nowrap;
}
.info {
font-size: 0.9em;
text-align: right;
}
.description {
}
}
@media all and (max-width: 750px) { @media all and (max-width: 750px) {
.show-badge .user-badge-with-posts .badge-user a.post-link { .show-badge .user-badge-with-posts .badge-user a.post-link {
width: auto; width: auto;
@ -135,66 +57,23 @@
} }
} }
} }
.user-info.medium.badge-info {
min-height: 80px;
.granted-on {
/* /badges/:id/:slug page styling. */ color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
.show-badge {
.badge-user {
text-align: center;
width: 100px;
padding: 5px 10px;
margin-bottom: 10px;
display: inline-block;
vertical-align: top;
.details {
margin: 0 10px;
padding-top: 3px;
color: $primary;
}
.username {
word-wrap: break-word;
}
.date {
display: block;
color: lighten($primary, 40%);
font-size: 0.714em;
}
}
}
.show-badge .user-badge-with-posts .badge-user {
width: 45%;
padding: 0 0 0 4%;
margin-bottom: 20px;
.badge-info {
width: 100px;
display: block;
float: left;
} }
.post-link { .post-link {
width: 250px;
display: block; display: block;
float: left; margin-top: 0.2em;
margin-left: 18px;
text-align: left;
} }
} }
.show-badge .badge-user-info { .show-badge .badge-user-info {
margin-left: 2%;
.earned { .earned {
margin-top: 15px;
font-size: 1.3em; font-size: 1.3em;
} margin-bottom: 1em;
.username {
margin-top: 5px;
display: block;
color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%));
} }
} }
@ -242,3 +121,123 @@
margin-bottom: 15px; margin-bottom: 15px;
margin-top: 15px; margin-top: 15px;
} }
.badge-card {
position: relative;
display: inline-block;
background-color: dark-light-diff($primary, $secondary, 95%, -65%);
margin-right: 5px;
margin-bottom: 10px;
box-shadow: 1px 1px 3px rgba(0.0, 0.0, 0.0, 0.2);
.check-display {
position: absolute;
left: 5px;
top: 5px;
}
.grant-count {
position: absolute;
right: 5px;
top: 5px;
font-weight: bold;
color: dark-light-diff($primary, $secondary, 50%, -65%);
font-size: 1.2em;
}
.badge-contents {
display: flex;
flex-direction: row;
min-height: 128px;
.badge-icon {
min-width: 90px;
display: flex;
align-items: center;
justify-content: center;
background-color: dark-light-diff($primary, $secondary, 92%, -60%);
font-size: 3em;
&.badge-type-gold .fa {
color: #ffd700 !important;
}
&.badge-type-silver .fa {
color: #c0c0c0 !important;
}
&.badge-type-bronze .fa {
color: #cd7f32 !important;
}
}
.badge-info {
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
color: $primary;
h3 {
margin-bottom: 0.25em;
}
}
}
}
.badge-card.medium {
width: 350px;
}
.badge-card.large {
width: 750px;
}
.badge-groups {
margin: 20px 0;
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
h3 {
margin-bottom: 1.0em;
}
}
.badge-grouping {
margin-bottom: 1.5em;
}
.show-badge-details {
display: flex;
flex-direction: row;
margin-bottom: 2em;
margin-top: 1em;
.badge-grant-info {
display: flex;
align-items: center;
margin-left: 1em;
}
.grant-info-item {
margin-bottom: 1em;
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
}
}
.check-display {
display: inline-block;
width: 18px;
border-radius: 10px;
text-align: center;
.fa {
font-size: 0.9em;
color: $secondary;
}
}
.check-display.status-checked {
background-color: $success;
}
.check-display.status-unchecked {
background-color: $danger;
}

View File

@ -19,6 +19,14 @@
i.fa-heart { i.fa-heart {
color: $love !important; color: $love !important;
} }
.nav-pills {
a {
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 40%));
}
i {
color: dark-light-choose(scale-color($primary, $lightness: 55%), scale-color($secondary, $lightness: 55%));
}
}
} }
.user-field { .user-field {
@ -92,10 +100,10 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
.user-small { .user-info {
display: inline-block; display: inline-block;
width: 333px;
clear: both; clear: both;
margin-bottom: 1em;
.user-image { .user-image {
float: left; float: left;
@ -128,7 +136,30 @@
margin-top: 3px; margin-top: 3px;
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
} }
}
}
.user-info.small {
width: 333px;
}
.user-info.medium {
width: 480px;
min-height: 60px;
.user-image {
width: 55px;
}
.user-detail {
width: 380px;
}
.username, .name {
display: block;
}
.name {
margin-left: 0;
} }
} }

View File

@ -181,10 +181,10 @@ input {
bottom: 35px; bottom: 35px;
} }
.submit-panel { .submit-panel {
width: 50%; // don't specify width; needs to auto-size for smallest phones
position: absolute; position: absolute;
display: block; display: block;
bottom: 2px; bottom: 0;
} }
} }
.category-input { .category-input {

View File

@ -14,11 +14,6 @@
margin-top: 5px; margin-top: 5px;
} }
.topic-post article {
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
padding: 6px 0;
}
.post-stream { .post-stream {
padding-bottom: 30px; padding-bottom: 30px;
} }
@ -287,7 +282,7 @@ a.star {
.btn { .btn {
border: 0; border: 0;
padding: 0 15px; padding: 0 15px;
color: $primary; color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%));
background: blend-primary-secondary(5%); background: blend-primary-secondary(5%);
border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%);
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%);

View File

@ -56,6 +56,10 @@
} }
} }
.delete-account {
overflow: hidden;
}
.checkbox-label { .checkbox-label {
overflow: auto; overflow: auto;
display: block; display: block;
@ -169,18 +173,15 @@
color: $primary; color: $primary;
} }
} }
.user-small { .user-info {
width: 245px; width: 245px;
} }
} }
.user-content { .user-content {
padding: 10px 8px;
background-color: $secondary; background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
margin-bottom: 10px;
box-sizing: border-box; box-sizing: border-box;
margin-top: 20px; margin-top: 10px;
.btn.right { .btn.right {
float: right float: right
@ -393,7 +394,6 @@
.user-stream { .user-stream {
padding: 0 10px;
.excerpt { .excerpt {
margin: 5px 0; margin: 5px 0;
font-size: 0.929em; font-size: 0.929em;
@ -596,3 +596,13 @@
.notification-buttons { .notification-buttons {
display: inline-block; display: inline-block;
} }
// mobile fixups for badges
.badge-card.medium {
width: 300px;
}
.show-badge-details .badge-grant-info {
display: none;
}

View File

@ -282,9 +282,13 @@ class Admin::UsersController < Admin::AdminController
return render nothing: true, status: 404 unless SiteSetting.enable_sso return render nothing: true, status: 404 unless SiteSetting.enable_sso
sso = DiscourseSingleSignOn.parse("sso=#{params[:sso]}&sig=#{params[:sig]}") sso = DiscourseSingleSignOn.parse("sso=#{params[:sso]}&sig=#{params[:sig]}")
user = sso.lookup_or_create_user
render_serialized(user, AdminDetailedUserSerializer, root: false) begin
user = sso.lookup_or_create_user
render_serialized(user, AdminDetailedUserSerializer, root: false)
rescue ActiveRecord::RecordInvalid => ex
render json: failed_json.merge(message: ex.message), status: 403
end
end end
def delete_other_accounts_with_same_ip def delete_other_accounts_with_same_ip

View File

@ -3,7 +3,6 @@ class CategoryHashtagsController < ApplicationController
def check def check
category_slugs = params[:category_slugs] category_slugs = params[:category_slugs]
category_slugs.each(&:downcase!)
ids = category_slugs.map { |category_slug| Category.query_from_hashtag_slug(category_slug).try(:id) } ids = category_slugs.map { |category_slug| Category.query_from_hashtag_slug(category_slug).try(:id) }

View File

@ -129,7 +129,7 @@ class UserAvatarsController < ApplicationController
unless File.exist? path unless File.exist? path
FileUtils.mkdir_p PROXY_PATH FileUtils.mkdir_p PROXY_PATH
tmp = FileHelper.download(url, 1.megabyte, filename, true) tmp = FileHelper.download(url, 1.megabyte, filename, true, 10)
FileUtils.mv tmp.path, path FileUtils.mv tmp.path, path
end end

View File

@ -96,9 +96,12 @@ module Jobs
process_popmail(p) process_popmail(p)
end end
end end
rescue Net::OpenTimeout => e
mark_as_errored!
Discourse.handle_job_exception(e, error_context(@args, "Connecting to '#{SiteSetting.pop3_polling_host}' for polling emails."))
rescue Net::POPAuthenticationError => e rescue Net::POPAuthenticationError => e
mark_as_errored! mark_as_errored!
Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming email")) Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming emails."))
end end
POLL_MAILBOX_ERRORS_KEY ||= "poll_mailbox_errors".freeze POLL_MAILBOX_ERRORS_KEY ||= "poll_mailbox_errors".freeze

View File

@ -173,12 +173,14 @@ class UserNotifications < ActionMailer::Base
end end
def user_invited_to_private_message(user, opts) def user_invited_to_private_message(user, opts)
opts[:use_template_html] = true opts[:allow_reply_by_email] = false
opts[:use_invite_template] = true
notification_email(user, opts) notification_email(user, opts)
end end
def user_invited_to_topic(user, opts) def user_invited_to_topic(user, opts)
opts[:use_template_html] = true opts[:allow_reply_by_email] = false
opts[:use_invite_template] = true
opts[:show_category_in_subject] = true opts[:show_category_in_subject] = true
notification_email(user, opts) notification_email(user, opts)
end end
@ -273,7 +275,7 @@ class UserNotifications < ActionMailer::Base
add_re_to_subject: opts[:add_re_to_subject], add_re_to_subject: opts[:add_re_to_subject],
show_category_in_subject: opts[:show_category_in_subject], show_category_in_subject: opts[:show_category_in_subject],
notification_type: notification_type, notification_type: notification_type,
use_template_html: opts[:use_template_html], use_invite_template: opts[:use_invite_template],
user: user user: user
) )
end end
@ -322,9 +324,21 @@ class UserNotifications < ActionMailer::Base
.where('created_at > ?', 1.day.ago) .where('created_at > ?', 1.day.ago)
.count) >= (SiteSetting.max_emails_per_day_per_user-1) .count) >= (SiteSetting.max_emails_per_day_per_user-1)
topic_excerpt = "" if opts[:use_invite_template]
if opts[:use_template_html] if post.topic.private_message?
invite_template = "user_notifications.invited_to_private_message_body"
else
invite_template = "user_notifications.invited_to_topic_body"
end
topic_excerpt = post.excerpt.gsub("\n", " ") if post.is_first_post? && post.excerpt topic_excerpt = post.excerpt.gsub("\n", " ") if post.is_first_post? && post.excerpt
message = I18n.t(invite_template, username: post.username, topic_title: title, topic_excerpt: topic_excerpt, site_title: SiteSetting.title, site_description: SiteSetting.site_description)
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
template: 'email/invite',
format: :html,
locals: { message: PrettyText.cook(message, sanitize: false).html_safe,
classes: RTL.new(user).css_class
}
)
else else
in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render( html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
@ -337,6 +351,7 @@ class UserNotifications < ActionMailer::Base
classes: RTL.new(user).css_class classes: RTL.new(user).css_class
} }
) )
message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "");
end end
template = "user_notifications.user_#{notification_type}" template = "user_notifications.user_#{notification_type}"
@ -348,8 +363,7 @@ class UserNotifications < ActionMailer::Base
email_opts = { email_opts = {
topic_title: title, topic_title: title,
topic_excerpt: topic_excerpt, message: message,
message: email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : ""),
url: post.url, url: post.url,
post_id: post.id, post_id: post.id,
topic_id: post.topic_id, topic_id: post.topic_id,

View File

@ -339,6 +339,10 @@ class Topic < ActiveRecord::Base
# Remove muted categories # Remove muted categories
muted_category_ids = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:muted]).pluck(:category_id) muted_category_ids = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:muted]).pluck(:category_id)
if SiteSetting.digest_suppress_categories.present?
muted_category_ids += SiteSetting.digest_suppress_categories.split("|").map(&:to_i)
muted_category_ids = muted_category_ids.uniq
end
if muted_category_ids.present? if muted_category_ids.present?
topics = topics.where("topics.category_id NOT IN (?)", muted_category_ids) topics = topics.where("topics.category_id NOT IN (?)", muted_category_ids)
end end

View File

@ -9,4 +9,5 @@ class BadgeIndexSerializer < BadgeSerializer
def has_badge def has_badge
@options[:user_badges].include?(object.id) @options[:user_badges].include?(object.id)
end end
end end

View File

@ -0,0 +1,13 @@
<div id='main' class=<%= classes %>>
<div class='header-instructions'>%{header_instructions}</div>
<% if message.present? %>
<div><%= message %></div>
<% end %>
<hr>
<div class='footer'>%{respond_instructions}</div>
<div class='footer'>%{unsubscribe_link}%{unsubscribe_via_email_link}</div>
</div>

View File

@ -64,17 +64,17 @@ store.redis_raw_connection = redis.without_namespace
severities = [Logger::WARN, Logger::ERROR, Logger::FATAL, Logger::UNKNOWN] severities = [Logger::WARN, Logger::ERROR, Logger::FATAL, Logger::UNKNOWN]
RailsMultisite::ConnectionManagement.each_connection do RailsMultisite::ConnectionManagement.each_connection do
error_rate_per_minute = SiteSetting.alert_admins_if_errors_per_minute error_rate_per_minute = SiteSetting.alert_admins_if_errors_per_minute rescue 0
if error_rate_per_minute > 0 if (error_rate_per_minute || 0) > 0
store.register_rate_limit_per_minute(severities, error_rate_per_minute) do |rate| store.register_rate_limit_per_minute(severities, error_rate_per_minute) do |rate|
MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'minute' }) MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'minute' })
end end
end end
error_rate_per_hour = SiteSetting.alert_admins_if_errors_per_hour error_rate_per_hour = SiteSetting.alert_admins_if_errors_per_hour rescue 0
if error_rate_per_hour > 0 if (error_rate_per_hour || 0) > 0
store.register_rate_limit_per_hour(severities, error_rate_per_hour) do |rate| store.register_rate_limit_per_hour(severities, error_rate_per_hour) do |rate|
MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'hour' }) MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'hour' })
end end

View File

@ -2855,10 +2855,11 @@ en:
earned_n_times: earned_n_times:
one: "Earned this badge 1 time" one: "Earned this badge 1 time"
other: "Earned this badge %{count} times" other: "Earned this badge %{count} times"
more_with_badge: "Others with this badge" granted_on: "Granted %{date}"
others_count: "Others with this badge (%{count})"
title: Badges title: Badges
allow_title: "can be used as a title" allow_title: "available title"
multiple_grant: "can be awarded multiple times" multiple_grant: "awarded multiple times"
badge_count: badge_count:
one: "1 Badge" one: "1 Badge"
other: "%{count} Badges" other: "%{count} Badges"

View File

@ -1195,6 +1195,7 @@ en:
digest_topics: "The maximum number of topics to display in the email digest." digest_topics: "The maximum number of topics to display in the email digest."
digest_min_excerpt_length: "Minimum post excerpt in the email digest, in characters." digest_min_excerpt_length: "Minimum post excerpt in the email digest, in characters."
delete_digest_email_after_days: "Suppress digest emails for users not seen on the site for more than (n) days." delete_digest_email_after_days: "Suppress digest emails for users not seen on the site for more than (n) days."
digest_suppress_categories: "Suppress these categories from digest emails."
disable_digest_emails: "Disable digest emails for all users." disable_digest_emails: "Disable digest emails for all users."
detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures."
@ -2067,53 +2068,57 @@ en:
posted_by: "Posted by %{username} on %{post_date}" posted_by: "Posted by %{username} on %{post_date}"
invited_to_private_message_body: |
%{username} invited you to a message
> **%{topic_title}**
>
> %{topic_excerpt}
at
> %{site_title} -- %{site_description}
invited_to_topic_body: |
%{username} invited you to a discussion
> **%{topic_title}**
>
> %{topic_excerpt}
at
> %{site_title} -- %{site_description}
user_invited_to_private_message_pm: user_invited_to_private_message_pm:
subject_template: "[%{site_name}] %{username} invited you to a message '%{topic_title}'" subject_template: "[%{site_name}] %{username} invited you to a message '%{topic_title}'"
text_body_template: | text_body_template: |
%{header_instructions}
%{username} invited you to a message %{message}
> **%{topic_title}** ---
> %{respond_instructions}
> %{topic_excerpt}
at
> %{site_title} -- %{site_description}
Please visit this link to view the message: %{base_url}%{url}
user_invited_to_private_message_pm_staged: user_invited_to_private_message_pm_staged:
subject_template: "[%{site_name}] %{username} invited you to a message '%{topic_title}'" subject_template: "[%{site_name}] %{username} invited you to a message '%{topic_title}'"
text_body_template: | text_body_template: |
%{header_instructions}
%{username} invited you to a message %{message}
> **%{topic_title}** ---
> %{respond_instructions}
> %{topic_excerpt}
at
> %{site_title} -- %{site_description}
Please visit this link to view the message: %{base_url}%{url}
user_invited_to_topic: user_invited_to_topic:
subject_template: "[%{site_name}] %{username} invited you to '%{topic_title}'" subject_template: "[%{site_name}] %{username} invited you to '%{topic_title}'"
text_body_template: | text_body_template: |
%{header_instructions}
%{username} invited you to a discussion %{message}
> **%{topic_title}** ---
> %{respond_instructions}
> %{topic_excerpt}
at
> %{site_title} -- %{site_description}
Please visit this link to view the message: %{base_url}%{url}
user_replied: user_replied:
subject_template: "[%{site_name}] %{topic_title}" subject_template: "[%{site_name}] %{topic_title}"
@ -2770,49 +2775,72 @@ en:
read_guidelines: | read_guidelines: |
This badge is granted for <a href="/guidelines">reading the community guidelines</a>. Following and sharing these simple guidelines helps build a safe, fun, and sustainable community for everyone. Always remember there's another human being, one very much like yourself, on the other side of that screen. Be nice! This badge is granted for <a href="/guidelines">reading the community guidelines</a>. Following and sharing these simple guidelines helps build a safe, fun, and sustainable community for everyone. Always remember there's another human being, one very much like yourself, on the other side of that screen. Be nice!
reader: | reader: |
This badge is granted the first time you read a long topic with more than 100 replies. Reading a conversation closely helps you follow the discussion, understand different viewpoints, and leads to more interesting conversations. The more you read, the better the conversation gets. As we like to say, Reading is Fundamental! :simple_smile: This badge is granted the first time you read a long topic with more than 100 replies. Reading a conversation closely helps you follow the discussion, understand different viewpoints, and leads to more interesting conversations. The more you read, the better the conversation gets. As we like to say, Reading is Fundamental! :slight_smile:
editor: | editor: |
This badge is granted the first time you edit one of your posts. While you won't be able to edit your posts forever, editing is always a good idea -- you can improve your posts, fix small mistakes, or add anything you missed when you originally posted. Edit to make your posts even better! This badge is granted the first time you edit one of your posts. While you won't be able to edit your posts forever, editing is always a good idea you can improve your posts, fix small mistakes, or add anything you missed when you originally posted. Edit to make your posts even better!
first_flag: | first_flag: |
This badge is granted the first time you flag a post. Flagging is how we all help keep this a clean, well lit place for everyone. If you notice any posts that require moderator attention for any reason please don't hesitate to flag. You can also use flag to send <b>personal messages</b> to fellow users if you see an issue with their post. If you see a problem, flag it! This badge is granted the first time you flag a post. Flagging is how we all help keep this a clean, well lit place for everyone. If you notice any posts that require moderator attention for any reason please don't hesitate to flag. You can also flag to send <b>personal messages</b> to fellow users if you see an issue with their post. If you see a problem, :flag_black: flag it!
nice_share: |
This badge is granted for sharing a link that was clicked by 25 outside visitors. Thanks for spreading the word about our discussions, and this community.
welcome: | welcome: |
This badge is granted when you receive your first like on a post. Congratulations, you've posted something that your fellow community members found interesting, cool, or useful! Now keep going! This badge is granted when you receive your first like on a post. Congratulations, you've posted something that your fellow community members found interesting, cool, or useful!
anniversary: | anniversary: |
This badge is granted when you've been a member for a year with at least one post in that year. Thank you for sticking around and contributing to our community. We couldn't do it without you. This badge is granted when you've been a member for a year with at least one post in that year. Thank you for sticking around and contributing to our community. We couldn't do it without you.
nice_share: |
This badge is granted for sharing a link that was clicked by 25 outside visitors. Thanks for spreading the word about our discussions, and this community.
good_share: | good_share: |
This badge is granted for sharing a link that was clicked by 300 outside visitors. Good work! You've shown off a great discussion to a bunch of new people and helped this community grow. This badge is granted for sharing a link that was clicked by 300 outside visitors. Good work! You've shown off a great discussion to a bunch of new people and helped this community grow.
great_share: | great_share: |
This badge is granted for sharing a link that was clicked by 1000 outside visitors. Wow! You've promoted an interesting discussion to a huge new audience, and helped us grow our community in a big way! This badge is granted for sharing a link that was clicked by 1000 outside visitors. Wow! You've promoted an interesting discussion to a huge new audience, and helped us grow our community in a big way!
nice_topic: | nice_topic: |
This badge is granted when your topic gets 10 likes. You started an interesting conversation that the community enjoyed. This badge is granted when your topic gets 10 likes. Hey, you started an interesting conversation that the community enjoyed!
nice_post: | nice_post: |
This badge is granted when your reply gets 10 likes. Your reply made an impression on the community and helped move the conversation forward. This badge is granted when your reply gets 10 likes. Your reply really made an impression on the community and helped move the conversation forward!
good_topic: | good_topic: |
This badge is granted when your topic gets 25 likes. You launched a vibrant conversation that the community really responded to. This badge is granted when your topic gets 25 likes. You launched a vibrant conversation that the community rallied around and loved!
good_post: | good_post: |
This badge is granted when your reply gets 25 likes. Your reply was exceptional and it made the conversation a whole lot better for everyone. This badge is granted when your reply gets 25 likes. Your reply was exceptional and made the conversation a whole lot better for everyone!
great_topic: | great_topic: |
This badge is granted when your topic gets 50 likes. Wow! You kicked off a fascinating conversation and the community loved the dynamic discussion that resulted. This badge is granted when your topic gets 50 likes. You kicked off a fascinating conversation and the community enjoyed the dynamic discussion that resulted!
great_post: | great_post: |
This badge is granted when your reply gets 50 likes. Wow! Your reply was inspiring, fascinating, hilarious, or insightful and the community loved it. This badge is granted when your reply gets 50 likes. Wow! Your reply was inspiring, fascinating, hilarious, or insightful and the community loved it.
basic: |
This badge is granted when you reach trust level 1. Thanks for sticking around a little while and reading a few topics to learn what our community is about. Your new user restrictions have been lifted; you've been granted all essential community abilities, such as personal messaging, flagging, wiki editing, and the ability to post multiple images and links.
appreciated: | appreciated: |
This badge is granted when you receive at least one like on 20 different posts. The community is enjoying your contributions to the conversations here, so keep them coming! This badge is granted when you receive at least one like on 20 different posts. The community is enjoying your contributions to the conversations here!
respected: |
This badge is granted when you receive at least 2 likes on 100 different posts. The community is growing to respect your many contributions to the conversations here.
admired: |
This badge is granted when you receive at least 5 likes on 300 different posts. Wow! The community admires your frequent, high quality contributions to the conversations here.
out_of_love: | out_of_love: |
This badge is granted when you use all 50 of your daily likes. Letting the community know what's great by regularly liking those posts you enjoy and appreciate is the best way to encourage people to create even more great discussions in the future. This badge is granted when you use all 50 of your daily likes. Remembering to take a moment and like the posts you enjoy and appreciate encourages your fellow community members to create even more great discussions in the future.
higher_love: |
This badge is granted when you use all 50 of your daily likes for 5 days. Thanks for taking the time actively encouraging the best conversations every day!
crazy_in_love: |
This badge is granted when you use all 50 of your daily likes for 20 days. Wow! You're a model of regularly encouraging your fellow community members!
promoter: | promoter: |
This badge is granted when you invite someone to join the community via the invite button on your user page, or at the bottom of a topic. Inviting friends who might be interested in specific discussions is an great way to introduce new people to our community, so thanks! This badge is granted when you invite someone to join the community via the invite button on your user page, or at the bottom of a topic. Inviting friends who might be interested in specific discussions is an great way to introduce new people to our community, so thanks!
campaigner: |
This badge is granted when you've invited 3 people who subsequently spent enough time on the site to become basic users. A vibrant community needs a regular infusion of newcomers who regularly participate and add new voices to the conversationss.
champion: |
This badge is granted when you've invited 5 people who subsequently spent enough time on the site to become full members. Wow! Thanks for expanding the diversity of our community with new members!
thank_you: | thank_you: |
This badge is granted when you have at least 20 liked posts. This badge is granted when you've received 20 likes on your posts and have given 10 or more likes in return. When someone likes your posts, you find the time to like what other people are posting in return.
gives_back: |
This badge is granted when you've received 100 likes and have given 100 or more likes in return. Thanks for paying it forward, and liking in return!
empathetic: |
This badge is granted when you've received 500 likes and have given 1000 or more likes in return. Wow! You're a model of generosity and mutual love :two_hearts:.
basic_user: |
This badge is granted when you reach trust level 1. Thanks for sticking around a little while and reading a few topics to learn what our community is about. Your new user restrictions have been lifted; you've been granted all essential community abilities, such as personal messaging, flagging, wiki editing, and the ability to post multiple images and links.
member: | member: |
This badge is granted when you reach trust level 2. Thanks for participating over a period of weeks to truly join our community. You can now send invitations from your user page or individual topics, create group personal messages, and have a few more likes per day. This badge is granted when you reach trust level 2. Thanks for participating over a period of weeks to truly join our community. You can now send invitations from your user page or individual topics, create group personal messages, and have a few more likes per day.
regular: | regular: |
This badge is granted when you reach trust level 3. Thanks for being a regular part of our community over a period of months. You're now one of the most active readers, and a reliable contributor that makes our community great. You can now recategorize and rename topics, take advantage of more powerful spam flags, access a private lounge area, and you'll also get lots more likes per day. This badge is granted when you reach trust level 3. Thanks for being a regular part of our community over a period of months. You're now one of the most active readers, and a reliable contributor that makes our community great. You can now recategorize and rename topics, take advantage of more powerful spam flags, access a private lounge area, and you'll also get lots more likes per day.
leader: | leader: |
This badge is granted when you reach trust level 4. You're a leader in this community as selected by staff, and you set a positive example for the rest of the community in your actions and words here. You have the ability to edit all posts, take common topic moderator actions such as pin, close, unlist, archive, split, and merge, and you have tons of likes per day. This badge is granted when you reach trust level 4. You're a leader in this community as selected by staff, and you set a positive example for the rest of the community in your actions and words here. You have the ability to edit all posts, take common topic moderator actions such as pin, close, unlist, archive, split, and merge, and you have tons of likes per day.
popular_link: |
This badge is granted when a link you shared gets 50 clicks. Thanks for posting a useful link that added interesting context to the conversation!
hot_link: |
This badge is granted when a link you shared gets 300 clicks. Thanks for posting a fascinating link that drove the conversation forward and illuminated the discussion!
famous_link: |
This badge is granted when a link you shared gets 1000 clicks. Wow! You posted a link that significantly improved the conversation by addding essential detail, context, and information. Great work!
admin_login: admin_login:
success: "Email Sent" success: "Email Sent"

View File

@ -252,15 +252,15 @@ Discourse::Application.routes.draw do
end end
resources :static resources :static
post "login" => "static#enter" post "login" => "static#enter", constraints: { format: /(json|html)/ }
get "login" => "static#show", id: "login" get "login" => "static#show", id: "login", constraints: { format: /(json|html)/ }
get "password-reset" => "static#show", id: "password_reset" get "password-reset" => "static#show", id: "password_reset", constraints: { format: /(json|html)/ }
get "faq" => "static#show", id: "faq" get "faq" => "static#show", id: "faq", constraints: { format: /(json|html)/ }
get "guidelines" => "static#show", id: "guidelines", as: 'guidelines' get "guidelines" => "static#show", id: "guidelines", as: 'guidelines', constraints: { format: /(json|html)/ }
get "tos" => "static#show", id: "tos", as: 'tos' get "tos" => "static#show", id: "tos", as: 'tos', constraints: { format: /(json|html)/ }
get "privacy" => "static#show", id: "privacy", as: 'privacy' get "privacy" => "static#show", id: "privacy", as: 'privacy', constraints: { format: /(json|html)/ }
get "signup" => "static#show", id: "signup" get "signup" => "static#show", id: "signup", constraints: { format: /(json|html)/ }
get "login-preferences" => "static#show", id: "login" get "login-preferences" => "static#show", id: "login", constraints: { format: /(json|html)/ }
get "users/admin-login" => "users#admin_login" get "users/admin-login" => "users#admin_login"
put "users/admin-login" => "users#admin_login" put "users/admin-login" => "users#admin_login"

View File

@ -512,6 +512,9 @@ email:
digest_min_excerpt_length: 100 digest_min_excerpt_length: 100
digest_topics: 20 digest_topics: 20
delete_digest_email_after_days: 365 delete_digest_email_after_days: 365
digest_suppress_categories:
type: category_list
default: ''
disable_digest_emails: disable_digest_emails:
default: false default: false
client: true client: true

View File

@ -134,7 +134,8 @@ Badge.seed do |b|
b.target_posts = true b.target_posts = true
b.show_posts = false b.show_posts = false
b.query = Badge::Queries::FirstFlag b.query = Badge::Queries::FirstFlag
b.default_badge_grouping_id = BadgeGrouping::Community b.badge_grouping_id = BadgeGrouping::GettingStarted
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostAction b.trigger = Badge::Trigger::PostAction
b.auto_revoke = false b.auto_revoke = false
b.system = true b.system = true
@ -228,7 +229,8 @@ Badge.seed do |b|
b.badge_type_id = BadgeType::Bronze b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false b.multiple_grant = false
b.query = Badge::Queries::Editor b.query = Badge::Queries::Editor
b.default_badge_grouping_id = BadgeGrouping::Community b.badge_grouping_id = BadgeGrouping::GettingStarted
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostRevision b.trigger = Badge::Trigger::PostRevision
b.system = true b.system = true
end end
@ -285,7 +287,8 @@ end
b.target_posts = true b.target_posts = true
b.show_posts = true b.show_posts = true
b.query = Badge::Queries.linking_badge(count) b.query = Badge::Queries.linking_badge(count)
b.default_badge_grouping_id = BadgeGrouping::Community b.badge_grouping_id = BadgeGrouping::Posting
b.default_badge_grouping_id = BadgeGrouping::Posting
# don't trigger for now, its too expensive # don't trigger for now, its too expensive
b.trigger = Badge::Trigger::None b.trigger = Badge::Trigger::None
b.system = true b.system = true

View File

@ -6,7 +6,7 @@ class FileHelper
filename =~ images_regexp filename =~ images_regexp
end end
def self.download(url, max_file_size, tmp_file_name, follow_redirect=false) def self.download(url, max_file_size, tmp_file_name, follow_redirect=false, read_timeout=5)
raise Discourse::InvalidParameters.new(:url) unless url =~ /^https?:\/\// raise Discourse::InvalidParameters.new(:url) unless url =~ /^https?:\/\//
uri = parse_url(url) uri = parse_url(url)
@ -14,7 +14,7 @@ class FileHelper
tmp = Tempfile.new([tmp_file_name, extension]) tmp = Tempfile.new([tmp_file_name, extension])
File.open(tmp.path, "wb") do |f| File.open(tmp.path, "wb") do |f|
downloaded = uri.open("rb", read_timeout: 5, redirect: follow_redirect, allow_redirections: :all) downloaded = uri.open("rb", read_timeout: read_timeout, redirect: follow_redirect, allow_redirections: :all)
while f.size <= max_file_size && data = downloaded.read(512.kilobytes) while f.size <= max_file_size && data = downloaded.read(512.kilobytes)
f.write(data) f.write(data)
end end

View File

@ -1,6 +1,6 @@
module MobileDetection module MobileDetection
def self.mobile_device?(user_agent) def self.mobile_device?(user_agent)
user_agent =~ /Mobile|Android|webOS/ && !(user_agent =~ /iPad|Nexus (9|10)/) user_agent =~ /Mobile/ && !(user_agent =~ /iPad/)
end end
# we need this as a reusable chunk that is called from the cache # we need this as a reusable chunk that is called from the cache

View File

@ -22,5 +22,13 @@ describe CategoryHashtag do
it "should return nil for incorrect parent and child category slug" do it "should return nil for incorrect parent and child category slug" do
expect(Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug")).to eq(nil) expect(Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug")).to eq(nil)
end end
it "should be case sensitive" do
parent_category.update_attributes!(slug: "ApPlE")
child_category.update_attributes!(slug: "OraNGE")
expect(Category.query_from_hashtag_slug("apple")).to eq(nil)
expect(Category.query_from_hashtag_slug("apple:orange")).to eq(nil)
end
end end
end end

View File

@ -507,52 +507,66 @@ describe Admin::UsersController do
end end
it 'can sync up sso' do
log_in(:admin)
SiteSetting.enable_sso = true context '#sync_sso' do
SiteSetting.sso_overrides_email = true let(:sso) { SingleSignOn.new }
SiteSetting.sso_overrides_name = true let(:sso_secret) { "sso secret" }
SiteSetting.sso_overrides_username = true
SiteSetting.sso_secret = "sso secret" before do
log_in(:admin)
sso = SingleSignOn.new SiteSetting.enable_sso = true
sso.sso_secret = "sso secret" SiteSetting.sso_overrides_email = true
sso.name = "Bob The Bob" SiteSetting.sso_overrides_name = true
sso.username = "bob" SiteSetting.sso_overrides_username = true
sso.email = "bob@bob.com" SiteSetting.sso_secret = sso_secret
sso.external_id = "1" sso.sso_secret = sso_secret
end
user = DiscourseSingleSignOn.parse(sso.payload)
.lookup_or_create_user
sso.name = "Bill" it 'can sync up with the sso' do
sso.username = "Hokli$$!!" sso.name = "Bob The Bob"
sso.email = "bob2@bob.com" sso.username = "bob"
sso.email = "bob@bob.com"
sso.external_id = "1"
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload) user = DiscourseSingleSignOn.parse(sso.payload)
expect(response).to be_success .lookup_or_create_user
user.reload sso.name = "Bill"
expect(user.email).to eq("bob2@bob.com") sso.username = "Hokli$$!!"
expect(user.name).to eq("Bill") sso.email = "bob2@bob.com"
expect(user.username).to eq("Hokli")
# It can also create new users xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
sso = SingleSignOn.new expect(response).to be_success
sso.sso_secret = "sso secret"
sso.name = "Dr. Claw"
sso.username = "dr_claw"
sso.email = "dr@claw.com"
sso.external_id = "2"
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
expect(response).to be_success
user = User.where(email: 'dr@claw.com').first user.reload
expect(user).to be_present expect(user.email).to eq("bob2@bob.com")
expect(user.ip_address).to be_blank expect(user.name).to eq("Bill")
expect(user.username).to eq("Hokli")
end
it 'should create new users' do
sso.name = "Dr. Claw"
sso.username = "dr_claw"
sso.email = "dr@claw.com"
sso.external_id = "2"
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
expect(response).to be_success
user = User.where(email: 'dr@claw.com').first
expect(user).to be_present
expect(user.ip_address).to be_blank
end
it 'should return the right message if the record is invalid' do
sso.email = ""
sso.name = ""
sso.external_id = "1"
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
expect(response.status).to eq(403)
expect(JSON.parse(response.body)["message"]).to include("Email can't be blank")
end
end end
end end

View File

@ -29,28 +29,38 @@ describe ApplicationHelper do
context "mobile_view is not set" do context "mobile_view is not set" do
it "is false if user agent is not mobile" do it "is false if user agent is not mobile" do
controller.request.stubs(:user_agent).returns('Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36') controller.request.stubs(:user_agent).returns('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36')
expect(helper.mobile_view?).to be_falsey expect(helper.mobile_view?).to be_falsey
end end
it "is true for iPhone" do it "is true for iPhone" do
controller.request.stubs(:user_agent).returns('Mozilla/5.0 (iPhone; U; ru; CPU iPhone OS 4_2_1 like Mac OS X; ru) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5') controller.request.stubs(:user_agent).returns('Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1')
expect(helper.mobile_view?).to eq(true)
end
it "is true for Android Samsung Galaxy" do
controller.request.stubs(:user_agent).returns('Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-G925F Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36')
expect(helper.mobile_view?).to eq(true)
end
it "is true for Android Google Nexus 5X" do
controller.request.stubs(:user_agent).returns('Mozilla/5.0 (Linux; Android 6.0; Nexus 5X Build/MDB08I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.43 Mobile Safari/537.36')
expect(helper.mobile_view?).to eq(true) expect(helper.mobile_view?).to eq(true)
end end
it "is false for iPad" do it "is false for iPad" do
controller.request.stubs(:user_agent).returns("Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3") controller.request.stubs(:user_agent).returns("Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B14 3 Safari/601.1")
expect(helper.mobile_view?).to eq(false) expect(helper.mobile_view?).to eq(false)
end end
it "is false for Nexus 10 tablet" do it "is false for Nexus 10 tablet" do
controller.request.stubs(:user_agent).returns("Mozilla/5.0 (Linux; Android 4.2.1; Nexus 10 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19") controller.request.stubs(:user_agent).returns("Mozilla/5.0 (Linux; Android 5.1.1; Nexus 10 Build/LMY49G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.91 Safari/537.36")
expect(helper.mobile_view?).to be_falsey expect(helper.mobile_view?).to be_falsey
end end
it "is true for Nexus 7 tablet" do it "is false for Nexus 7 tablet" do
controller.request.stubs(:user_agent).returns("Mozilla/5.0 (Linux; Android 4.1.2; Nexus 7 Build/JZ054K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19") controller.request.stubs(:user_agent).returns("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MMB29Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.91 Safari/537.36")
expect(helper.mobile_view?).to eq(true) expect(helper.mobile_view?).to be_falsey
end end
end end
end end

View File

@ -1296,6 +1296,16 @@ describe Topic do
expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank
end end
it "doesn't return topics from suppressed categories" do
user = Fabricate(:user)
category = Fabricate(:category)
Fabricate(:topic, category: category)
SiteSetting.digest_suppress_categories = "#{category.id}"
expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank
end
it "doesn't return topics from TL0 users" do it "doesn't return topics from TL0 users" do
new_user = Fabricate(:user, trust_level: 0) new_user = Fabricate(:user, trust_level: 0)
Fabricate(:topic, user_id: new_user.id) Fabricate(:topic, user_id: new_user.id)
@ -1607,7 +1617,7 @@ describe Topic do
topic.update_status('closed', true, user) topic.update_status('closed', true, user)
topic.reload topic.reload
expect(@topic_status_event_triggered).to eq(true) expect(@topic_status_event_triggered).to eq(true)
end end
end end

View File

@ -4,8 +4,8 @@ acceptance("About");
test("viewing", () => { test("viewing", () => {
visit("/about"); visit("/about");
andThen(() => { andThen(() => {
ok(exists('.about.admins .user-small'), 'has admins'); ok(exists('.about.admins .user-info'), 'has admins');
ok(exists('.about.moderators .user-small'), 'has moderators'); ok(exists('.about.moderators .user-info'), 'has moderators');
ok(exists('.about.stats tr td'), 'has stats'); ok(exists('.about.stats tr td'), 'has stats');
}); });
}); });

View File

@ -5,12 +5,12 @@ acceptance("Badges");
test("Visit Badge Pages", () => { test("Visit Badge Pages", () => {
visit("/badges"); visit("/badges");
andThen(() => { andThen(() => {
ok(exists('.badges-listing tr'), "has a list of badges"); ok(exists('.badge-groups .badge-card'), "has a list of badges");
}); });
visit("/badges/9/autobiographer"); visit("/badges/9/autobiographer");
andThen(() => { andThen(() => {
ok(exists('.badges-listing div'), "has the badge in the listing"); ok(exists('.badge-card'), "has the badge in the listing");
ok(exists('.badge-user'), "has the list of users with that badge"); ok(exists('.user-info'), "has the list of users with that badge");
}); });
}); });

View File

@ -62,20 +62,6 @@ test('tags with arguments', function() {
format("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line"); format("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line");
}); });
test("size tags", function() {
format("[size=35]BIG [b]whoop[/b][/size]",
"<span class=\"bbcode-size-35\">BIG <span class=\"bbcode-b\">whoop</span></span>",
"supports [size=]");
format("[size=asdf]regular[/size]",
"<span class=\"bbcode-size-1\">regular</span>",
"it only supports numbers in bbcode");
format("[size=35]NEWLINE\n\ntest[/size]",
"<span class=\"bbcode-size-35\"><p>NEWLINE</p><p>test</p></span>",
"works with newlines");
format("[size=35][quote=\"user\"]quote[/quote][/size]",
"<span class=\"bbcode-size-35\"><aside class=\"quote\"><div class=\"title\"><div class=\"quote-controls\"></div>user:</div><blockquote><p>quote</p></blockquote></aside></span>",
"works with nested complex blocks");
});
test("quotes", function() { test("quotes", function() {

View File

@ -510,9 +510,9 @@ test("sanitize", function() {
cooked("<i class=\"fa fa-bug fa-spin\" style=\"font-size:600%\"></i>\n<!-- -->", "<p><i></i><br/></p>", "it doesn't circumvent XSS with comments"); cooked("<i class=\"fa fa-bug fa-spin\" style=\"font-size:600%\"></i>\n<!-- -->", "<p><i></i><br/></p>", "it doesn't circumvent XSS with comments");
cooked("<span class=\"-bbcode-size-0 fa fa-spin\">a</span>", "<p><span>a</span></p>", "it sanitizes spans"); cooked("<span class=\"-bbcode-s fa fa-spin\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"fa fa-spin -bbcode-size-0\">a</span>", "<p><span>a</span></p>", "it sanitizes spans"); cooked("<span class=\"fa fa-spin -bbcode-s\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"bbcode-size-10\">a</span>", "<p><span class=\"bbcode-size-10\">a</span></p>", "it sanitizes spans"); cooked("<span class=\"bbcode-s\">a</span>", "<p><span class=\"bbcode-s\">a</span></p>", "it sanitizes spans");
}); });
test("URLs in BBCode tags", function() { test("URLs in BBCode tags", function() {