mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-24 08:09:13 -05:00
Merge branch 'master' of github.com:discourse/discourse
This commit is contained in:
commit
3f5b5f1581
44 changed files with 508 additions and 501 deletions
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 6.4 KiB |
|
@ -119,6 +119,24 @@ Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Pres
|
|||
approveUsers: function() {
|
||||
Discourse.AdminUser.bulkApprove(this.get('content').filterProperty('selected'));
|
||||
this.refreshUsers();
|
||||
},
|
||||
|
||||
/**
|
||||
Reject all the currently selected users.
|
||||
|
||||
@method rejectUsers
|
||||
**/
|
||||
rejectUsers: function() {
|
||||
var controller = this;
|
||||
Discourse.AdminUser.bulkReject(this.get('content').filterProperty('selected')).then(function(result){
|
||||
var message = I18n.t("admin.users.reject_successful", {count: result.success});
|
||||
if (result.failed > 0) {
|
||||
message += ' ' + I18n.t("admin.users.reject_failures", {count: result.failed});
|
||||
message += ' ' + I18n.t("admin.user.delete_forbidden", {count: Discourse.SiteSettings.delete_user_max_age});
|
||||
}
|
||||
bootbox.alert(message);
|
||||
controller.refreshUsers();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -343,6 +343,21 @@ Discourse.AdminUser.reopenClass({
|
|||
});
|
||||
},
|
||||
|
||||
bulkReject: function(users) {
|
||||
_.each(users, function(user){
|
||||
user.set('can_approve', false);
|
||||
user.set('selected', false);
|
||||
});
|
||||
|
||||
return Discourse.ajax("/admin/users/reject-bulk", {
|
||||
type: 'DELETE',
|
||||
data: {
|
||||
users: users.map(function(u) { return u.id; }),
|
||||
context: window.location.pathname
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
find: function(username) {
|
||||
return Discourse.ajax("/admin/users/" + username).then(function (result) {
|
||||
result.loadedDetails = true;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
{{#if hasSelection}}
|
||||
<div id='selected-controls'>
|
||||
<button {{action approveUsers}} class='btn'>{{countI18n admin.users.approved_selected countBinding="selectedCount"}}</button>
|
||||
<button {{action rejectUsers}} class='btn btn-danger'>{{countI18n admin.users.reject_selected countBinding="selectedCount"}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
The modal for selecting an avatar
|
||||
|
||||
@class AvatarSelectorController
|
||||
@extends Discourse.Controller
|
||||
@namespace Discourse
|
||||
@uses Discourse.ModalFunctionality
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, {
|
||||
init: function() {
|
||||
// copy some data to support the cancel action
|
||||
this.setProperties(this.get("currentUser").getProperties(
|
||||
"username",
|
||||
"has_uploaded_avatar",
|
||||
"use_uploaded_avatar",
|
||||
"gravatar_template",
|
||||
"uploaded_avatar_template"
|
||||
));
|
||||
},
|
||||
|
||||
toggleUseUploadedAvatar: function(toggle) {
|
||||
this.set("use_uploaded_avatar", toggle);
|
||||
},
|
||||
|
||||
saveAvatarSelection: function() {
|
||||
// sends the information to the server if it has changed
|
||||
if (this.get("use_uploaded_avatar") !== this.get("currentUser.use_uploaded_avatar")) {
|
||||
var data = { use_uploaded_avatar: this.get("use_uploaded_avatar") };
|
||||
Discourse.ajax("/users/" + this.get("currentUser.username") + "/preferences/avatar/toggle", { type: 'PUT', data: data });
|
||||
}
|
||||
// saves the data back to the currentUser object
|
||||
var currentUser = this.get("currentUser");
|
||||
currentUser.setProperties(this.getProperties(
|
||||
"has_uploaded_avatar",
|
||||
"use_uploaded_avatar",
|
||||
"gravatar_template",
|
||||
"uploaded_avatar_template"
|
||||
));
|
||||
if (this.get("use_uploaded_avatar")) {
|
||||
currentUser.set("avatar_template", this.get("uploaded_avatar_template"));
|
||||
} else {
|
||||
currentUser.set("avatar_template", this.get("gravatar_template"));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
/**
|
||||
This controller supports actions related to updating one's avatar
|
||||
|
||||
@class PreferencesAvatarController
|
||||
@extends Discourse.ObjectController
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.PreferencesAvatarController = Discourse.ObjectController.extend({
|
||||
uploading: false,
|
||||
uploadProgress: 0,
|
||||
uploadDisabled: Em.computed.or("uploading"),
|
||||
useGravatar: Em.computed.not("use_uploaded_avatar"),
|
||||
useUploadedAvatar: Em.computed.alias("use_uploaded_avatar"),
|
||||
|
||||
toggleUseUploadedAvatar: function(toggle) {
|
||||
if (this.get("use_uploaded_avatar") !== toggle) {
|
||||
var controller = this;
|
||||
this.set("use_uploaded_avatar", toggle);
|
||||
Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: { use_uploaded_avatar: toggle }})
|
||||
.then(function(result) { controller.set("avatar_template", result.avatar_template); });
|
||||
}
|
||||
},
|
||||
|
||||
uploadButtonText: function() {
|
||||
return this.get("uploading") ? I18n.t("user.change_avatar.uploading") : I18n.t("user.change_avatar.upload");
|
||||
}.property("uploading"),
|
||||
|
||||
uploadAvatar: function() {
|
||||
var controller = this;
|
||||
var $upload = $("#avatar-input");
|
||||
|
||||
// do nothing if no file is selected
|
||||
if (Em.isEmpty($upload.val())) { return; }
|
||||
|
||||
this.set("uploading", true);
|
||||
|
||||
// define the upload endpoint
|
||||
$upload.fileupload({
|
||||
url: Discourse.getURL("/users/" + this.get("username") + "/preferences/avatar"),
|
||||
dataType: "json",
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
// when there is a progression for the upload
|
||||
$upload.on("fileuploadprogressall", function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
controller.set("uploadProgress", progress);
|
||||
});
|
||||
|
||||
// when the upload is successful
|
||||
$upload.on("fileuploaddone", function (e, data) {
|
||||
// set some properties
|
||||
controller.setProperties({
|
||||
has_uploaded_avatar: true,
|
||||
use_uploaded_avatar: true,
|
||||
avatar_template: data.result.url,
|
||||
uploaded_avatar_template: data.result.url
|
||||
});
|
||||
});
|
||||
|
||||
// when there has been an error with the upload
|
||||
$upload.on("fileuploadfail", function (e, data) {
|
||||
Discourse.Utilities.displayErrorForUpload(data);
|
||||
});
|
||||
|
||||
// when the upload is done
|
||||
$upload.on("fileuploadalways", function (e, data) {
|
||||
// prevent automatic upload when selecting a file
|
||||
$upload.fileupload("destroy");
|
||||
$upload.off();
|
||||
// clear file input
|
||||
$upload.val("");
|
||||
// indicate upload is done
|
||||
controller.setProperties({
|
||||
uploading: false,
|
||||
uploadProgress: 0
|
||||
});
|
||||
});
|
||||
|
||||
// *actually* launch the upload
|
||||
$("#avatar-input").fileupload("add", { fileInput: $("#avatar-input") });
|
||||
}
|
||||
});
|
|
@ -171,24 +171,9 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
|||
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
|
||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||
size: options.hash.imageSize,
|
||||
avatarTemplate: Em.get(user, 'avatar_template')
|
||||
avatarTemplate: Em.get(user, options.hash.template || 'avatar_template')
|
||||
}));
|
||||
}, 'avatar_template');
|
||||
|
||||
/**
|
||||
Bound avatar helper.
|
||||
Will rerender whenever the "uploaded_avatar_template" changes.
|
||||
Only available for the current user.
|
||||
|
||||
@method boundUploadedAvatar
|
||||
@for Handlebars
|
||||
**/
|
||||
Ember.Handlebars.registerBoundHelper('boundUploadedAvatar', function(user, options) {
|
||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||
size: options.hash.imageSize,
|
||||
avatarTemplate: Em.get(user, 'uploaded_avatar_template')
|
||||
}));
|
||||
}, 'uploaded_avatar_template');
|
||||
}, 'avatar_template', 'uploaded_avatar_template', 'gravatar_template');
|
||||
|
||||
/**
|
||||
Nicely format a date without a binding since the date doesn't need to change.
|
||||
|
|
|
@ -13,6 +13,13 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
|
|||
|
||||
renderTemplate: function() {
|
||||
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
|
||||
},
|
||||
|
||||
events: {
|
||||
showAvatarSelector: function() {
|
||||
Discourse.Route.showModal(this, 'avatarSelector');
|
||||
this.controllerFor("avatarSelector").init();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -117,32 +124,3 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
|
|||
controller.setProperties({ model: user, newUsername: user.get('username') });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
The route for updating a user's avatar
|
||||
|
||||
@class PreferencesAvatarRoute
|
||||
@extends Discourse.RestrictedUserRoute
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.PreferencesAvatarRoute = Discourse.RestrictedUserRoute.extend({
|
||||
model: function() {
|
||||
return this.modelFor('user');
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
return this.render({ into: 'user', outlet: 'userOutlet' });
|
||||
},
|
||||
|
||||
// A bit odd, but if we leave to /preferences we need to re-render that outlet
|
||||
exit: function() {
|
||||
this._super();
|
||||
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
|
||||
},
|
||||
|
||||
setupController: function(controller, user) {
|
||||
controller.setProperties({ model: user });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class='topic-meta-data span2'>
|
||||
<div class='contents'>
|
||||
<div>
|
||||
<a href='/users/{{unbound username}}'>{{avatar this imageSize="medium"}}</a>
|
||||
<a href='/users/{{unbound username}}'>{{avatar this imageSize="large"}}</a>
|
||||
</div>
|
||||
<h5 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h5>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action saveAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button>
|
||||
<button class='btn' data-dismiss="modal">{{i18n topic.auto_close_cancel}}</button>
|
||||
<a data-dismiss="modal">{{i18n topic.auto_close_cancel}}</a>
|
||||
<button class='btn pull-right' {{action removeAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button>
|
||||
</div>
|
|
@ -0,0 +1,29 @@
|
|||
<div class="modal-body">
|
||||
<div>
|
||||
<input type="radio" id="avatar" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}>
|
||||
<label class="radio" for="avatar">{{avatar controller imageSize="large" template="gravatar_template"}} {{{i18n user.change_avatar.gravatar}}} {{currentUser.email}}</label>
|
||||
<a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn"><i class="icon-pencil"></i></a>
|
||||
<div>
|
||||
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}} {{bindAttr disabled="view.uploadedAvatarDisabled" }}>
|
||||
<label class="radio" for="uploaded_avatar">
|
||||
{{#if has_uploaded_avatar}}
|
||||
{{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}}
|
||||
{{else}}
|
||||
{{i18n user.change_avatar.uploaded_avatar_empty}}
|
||||
{{/if}}
|
||||
</label>
|
||||
<button id="fake-avatar-input" class="btn" {{bindAttr disabled="view.uploading"}} title="{{i18n user.change_avatar.upload_title}}">
|
||||
<i class="icon-picture"></i> {{view.uploadButtonText}}
|
||||
</button>
|
||||
<input type="file" id="avatar-input" accept="image/*" style="display:none">
|
||||
{{#if view.uploading}}
|
||||
<span>{{i18n upload_selector.uploading}} {{view.uploadProgress}}%</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" {{action saveAvatarSelection}} data-dismiss="modal">{{i18n save}}</button>
|
||||
<a data-dismiss="modal">{{i18n cancel}}</a>
|
||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||
<form class="form-horizontal">
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<h3>{{i18n user.change_avatar.title}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.avatar.title}}</label>
|
||||
<div class="controls">
|
||||
<label class="radio">
|
||||
<input type="radio" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}> {{avatar this imageSize="large" template="gravatar_template"}} {{i18n user.change_avatar.gravatar}} <a href="//gravatar.com/emails/" target="_blank" class="btn pad-left" title="{{i18n user.change_avatar.gravatar_title}}">{{i18n user.change}}</a>
|
||||
</label>
|
||||
{{#if has_uploaded_avatar}}
|
||||
<label class="radio">
|
||||
<input type="radio" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}}> {{boundUploadedAvatar this imageSize="large"}} {{i18n user.change_avatar.uploaded_avatar}}
|
||||
</label>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="instructions">{{i18n user.change_avatar.upload_instructions}}</div>
|
||||
<div class="controls">
|
||||
<div>
|
||||
<input type="file" id="avatar-input" accept="image/*">
|
||||
</div>
|
||||
<button {{action uploadAvatar}} {{bindAttr disabled="uploadDisabled"}} class="btn btn-primary">
|
||||
<span class="add-upload"><i class="icon-picture"></i><i class="icon-plus"></i></span>
|
||||
{{uploadButtonText}}
|
||||
</button>
|
||||
{{#if uploading}}
|
||||
<span>{{i18n upload_selector.uploading}} {{uploadProgress}}%</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
|
@ -37,14 +37,15 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.password.title}}</label>
|
||||
<div class="controls">
|
||||
<a href="#" {{action changePassword}} class='btn'><i class="icon-pencil"></i></a> {{passwordProgress}}
|
||||
<a href="#" {{action changePassword}} class='btn'>{{i18n user.change_password.action}}</a> {{passwordProgress}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.avatar.title}}</label>
|
||||
<div class="controls">
|
||||
{{avatar model imageSize="large"}}
|
||||
{{boundAvatar model imageSize="large"}}
|
||||
<button {{action showAvatarSelector}} class="btn pad-left">{{i18n user.change}}</button>
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{#if Discourse.SiteSettings.allow_uploaded_avatars}}
|
||||
|
@ -53,7 +54,6 @@
|
|||
{{else}}
|
||||
{{{i18n user.avatar.instructions.gravatar}}} {{email}}
|
||||
{{/if}}
|
||||
{{#linkTo "preferences.avatar" class="btn pad-left"}}{{i18n user.change}}{{/linkTo}}
|
||||
{{else}}
|
||||
{{{i18n user.avatar.instructions.gravatar}}} {{email}}
|
||||
<a href="//gravatar.com/emails/" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn pad-left">{{i18n user.change}}</a>
|
||||
|
|
|
@ -17,11 +17,12 @@ Discourse.TopicListItemView = Discourse.GroupedView.extend({
|
|||
highlight: function() {
|
||||
var $topic = this.$();
|
||||
var originalCol = $topic.css('backgroundColor');
|
||||
$topic.css({
|
||||
backgroundColor: "#ffffcc"
|
||||
}).animate({
|
||||
backgroundColor: originalCol
|
||||
}, 2500);
|
||||
$topic
|
||||
.addClass('highlighted')
|
||||
.stop()
|
||||
.animate({ backgroundColor: originalCol }, 2500, 'swing', function(){
|
||||
$topic.removeClass('highlighted');
|
||||
});
|
||||
},
|
||||
|
||||
didInsertElement: function() {
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
This view handles the avatar selection interface
|
||||
|
||||
@class AvatarSelectorView
|
||||
@extends Discourse.ModalBodyView
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
|
||||
templateName: 'modal/avatar_selector',
|
||||
classNames: ['avatar-selector'],
|
||||
title: I18n.t('user.change_avatar.title'),
|
||||
uploading: false,
|
||||
uploadProgress: 0,
|
||||
uploadedAvatarDisabled: Em.computed.not("controller.has_uploaded_avatar"),
|
||||
|
||||
didInsertElement: function() {
|
||||
var view = this;
|
||||
var $upload = $("#avatar-input");
|
||||
|
||||
this._super();
|
||||
|
||||
// simulate a click on the hidden file input when clicking on our fake file input
|
||||
$("#fake-avatar-input").on("click", function(e) {
|
||||
// do *NOT* use the cached `$upload` variable, because fileupload is cloning & replacing the input
|
||||
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection
|
||||
$("#avatar-input").click();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// define the upload endpoint
|
||||
$upload.fileupload({
|
||||
url: Discourse.getURL("/users/" + this.get("controller.username") + "/preferences/avatar"),
|
||||
dataType: "json",
|
||||
timeout: 20000,
|
||||
fileInput: $upload
|
||||
});
|
||||
|
||||
// when a file has been selected
|
||||
$upload.on("fileuploadadd", function (e, data) {
|
||||
view.set("uploading", true);
|
||||
});
|
||||
|
||||
// when there is a progression for the upload
|
||||
$upload.on("fileuploadprogressall", function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
view.set("uploadProgress", progress);
|
||||
});
|
||||
|
||||
// when the upload is successful
|
||||
$upload.on("fileuploaddone", function (e, data) {
|
||||
// set some properties
|
||||
view.get("controller").setProperties({
|
||||
has_uploaded_avatar: true,
|
||||
use_uploaded_avatar: true,
|
||||
uploaded_avatar_template: data.result.url
|
||||
});
|
||||
});
|
||||
|
||||
// when there has been an error with the upload
|
||||
$upload.on("fileuploadfail", function (e, data) {
|
||||
Discourse.Utilities.displayErrorForUpload(data);
|
||||
});
|
||||
|
||||
// when the upload is done
|
||||
$upload.on("fileuploadalways", function (e, data) {
|
||||
view.setProperties({ uploading: false, uploadProgress: 0 });
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
$("#fake-avatar-input").off("click");
|
||||
$("#avatar-input").fileupload("destroy");
|
||||
},
|
||||
|
||||
// *HACK* used to select the proper radio button
|
||||
selectedChanged: function() {
|
||||
var view = this;
|
||||
Em.run.next(function() {
|
||||
var value = view.get('controller.use_uploaded_avatar') ? 'uploaded_avatar' : 'gravatar';
|
||||
view.$('input:radio[name="avatar"]').val([value]);
|
||||
});
|
||||
}.observes('controller.use_uploaded_avatar'),
|
||||
|
||||
uploadButtonText: function() {
|
||||
return this.get("uploading") ? I18n.t("uploading") : I18n.t("upload");
|
||||
}.property("uploading")
|
||||
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
This view handles rendering of a user's avatar uploader
|
||||
|
||||
@class PreferencesAvatarView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.PreferencesAvatarView = Discourse.View.extend({
|
||||
templateName: "user/avatar",
|
||||
classNames: ["user-preferences"],
|
||||
|
||||
selectedChanged: function() {
|
||||
var view = this;
|
||||
Em.run.next(function() {
|
||||
var value = view.get("controller.use_uploaded_avatar") ? "uploaded_avatar" : "gravatar";
|
||||
view.$('input:radio[name="avatar"]').val([value]);
|
||||
});
|
||||
}.observes('controller.use_uploaded_avatar')
|
||||
|
||||
});
|
|
@ -1,226 +1,53 @@
|
|||
/**
|
||||
* jQuery Favicon Notify
|
||||
*
|
||||
* Updates the favicon to notify the user of changes. In the original tests I
|
||||
* had an embedded font collection to allow any charachers - I decided that the
|
||||
* ~130Kb and added complexity was overkill. As such it now uses a manual glyph
|
||||
* set meaning that only numerical notifications are possible.
|
||||
*
|
||||
* Dual licensed under the MIT and GPL licenses:
|
||||
*
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*
|
||||
* @author David King
|
||||
* @copyright Copyright (c) 2011 +
|
||||
* @url oodavid.com
|
||||
*/
|
||||
* jQuery Favicon Notify
|
||||
*
|
||||
* Updates the favicon with a number to notify the user of changes.
|
||||
*
|
||||
* iconUrl: Url of favicon image or icon
|
||||
* count: Integer count to place above favicon
|
||||
*
|
||||
* $.faviconNotify(iconUrl, count)
|
||||
*/
|
||||
(function($){
|
||||
var canvas;
|
||||
var bg = '#000000';
|
||||
var fg = '#FFFFFF';
|
||||
var pos = 'br';
|
||||
$.faviconNotify = function(icon, num, myPos, myBg, myFg){
|
||||
// Default the positions
|
||||
myPos = myPos || pos;
|
||||
myFg = myFg || fg;
|
||||
myBg = myBg || bg;
|
||||
// Create a canvas if we need one
|
||||
canvas = canvas || $('<canvas />')[0];
|
||||
if(canvas.getContext){
|
||||
// Load the icon
|
||||
$('<img />').load(function(e){
|
||||
// Load the icon into the canvas
|
||||
canvas.height = canvas.width = 16;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(this, 0, 0);
|
||||
// We gots num?
|
||||
if(num !== undefined){
|
||||
num = parseFloat(num, 10);
|
||||
// Convert the num into a glyphs array
|
||||
var myGlyphs = [];
|
||||
if(num > 99){
|
||||
myGlyphs.push(glyphs['LOTS']);
|
||||
} else {
|
||||
num = num.toString().split('');
|
||||
$.each(num, function(k,v){
|
||||
myGlyphs.push(glyphs[v]);
|
||||
});
|
||||
}
|
||||
if(num>0) {
|
||||
// Merge the glyphs together
|
||||
var combined = [];
|
||||
var glyphHeight = myGlyphs[0].length;
|
||||
$.each(myGlyphs, function(k,v){
|
||||
for(y=0; y<glyphHeight; y++){
|
||||
// First pass?
|
||||
if(combined[y] === undefined) {
|
||||
combined[y] = v[y];
|
||||
} else {
|
||||
// Merge the glyph parts, careful of the boundaries
|
||||
var l = combined[y].length;
|
||||
if(combined[y][(l-1)] === ' '){
|
||||
combined[y] = combined[y].substring(0, (l-1)) + v[y];
|
||||
} else {
|
||||
combined[y] += v[y].substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Figure out our starting position
|
||||
var glyphWidth = combined[0].length;
|
||||
var x = (myPos.indexOf('l') !== -1) ? 0 : (16 - glyphWidth);
|
||||
var y = (myPos.indexOf('t') !== -1) ? 0 : (16 - glyphHeight);
|
||||
// Draw them pixels!
|
||||
for(dX=0; dX<glyphWidth; dX++){
|
||||
for(dY=0; dY<glyphHeight; dY++){
|
||||
var pixel = combined[dY][dX];
|
||||
if(pixel !== ' '){
|
||||
ctx.fillStyle = (pixel === '@') ? myFg : myBg;
|
||||
ctx.fillRect((x+dX), (y+dY), 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the favicon
|
||||
$('link[rel$=icon]').remove();
|
||||
$('head').append($('<link rel="shortcut icon" type="image/x-icon"/>').attr('href', canvas.toDataURL('image/png')));
|
||||
}).attr('src', icon)
|
||||
}
|
||||
};
|
||||
var glyphs = {
|
||||
'0': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
'-@- -@-',
|
||||
'-@- -@-',
|
||||
'-@- -@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'1': [
|
||||
' - ',
|
||||
' -@- ',
|
||||
'-@@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
'-@@@-',
|
||||
' --- ' ],
|
||||
'2': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
' - --@-',
|
||||
' -@@- ',
|
||||
' -@-- ',
|
||||
'-@---- ',
|
||||
'-@@@@@-',
|
||||
' ----- ' ],
|
||||
'3': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
' - --@-',
|
||||
' -@@- ',
|
||||
' - --@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'4': [
|
||||
' -- ',
|
||||
' -@@-',
|
||||
' -@-@-',
|
||||
' -@--@-',
|
||||
'-@---@-',
|
||||
'-@@@@@-',
|
||||
' ----@-',
|
||||
' -@-',
|
||||
' - ' ],
|
||||
'5': [
|
||||
' ----- ',
|
||||
'-@@@@@-',
|
||||
'-@---- ',
|
||||
'-@--- ',
|
||||
'-@@@@- ',
|
||||
' ----@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'6': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
'-@---- ',
|
||||
'-@@@@- ',
|
||||
'-@---@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'7': [
|
||||
' ----- ',
|
||||
'-@@@@@-',
|
||||
' ----@-',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' -@- ',
|
||||
' - ' ],
|
||||
'8': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'9': [
|
||||
' --- ',
|
||||
' -@@@- ',
|
||||
'-@---@-',
|
||||
'-@---@-',
|
||||
' -@@@@-',
|
||||
' ----@-',
|
||||
'-@---@-',
|
||||
' -@@@- ',
|
||||
' --- ' ],
|
||||
'!': [
|
||||
' - ',
|
||||
'-@-',
|
||||
'-@-',
|
||||
'-@-',
|
||||
'-@-',
|
||||
'-@-',
|
||||
' - ',
|
||||
'-@-',
|
||||
' - ' ],
|
||||
'.': [
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' - ',
|
||||
'-@-',
|
||||
' - ' ],
|
||||
'LOTS': [
|
||||
' - -- --- -- ',
|
||||
'-@- -@@-@@@--@@-',
|
||||
'-@--@--@-@--@- ',
|
||||
'-@--@--@-@--@- ',
|
||||
'-@--@--@-@- -@- ',
|
||||
'-@--@--@-@- -@-',
|
||||
'-@--@--@-@----@-',
|
||||
'-@@@-@@--@-@@@- ',
|
||||
' --- -- - --- '
|
||||
]
|
||||
};
|
||||
$.faviconNotify = function(iconUrl, count){
|
||||
var canvas = canvas || $('<canvas />')[0],
|
||||
img = $('<img />')[0],
|
||||
multiplier, fontSize, context, xOffset, yOffset;
|
||||
|
||||
if (canvas.getContext) {
|
||||
if (count < 1) { count = '' }
|
||||
else if (count < 10) { count = ' ' + count }
|
||||
else if (count > 99) { count = '99' }
|
||||
|
||||
img.onload = function () {
|
||||
canvas.height = canvas.width = this.width;
|
||||
multiplier = (this.width / 16);
|
||||
|
||||
fontSize = multiplier * 11;
|
||||
xOffset = multiplier;
|
||||
yOffset = multiplier * 11;
|
||||
|
||||
context = canvas.getContext('2d');
|
||||
context.drawImage(this, 0, 0);
|
||||
context.font = 'bold ' + fontSize + 'px "helvetica", sans-serif';
|
||||
|
||||
context.fillStyle = '#FFF';
|
||||
context.fillText(count, xOffset, yOffset);
|
||||
context.fillText(count, xOffset + 2, yOffset);
|
||||
context.fillText(count, xOffset, yOffset + 2);
|
||||
context.fillText(count, xOffset + 2, yOffset + 2);
|
||||
|
||||
context.fillStyle = '#000';
|
||||
context.fillText(count, xOffset + 1, yOffset + 1);
|
||||
|
||||
$('link[rel$=icon]').remove();
|
||||
$('head').append(
|
||||
$('<link rel="shortcut icon" type="image/x-icon"/>').attr(
|
||||
'href', canvas.toDataURL('image/png')
|
||||
)
|
||||
);
|
||||
};
|
||||
img.src = iconUrl;
|
||||
}
|
||||
};
|
||||
})(jQuery);
|
||||
|
|
|
@ -59,6 +59,9 @@
|
|||
color: $nav-pills-border-color-active;
|
||||
}
|
||||
}
|
||||
&.highlighted {
|
||||
background-color: $highlight;
|
||||
}
|
||||
}
|
||||
th,
|
||||
td {
|
||||
|
|
|
@ -461,7 +461,7 @@
|
|||
background-color: transparent;
|
||||
@include box-shadow(none);
|
||||
h5 {
|
||||
margin-top: 1px;
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
|
|
@ -323,3 +323,18 @@
|
|||
width: 680px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-selector {
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#avatar-input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar {
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,15 @@ class Admin::UsersController < Admin::AdminController
|
|||
render nothing: true
|
||||
end
|
||||
|
||||
def reject_bulk
|
||||
d = UserDestroyer.new(current_user)
|
||||
success_count = 0
|
||||
User.where(id: params[:users]).each do |u|
|
||||
success_count += 1 if guardian.can_delete_user?(u) and d.destroy(u, params.slice(:context)) rescue UserDestroyer::PostsExistError
|
||||
end
|
||||
render json: {success: success_count, failed: (params[:users].try(:size) || 0) - success_count}
|
||||
end
|
||||
|
||||
def destroy
|
||||
user = User.where(id: params[:id]).first
|
||||
guardian.ensure_can_delete_user!(user)
|
||||
|
|
|
@ -376,7 +376,7 @@ class UsersController < ApplicationController
|
|||
user.use_uploaded_avatar = params[:use_uploaded_avatar]
|
||||
user.save!
|
||||
|
||||
render json: { avatar_template: user.avatar_template }
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
private
|
||||
|
|
14
app/mailers/version_mailer.rb
Normal file
14
app/mailers/version_mailer.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
require_dependency 'email/message_builder'
|
||||
|
||||
class VersionMailer < ActionMailer::Base
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
def send_notice
|
||||
if SiteSetting.contact_email.present?
|
||||
build_email( SiteSetting.contact_email,
|
||||
template: 'new_version_mailer',
|
||||
new_version: DiscourseUpdates.latest_version,
|
||||
installed_version: Discourse::VERSION::STRING )
|
||||
end
|
||||
end
|
||||
end
|
|
@ -45,6 +45,7 @@ class SiteSetting < ActiveRecord::Base
|
|||
client_setting(:email_domains_blacklist, 'mailinator.com')
|
||||
client_setting(:email_domains_whitelist)
|
||||
client_setting(:version_checks, true)
|
||||
setting(:new_version_emails, true)
|
||||
client_setting(:min_title_similar_length, 10)
|
||||
client_setting(:min_body_similar_length, 15)
|
||||
# cf. https://github.com/discourse/discourse/pull/462#issuecomment-14991562
|
||||
|
|
|
@ -297,13 +297,6 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.avatar_template(email)
|
||||
user = User.select([:email, :use_uploaded_avatar, :uploaded_avatar_template, :uploaded_avatar_id])
|
||||
.where(email: Email.downcase(email))
|
||||
.first
|
||||
user.avatar_template if user.present?
|
||||
end
|
||||
|
||||
def self.gravatar_template(email)
|
||||
email_hash = self.email_hash(email)
|
||||
"//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
|
||||
|
@ -314,8 +307,8 @@ class User < ActiveRecord::Base
|
|||
# - self oneboxes in open graph data
|
||||
# - emails
|
||||
def small_avatar_url
|
||||
template = User.avatar_template(email)
|
||||
template.gsub("{size}", "60")
|
||||
template = avatar_template
|
||||
template.gsub("{size}", "45")
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
|
|
|
@ -35,9 +35,9 @@
|
|||
|
||||
<%= raw(@markdown_linker.references) %>
|
||||
|
||||
<div class='footer-notice'><%=raw(t :'user_notifications.digest.unsubscribe',
|
||||
<span class='footer-notice'><%=raw(t :'user_notifications.digest.unsubscribe',
|
||||
site_link: site_link,
|
||||
unsubscribe_link: raw(@markdown_linker.create(t('user_notifications.digest.click_here'), email_unsubscribe_path(key: @user.temporary_key)))) %></div>
|
||||
unsubscribe_link: raw(@markdown_linker.create(t('user_notifications.digest.click_here'), email_unsubscribe_path(key: @user.temporary_key)))) %></span>
|
||||
|
||||
<%= raw(@markdown_linker.references) %>
|
||||
|
||||
|
|
|
@ -198,6 +198,7 @@ en:
|
|||
success: "(email sent)"
|
||||
in_progress: "(sending email)"
|
||||
error: "(error)"
|
||||
action: "Send Password Reset Email"
|
||||
|
||||
change_about:
|
||||
title: "Change About Me"
|
||||
|
@ -217,12 +218,11 @@ en:
|
|||
|
||||
change_avatar:
|
||||
title: "Change your avatar"
|
||||
upload_instructions: "Or you could upload an image"
|
||||
upload: "Upload a picture"
|
||||
uploading: "Uploading the picture..."
|
||||
gravatar: "Gravatar"
|
||||
gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, based on"
|
||||
gravatar_title: "Change your avatar on Gravatar's website"
|
||||
uploaded_avatar: "Uploaded picture"
|
||||
uploaded_avatar: "Custom picture"
|
||||
uploaded_avatar_empty: "Add a custom picture"
|
||||
upload_title: "Upload your picture"
|
||||
|
||||
email:
|
||||
title: "Email"
|
||||
|
@ -1232,6 +1232,9 @@ en:
|
|||
approved_selected:
|
||||
one: "approve user"
|
||||
other: "approve users ({{count}})"
|
||||
reject_selected:
|
||||
one: "reject user"
|
||||
other: "reject users ({{count}})"
|
||||
titles:
|
||||
active: 'Active Users'
|
||||
new: 'New Users'
|
||||
|
@ -1245,6 +1248,12 @@ en:
|
|||
moderators: 'Moderators'
|
||||
blocked: 'Blocked Users'
|
||||
banned: 'Banned Users'
|
||||
reject_successful:
|
||||
one: "Successfully rejected 1 user."
|
||||
other: "Successfully rejected %{count} users."
|
||||
reject_failures:
|
||||
one: "Failed to reject 1 user."
|
||||
other: "Failed to reject %{count} users."
|
||||
|
||||
user:
|
||||
ban_failed: "Something went wrong banning this user {{error}}"
|
||||
|
|
|
@ -51,23 +51,23 @@ zh_CN:
|
|||
other: "%{count}年"
|
||||
medium:
|
||||
x_minutes:
|
||||
one: "1分钟"
|
||||
one: "1分钟"
|
||||
other: "%{count}分钟"
|
||||
x_hours:
|
||||
one: "1小时"
|
||||
one: "1小时"
|
||||
other: "%{count}小时"
|
||||
x_days:
|
||||
one: "1天"
|
||||
one: "1天"
|
||||
other: "%{count}天"
|
||||
medium_with_ago:
|
||||
x_minutes:
|
||||
one: "1分钟前"
|
||||
one: "1分钟前"
|
||||
other: "%{count}分钟前"
|
||||
x_hours:
|
||||
one: "1小时之前"
|
||||
one: "1小时之前"
|
||||
other: "%{count}小时之前"
|
||||
x_days:
|
||||
one: "1天前"
|
||||
one: "1天前"
|
||||
other: "%{count}天前"
|
||||
share:
|
||||
topic: '分享一个到本主题的链接'
|
||||
|
@ -86,7 +86,7 @@ zh_CN:
|
|||
generic_error: "抱歉,发生了一个错误。"
|
||||
generic_error_with_reason: "发生一个错误:%{error}"
|
||||
log_in: "登录"
|
||||
age: "寿命"
|
||||
age: "时间"
|
||||
last_post: "最后一帖"
|
||||
admin_title: "管理员"
|
||||
flags_title: "报告"
|
||||
|
@ -132,6 +132,10 @@ zh_CN:
|
|||
saving: "保存中……"
|
||||
saved: "已保存!"
|
||||
|
||||
upload: "上传"
|
||||
uploading: "上传中……"
|
||||
uploaded: "上传完成!"
|
||||
|
||||
choose_topic:
|
||||
none_found: "没有找到主题"
|
||||
title:
|
||||
|
@ -211,6 +215,15 @@ zh_CN:
|
|||
error: "抱歉在修改你的电子邮箱时发生了错误,可能此邮箱已经被使用了?"
|
||||
success: "我们发送了一封确认信到此邮箱地址,请按照邮箱内指示完成确认。"
|
||||
|
||||
change_avatar:
|
||||
title: "修改头像"
|
||||
upload_instructions: "也可上传头像"
|
||||
upload: "上传图片"
|
||||
uploading: "正在上传图片……"
|
||||
gravatar: "Gravatar"
|
||||
gravatar_title: "修改你在Gravatar的头像"
|
||||
uploaded_avatar: "已上传图片"
|
||||
|
||||
email:
|
||||
title: "电子邮箱"
|
||||
instructions: "你的电子邮箱绝不会公开给他人。"
|
||||
|
@ -304,7 +317,9 @@ zh_CN:
|
|||
title: "最后使用的IP地址"
|
||||
avatar:
|
||||
title: "头像"
|
||||
instructions: "我们目前使用 <a href='https://gravatar.com' target='_blank'>Gravatar</a> 来基于你的邮箱生成头像"
|
||||
instructions:
|
||||
gravatar: "正在使用<a href='https://gravatar.com' target='_blank'>Gravatar</a>头像"
|
||||
uploaded_avatar: "正在使用上传的头像"
|
||||
title:
|
||||
title: "头衔"
|
||||
|
||||
|
@ -1166,15 +1181,11 @@ zh_CN:
|
|||
title: "日志"
|
||||
action: "操作"
|
||||
created_at: "创建"
|
||||
screened_emails:
|
||||
title: "被屏蔽的邮件地址"
|
||||
description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。"
|
||||
email: "邮件地址"
|
||||
last_match_at: "最近匹配"
|
||||
match_count: "匹配"
|
||||
actions:
|
||||
block: "阻挡"
|
||||
do_nothing: "无操作"
|
||||
last_match_at: "最近匹配"
|
||||
match_count: "匹配"
|
||||
screened_actions:
|
||||
block: "阻挡"
|
||||
do_nothing: "无操作"
|
||||
staff_actions:
|
||||
title: "管理人员操作"
|
||||
instructions: "点击用户名和操作可以过滤列表。点击头像可以访问用户个人页面。"
|
||||
|
@ -1187,6 +1198,14 @@ zh_CN:
|
|||
actions:
|
||||
delete_user: "删除用户"
|
||||
change_trust_level: "更改信任等级"
|
||||
screened_emails:
|
||||
title: "被屏蔽的邮件地址"
|
||||
description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。"
|
||||
email: "邮件地址"
|
||||
screened_urls:
|
||||
title: "被屏蔽的URL"
|
||||
description: "The URLs listed here were used in posts by users who have been identified as spammers."
|
||||
url: "URL"
|
||||
|
||||
impersonate:
|
||||
title: "假冒用户"
|
||||
|
@ -1232,6 +1251,7 @@ zh_CN:
|
|||
unban_failed: "解禁此用户时发生了错误 {{error}}"
|
||||
ban_duration: "你计划禁止该用户多久?(天)"
|
||||
delete_all_posts: "删除所有帖子"
|
||||
delete_all_posts_confirm: "You are about to delete %{posts} posts and %{topics} topics. Are you sure?"
|
||||
ban: "禁止"
|
||||
unban: "解禁"
|
||||
banned: "已禁止?"
|
||||
|
|
|
@ -529,6 +529,7 @@ en:
|
|||
email_domains_blacklist: "A pipe-delimited list of email domains that are not allowed. Example: mailinator.com|trashmail.net"
|
||||
email_domains_whitelist: "A pipe-delimited list of email domains that users may register with. WARNING: Users with email domains other than those listed will not be allowed."
|
||||
version_checks: "Ping the Discourse Hub for version updates and show version messages on the /admin dashboard"
|
||||
new_version_emails: "Send an email to the contact_email address when a new version is available."
|
||||
|
||||
port: "DEVELOPER ONLY! WARNING! Use this HTTP port rather than the default of port 80. Leave blank for default of 80."
|
||||
force_hostname: "DEVELOPER ONLY! WARNING! Specify a hostname in the URL. Leave blank for default."
|
||||
|
@ -794,6 +795,17 @@ en:
|
|||
|
||||
<small>There should be an unsubscribe footer on every email you send, so let's mock one up. This email was sent by Name of Company, 55 Main Street, Anytown, USA 12345. If you would like to opt out of future emails, [click here to unsubscribe][5].</small>
|
||||
|
||||
new_version_mailer:
|
||||
subject_template: "[%{site_name}] Updates Are Available"
|
||||
text_body_template: |
|
||||
A new version of Discourse is available.
|
||||
|
||||
**New version: %{new_version}**
|
||||
|
||||
Your version: %{installed_version}
|
||||
|
||||
Please upgrade as soon as possible to get the latest fixes and new features.
|
||||
|
||||
system_messages:
|
||||
post_hidden:
|
||||
subject_template: "Post hidden due to community flagging"
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
# Additional MIME types that you'd like nginx to handle go in here
|
||||
types {
|
||||
text/csv csv;
|
||||
}
|
||||
|
||||
upstream discourse {
|
||||
server unix:/var/www/discourse/tmp/sockets/thin.0.sock;
|
||||
server unix:/var/www/discourse/tmp/sockets/thin.1.sock;
|
||||
|
|
|
@ -36,6 +36,7 @@ Discourse::Application.routes.draw do
|
|||
collection do
|
||||
get 'list/:query' => 'users#index'
|
||||
put 'approve-bulk' => 'users#approve_bulk'
|
||||
delete 'reject-bulk' => 'users#reject_bulk'
|
||||
end
|
||||
put 'ban'
|
||||
put 'delete_all_posts'
|
||||
|
|
|
@ -50,6 +50,9 @@ Install necessary packages:
|
|||
# Run these commands as your normal login (e.g. "michael")
|
||||
sudo apt-get -y install build-essential libssl-dev libyaml-dev git libtool libxslt-dev libxml2-dev libpq-dev gawk curl pngcrush python-software-properties
|
||||
|
||||
# If you're on Ubuntu >= 12.10, change:
|
||||
# python-software-properties to software-properties-common
|
||||
|
||||
## Caching: Redis
|
||||
|
||||
Redis is a networked, in memory key-value store cache. Without the Redis caching layer, we'd have to go to the database a lot more often for common information and the site would be slower as a result.
|
||||
|
|
|
@ -24,8 +24,6 @@ module Email
|
|||
img['src'] = "#{Discourse.base_url}#{img['src']}"
|
||||
end
|
||||
end
|
||||
|
||||
style('div.post-indent',' margin-left: 15px; margin-top: 20px; max-width: 694px;')
|
||||
end
|
||||
|
||||
def format_notification
|
||||
|
@ -54,7 +52,7 @@ module Email
|
|||
style('li', 'padding-bottom: 10px')
|
||||
style('div.digest-post', 'margin-left: 15px; margin-top: 20px; max-width: 694px;')
|
||||
style('div.digest-post h1', 'font-size: 20px;')
|
||||
style('div.footer-notice', 'color:#666; font-size:80%')
|
||||
style('span.footer-notice', 'color:#666; font-size:80%')
|
||||
|
||||
@fragment.css('pre').each do |pre|
|
||||
pre.replace(pre.text)
|
||||
|
|
|
@ -46,18 +46,12 @@ class Guardian
|
|||
|
||||
# Can the user edit the obj
|
||||
def can_edit?(obj)
|
||||
if obj && authenticated?
|
||||
edit_method = method_name_for :edit, obj
|
||||
return (edit_method ? send(edit_method, obj) : true)
|
||||
end
|
||||
can_do?(:edit, obj)
|
||||
end
|
||||
|
||||
# Can we delete the object
|
||||
def can_delete?(obj)
|
||||
if obj && authenticated?
|
||||
delete_method = method_name_for :delete, obj
|
||||
return (delete_method ? send(delete_method, obj) : true)
|
||||
end
|
||||
can_do?(:delete, obj)
|
||||
end
|
||||
|
||||
def can_moderate?(obj)
|
||||
|
@ -428,4 +422,11 @@ class Guardian
|
|||
return method_name if respond_to?(method_name)
|
||||
end
|
||||
|
||||
def can_do?(action, obj)
|
||||
if obj && authenticated?
|
||||
action_method = method_name_for action, obj
|
||||
return (action_method ? send(action_method, obj) : true)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -21,8 +21,10 @@ module Jobs
|
|||
# create a temp file with the same extension as the original
|
||||
temp_file = Tempfile.new(["discourse-avatar", File.extname(original_path)])
|
||||
temp_path = temp_file.path
|
||||
#
|
||||
Discourse.store.store_avatar(temp_file, upload, size) if ImageSorcery.new(original_path).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
|
||||
# create a centered square thumbnail
|
||||
if ImageSorcery.new(original_path).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
|
||||
Discourse.store.store_avatar(temp_file, upload, size)
|
||||
end
|
||||
# close && remove temp file
|
||||
temp_file.close!
|
||||
end
|
||||
|
|
|
@ -8,11 +8,18 @@ module Jobs
|
|||
def execute(args)
|
||||
if SiteSetting.version_checks? and (DiscourseUpdates.updated_at.nil? or DiscourseUpdates.updated_at < 1.minute.ago)
|
||||
begin
|
||||
should_send_email = (SiteSetting.new_version_emails and DiscourseUpdates.missing_versions_count and DiscourseUpdates.missing_versions_count == 0)
|
||||
|
||||
json = DiscourseHub.discourse_version_check
|
||||
DiscourseUpdates.latest_version = json['latestVersion']
|
||||
DiscourseUpdates.critical_updates_available = json['criticalUpdates']
|
||||
DiscourseUpdates.missing_versions_count = json['missingVersionsCount']
|
||||
DiscourseUpdates.updated_at = Time.zone.now
|
||||
|
||||
if should_send_email and json['missingVersionsCount'] > 0
|
||||
message = VersionMailer.send_notice
|
||||
Email::Sender.new(message, :new_version).send
|
||||
end
|
||||
rescue => e
|
||||
raise e unless Rails.env == 'development' # Fail version check silently in development mode
|
||||
end
|
||||
|
|
|
@ -57,10 +57,12 @@ module Oneboxer
|
|||
post = topic.posts.first
|
||||
|
||||
posters = topic.posters_summary.map do |p|
|
||||
{username: p[:user][:username],
|
||||
avatar: PrettyText.avatar_img(p[:user][:avatar_template], 'tiny'),
|
||||
description: p[:description],
|
||||
extras: p[:extras]}
|
||||
{
|
||||
username: p[:user].username,
|
||||
avatar: PrettyText.avatar_img(p[:user].avatar_template, 'tiny'),
|
||||
description: p[:description],
|
||||
extras: p[:extras]
|
||||
}
|
||||
end
|
||||
|
||||
category = topic.category
|
||||
|
@ -70,7 +72,7 @@ module Oneboxer
|
|||
|
||||
quote = post.excerpt(SiteSetting.post_onebox_maxlength)
|
||||
args.merge! title: topic.title,
|
||||
avatar: PrettyText.avatar_img(topic.user.username, 'tiny'),
|
||||
avatar: PrettyText.avatar_img(topic.user.avatar_template, 'tiny'),
|
||||
posts_count: topic.posts_count,
|
||||
last_post: FreedomPatches::Rails4.time_ago_in_words(topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose'),
|
||||
age: FreedomPatches::Rails4.time_ago_in_words(topic.created_at, false, scope: :'datetime.distance_in_words_verbose'),
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
<aside class='quote' data-post="1" data-topic="{{topic}}">
|
||||
<div class='title'>
|
||||
<div class='quote-controls'></div>
|
||||
{{{avatar}}}
|
||||
<a href="{{original_url}}">{{title}}</a> {{{category}}} </div>
|
||||
<blockquote>{{{quote}}}
|
||||
<div class='topic-info'>
|
||||
<div class='info-line'>
|
||||
{{posts_count}} posts, last post {{last_post}}, created {{age}}, {{views}} views
|
||||
</div>
|
||||
<div class='posters'>
|
||||
{{#posters}}
|
||||
{{{avatar}}}
|
||||
{{/posters}}
|
||||
</div>
|
||||
<div class='clearfix'>
|
||||
</div>
|
||||
<a href="{{original_url}}">{{title}}</a> {{{category}}}
|
||||
</div>
|
||||
<blockquote>{{{quote}}}
|
||||
<div class='topic-info'>
|
||||
<div class='info-line'>
|
||||
{{posts_count}} posts, last post {{last_post}}, created {{age}}, {{views}} views
|
||||
</div>
|
||||
<div class='posters'>
|
||||
{{#posters}}
|
||||
{{{avatar}}}
|
||||
{{/posters}}
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
</blockquote>
|
||||
</aside>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class Search
|
|||
|
||||
def self.from_user(u)
|
||||
SearchResult.new(type: :user, id: u.username_lower, title: u.username, url: "/users/#{u.username_lower}").tap do |r|
|
||||
r.avatar_template = User.avatar_template(u.email)
|
||||
r.avatar_template = u.avatar_template
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -57,9 +57,8 @@ ENV["RUBY_HEAP_SLOTS_GROWTH_FACTOR"] = "1.25"
|
|||
ENV["RUBY_HEAP_MIN_SLOTS"] = "800000"
|
||||
ENV["RUBY_FREE_MIN"] = "600000"
|
||||
|
||||
|
||||
def port_available? port
|
||||
server = TCPServer.open port
|
||||
server = TCPServer.open("0.0.0.0", port)
|
||||
server.close
|
||||
true
|
||||
rescue Errno::EADDRINUSE
|
||||
|
@ -86,9 +85,9 @@ run("bundle exec ruby script/profile_db_generator.rb")
|
|||
|
||||
def bench(path)
|
||||
puts "Running apache bench warmup"
|
||||
`ab -n 100 http://localhost:#{@port}#{path}`
|
||||
`ab -n 100 http://127.0.0.1:#{@port}#{path}`
|
||||
puts "Benchmarking #{path}"
|
||||
`ab -n 100 -e tmp/ab.csv http://localhost:#{@port}#{path}`
|
||||
`ab -n 100 -e tmp/ab.csv http://127.0.0.1:#{@port}#{path}`
|
||||
|
||||
percentiles = Hash[*[50, 75, 90, 99].zip([]).flatten]
|
||||
CSV.foreach("tmp/ab.csv") do |percent, time|
|
||||
|
@ -105,6 +104,8 @@ begin
|
|||
sleep 1
|
||||
end
|
||||
|
||||
puts "Starting benchmark..."
|
||||
|
||||
home_page = bench("/")
|
||||
topic_page = bench("/t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69")
|
||||
|
||||
|
|
|
@ -196,6 +196,57 @@ describe Admin::UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
context '.reject_bulk' do
|
||||
let(:reject_me) { Fabricate(:user) }
|
||||
let(:reject_me_too) { Fabricate(:user) }
|
||||
|
||||
it 'does nothing without users' do
|
||||
UserDestroyer.any_instance.expects(:destroy).never
|
||||
xhr :delete, :reject_bulk
|
||||
end
|
||||
|
||||
it "won't delete users if not allowed" do
|
||||
Guardian.any_instance.stubs(:can_delete_user?).returns(false)
|
||||
UserDestroyer.any_instance.expects(:destroy).never
|
||||
xhr :delete, :reject_bulk, users: [reject_me.id]
|
||||
end
|
||||
|
||||
it "reports successes" do
|
||||
Guardian.any_instance.stubs(:can_delete_user?).returns(true)
|
||||
UserDestroyer.any_instance.stubs(:destroy).returns(true)
|
||||
xhr :delete, :reject_bulk, users: [reject_me.id, reject_me_too.id]
|
||||
response.should be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
json['success'].to_i.should == 2
|
||||
json['failed'].to_i.should == 0
|
||||
end
|
||||
|
||||
context 'failures' do
|
||||
before do
|
||||
Guardian.any_instance.stubs(:can_delete_user?).returns(true)
|
||||
end
|
||||
|
||||
it 'can handle some successes and some failures' do
|
||||
UserDestroyer.any_instance.stubs(:destroy).with(reject_me, anything).returns(false)
|
||||
UserDestroyer.any_instance.stubs(:destroy).with(reject_me_too, anything).returns(true)
|
||||
xhr :delete, :reject_bulk, users: [reject_me.id, reject_me_too.id]
|
||||
response.should be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
json['success'].to_i.should == 1
|
||||
json['failed'].to_i.should == 1
|
||||
end
|
||||
|
||||
it 'reports failure due to a user still having posts' do
|
||||
UserDestroyer.any_instance.expects(:destroy).with(reject_me, anything).raises(UserDestroyer::PostsExistError)
|
||||
xhr :delete, :reject_bulk, users: [reject_me.id]
|
||||
response.should be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
json['success'].to_i.should == 0
|
||||
json['failed'].to_i.should == 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '.destroy' do
|
||||
before do
|
||||
@delete_me = Fabricate(:user)
|
||||
|
|
18
spec/mailers/version_mailer_spec.rb
Normal file
18
spec/mailers/version_mailer_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe VersionMailer do
|
||||
subject { VersionMailer.send_notice }
|
||||
|
||||
context 'contact_email is blank' do
|
||||
before { SiteSetting.stubs(:contact_email).returns('') }
|
||||
its(:to) { should be_blank }
|
||||
end
|
||||
|
||||
context 'contact_email is set' do
|
||||
before { SiteSetting.stubs(:contact_email).returns('me@example.com') }
|
||||
its(:to) { should == ['me@example.com'] }
|
||||
its(:subject) { should be_present }
|
||||
its(:from) { should == [SiteSetting.notification_email] }
|
||||
its(:body) { should be_present }
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue