mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
Merge remote-tracking branch 'upstream/master' into mywork
This commit is contained in:
commit
3a6a4c628d
2
Gemfile
2
Gemfile
@ -12,7 +12,7 @@ gem 'redcarpet', require: false
|
||||
gem 'activerecord-postgres-hstore'
|
||||
gem 'acts_as_paranoid'
|
||||
gem 'active_attr' # until we get ActiveModel::Model with Rails 4
|
||||
gem 'airbrake', '3.1.2' # errbit is broken with 3.1.3 for now
|
||||
gem 'airbrake', '3.1.2', require: false # errbit is broken with 3.1.3 for now
|
||||
gem 'clockwork', require: false
|
||||
gem 'em-redis'
|
||||
gem 'eventmachine'
|
||||
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
This controller is for the widget that shows the commits to the discourse repo.
|
||||
|
||||
@class AdminGithubCommitsController
|
||||
@extends Ember.ArrayController
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminGithubCommitsController = Ember.ArrayController.extend({
|
||||
goToGithub: function() {
|
||||
window.open('https://github.com/discourse/discourse');
|
||||
}
|
||||
});
|
43
app/assets/javascripts/admin/models/github_commit.js
Normal file
43
app/assets/javascripts/admin/models/github_commit.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
A model for a git commit to the discourse repo, fetched from the github.com api.
|
||||
|
||||
@class GithubCommit
|
||||
@extends Discourse.Model
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.GithubCommit = Discourse.Model.extend({
|
||||
gravatarUrl: function(){
|
||||
if( this.get('author') && this.get('author.gravatar_id') ){
|
||||
return("https://www.gravatar.com/avatar/" + this.get('author.gravatar_id') + ".png?s=38&r=pg&d=identicon");
|
||||
} else {
|
||||
return "https://www.gravatar.com/avatar/b30fff48d257cdd17c4437afac19fd30.png?s=38&r=pg&d=identicon";
|
||||
}
|
||||
}.property("commit"),
|
||||
|
||||
commitUrl: function(){
|
||||
return("https://github.com/discourse/discourse/commit/" + this.get('sha'));
|
||||
}.property("sha"),
|
||||
|
||||
timeAgo: function() {
|
||||
return Date.create(this.get('commit.committer.date')).relative();
|
||||
}.property("commit.committer.date")
|
||||
});
|
||||
|
||||
Discourse.GithubCommit.reopenClass({
|
||||
findAll: function() {
|
||||
var result;
|
||||
result = Em.A();
|
||||
$.ajax( "https://api.github.com/repos/discourse/discourse/commits?callback=callback", {
|
||||
dataType: 'jsonp',
|
||||
type: 'get',
|
||||
data: { per_page: 10 },
|
||||
success: function(response, textStatus, jqXHR) {
|
||||
response.data.each(function(commit) {
|
||||
result.pushObject( Discourse.GithubCommit.create(commit) );
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
@ -8,12 +8,9 @@
|
||||
**/
|
||||
Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
||||
setupController: function(c) {
|
||||
if( !c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt') ) {
|
||||
this.checkVersion(c);
|
||||
}
|
||||
if( !c.get('reportsCheckedAt') || Date.create('1 hour ago') > c.get('reportsCheckedAt') ) {
|
||||
this.fetchReports(c);
|
||||
}
|
||||
this.checkVersion(c);
|
||||
this.fetchReports(c);
|
||||
this.fetchGithubCommits(c);
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
@ -21,7 +18,7 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
||||
},
|
||||
|
||||
checkVersion: function(c) {
|
||||
if( Discourse.SiteSettings.version_checks ) {
|
||||
if( Discourse.SiteSettings.version_checks && (!c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt')) ) {
|
||||
c.set('versionCheckedAt', new Date());
|
||||
Discourse.VersionCheck.find().then(function(vc) {
|
||||
c.set('versionCheck', vc);
|
||||
@ -31,11 +28,20 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
||||
},
|
||||
|
||||
fetchReports: function(c) {
|
||||
// TODO: use one request to get all reports, or maybe one request for all dashboard data including version check.
|
||||
c.set('reportsCheckedAt', new Date());
|
||||
['visits', 'signups', 'topics', 'posts'].each(function(reportType){
|
||||
c.set(reportType, Discourse.Report.find(reportType));
|
||||
});
|
||||
if( !c.get('reportsCheckedAt') || Date.create('1 hour ago') > c.get('reportsCheckedAt') ) {
|
||||
// TODO: use one request to get all reports, or maybe one request for all dashboard data including version check.
|
||||
c.set('reportsCheckedAt', new Date());
|
||||
['visits', 'signups', 'topics', 'posts'].each(function(reportType){
|
||||
c.set(reportType, Discourse.Report.find(reportType));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchGithubCommits: function(c) {
|
||||
if( !c.get('commitsCheckedAt') || Date.create('1 hour ago') > c.get('commitsCheckedAt') ) {
|
||||
c.set('commitsCheckedAt', new Date());
|
||||
c.set('githubCommits', Discourse.GithubCommit.findAll());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
18
app/assets/javascripts/admin/templates/commits.js.handlebars
Normal file
18
app/assets/javascripts/admin/templates/commits.js.handlebars
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="commits-widget">
|
||||
<div class="header" {{action "goToGithub"}}>
|
||||
<h1>Latest Changes</h4>
|
||||
</div>
|
||||
<ul class="commits-list">
|
||||
{{#each controller}}
|
||||
<li>
|
||||
<div class="left">
|
||||
<img {{bindAttr src="gravatarUrl"}}>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="commit-message"><a {{bindAttr href="commitUrl"}} target="_blank">{{ commit.message }}</a></span><br/>
|
||||
<span class="commit-meta">by <span class="committer-name">{{ commit.author.name }}</span> - <span class="commit-time">{{ timeAgo }}</span></span>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<div class="version-check-right">
|
||||
<iframe src="/commits-widget/index.html?limit=10&height=160" allowtransparency="true" frameborder="0" scrolling="no" width="502px" height="162px"></iframe>
|
||||
{{ render admin_github_commits githubCommits }}
|
||||
</div>
|
||||
|
||||
<div class='clearfix'></div>
|
||||
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
A view for showing commits to the discourse repo.
|
||||
|
||||
@class AdminGithubCommitsView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminGithubCommitsView = Discourse.View.extend({
|
||||
templateName: 'admin/templates/commits'
|
||||
});
|
@ -24,6 +24,8 @@
|
||||
//= require ./discourse/helpers/i18n_helpers
|
||||
//= require ./discourse
|
||||
|
||||
//= require ./locales/date_locales.js
|
||||
|
||||
// Stuff we need to load first
|
||||
//= require_tree ./discourse/mixins
|
||||
//= require ./discourse/views/view
|
||||
|
@ -29,50 +29,44 @@ Discourse.Development = {
|
||||
},
|
||||
|
||||
after: function(data, owner, args) {
|
||||
var ary, f, n, v, _ref;
|
||||
|
||||
if (typeof console === "undefined") return;
|
||||
if (console === null) return;
|
||||
|
||||
var f, n, v;
|
||||
if (owner && data.time > 10) {
|
||||
|
||||
f = function(name, data) {
|
||||
if (data && data.count) return name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms";
|
||||
};
|
||||
|
||||
if (console && console.group) {
|
||||
if (console.group) {
|
||||
console.group(f(name, data));
|
||||
} else {
|
||||
console.log("");
|
||||
console.log(f(name, data));
|
||||
}
|
||||
|
||||
ary = [];
|
||||
_ref = window.probes;
|
||||
for (n in _ref) {
|
||||
v = _ref[n];
|
||||
if (n === name || v.time < 1) {
|
||||
continue;
|
||||
}
|
||||
ary.push({
|
||||
k: n,
|
||||
v: v
|
||||
});
|
||||
var ary = [];
|
||||
for (n in window.probes) {
|
||||
v = window.probes[n];
|
||||
if (n === name || v.time < 1) continue;
|
||||
ary.push({ k: n, v: v });
|
||||
}
|
||||
ary.sortBy(function(item) {
|
||||
if (item.v && item.v.time) {
|
||||
return -item.v.time;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (item.v && item.v.time) return -item.v.time;
|
||||
return 0;
|
||||
}).each(function(item) {
|
||||
var output = f("" + item.k, item.v);
|
||||
if (output) {
|
||||
return console.log(output);
|
||||
console.log(output);
|
||||
}
|
||||
});
|
||||
if (typeof console !== "undefined" && console !== null) {
|
||||
if (typeof console.groupEnd === "function") {
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
if (console.group) {
|
||||
console.groupEnd();
|
||||
}
|
||||
return window.probes.clear();
|
||||
window.probes.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -8,13 +8,13 @@
|
||||
* someFunction = window.probes.measure(someFunction, {
|
||||
* name: "somename" // or function(args) { return "name"; },
|
||||
* before: function(data, owner, args) {
|
||||
* // if owner is true, we are not in a recursive function call.
|
||||
* // if owner is true, we are not in a recursive function call.
|
||||
* //
|
||||
* // data contains the bucker of data already measuer
|
||||
* // data.count >= 0
|
||||
* // data.time is the total time measured till now
|
||||
* // data contains the bucker of data already measuer
|
||||
* // data.count >= 0
|
||||
* // data.time is the total time measured till now
|
||||
* //
|
||||
* // arguments contains the original arguments sent to the function
|
||||
* // arguments contains the original arguments sent to the function
|
||||
* },
|
||||
* after: function(data, owner, args) {
|
||||
* // same format as before
|
||||
@ -22,9 +22,9 @@
|
||||
* });
|
||||
*
|
||||
*
|
||||
* // minimal
|
||||
* // minimal
|
||||
* someFunction = window.probes.measure(someFunction, "someFunction");
|
||||
*
|
||||
*
|
||||
* */
|
||||
(function(){
|
||||
var measure, clear;
|
||||
@ -92,8 +92,7 @@
|
||||
start = now();
|
||||
callStart = start;
|
||||
}
|
||||
else if(after)
|
||||
{
|
||||
else if(after) {
|
||||
callStart = now();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
/*global humaneDate:true */
|
||||
|
||||
/**
|
||||
Breaks up a long string
|
||||
|
||||
@ -162,7 +160,7 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||
Handlebars.registerHelper('unboundDate', function(property, options) {
|
||||
var dt;
|
||||
dt = new Date(Ember.Handlebars.get(this, property, options));
|
||||
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
|
||||
return dt.format("long");
|
||||
});
|
||||
|
||||
/**
|
||||
@ -176,9 +174,9 @@ Handlebars.registerHelper('editDate', function(property, options) {
|
||||
dt = Date.create(Ember.Handlebars.get(this, property, options));
|
||||
yesterday = new Date() - (60 * 60 * 24 * 1000);
|
||||
if (yesterday > dt.getTime()) {
|
||||
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
|
||||
return dt.format("long");
|
||||
} else {
|
||||
return humaneDate(dt);
|
||||
return dt.relative();
|
||||
}
|
||||
});
|
||||
|
||||
@ -215,7 +213,7 @@ Handlebars.registerHelper('number', function(property, options) {
|
||||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('date', function(property, options) {
|
||||
var displayDate, dt, fiveDaysAgo, fullReadable, humanized, leaveAgo, val;
|
||||
var displayDate, dt, fiveDaysAgo, oneMinuteAgo, fullReadable, humanized, leaveAgo, val;
|
||||
if (property.hash) {
|
||||
if (property.hash.leaveAgo) {
|
||||
leaveAgo = property.hash.leaveAgo === "true";
|
||||
@ -229,23 +227,26 @@ Handlebars.registerHelper('date', function(property, options) {
|
||||
return new Handlebars.SafeString("—");
|
||||
}
|
||||
dt = new Date(val);
|
||||
fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
|
||||
fullReadable = dt.format("long");
|
||||
displayDate = "";
|
||||
fiveDaysAgo = (new Date()) - 432000000;
|
||||
if (fiveDaysAgo > (dt.getTime())) {
|
||||
oneMinuteAgo = (new Date()) - 60000;
|
||||
if (oneMinuteAgo <= dt.getTime() && dt.getTime() <= (new Date())) {
|
||||
displayDate = Em.String.i18n("now");
|
||||
} else if (fiveDaysAgo > (dt.getTime())) {
|
||||
if ((new Date()).getFullYear() !== dt.getFullYear()) {
|
||||
displayDate = dt.format("{d} {Mon} '{yy}");
|
||||
displayDate = dt.format("short");
|
||||
} else {
|
||||
displayDate = dt.format("{d} {Mon}");
|
||||
displayDate = dt.format("short_no_year");
|
||||
}
|
||||
} else {
|
||||
humanized = humaneDate(dt);
|
||||
humanized = dt.relative();
|
||||
if (!humanized) {
|
||||
return "";
|
||||
}
|
||||
displayDate = humanized;
|
||||
if (!leaveAgo) {
|
||||
displayDate = displayDate.replace(' ago', '');
|
||||
displayDate = (dt.millisecondsAgo()).duration();
|
||||
}
|
||||
}
|
||||
return new Handlebars.SafeString("<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>");
|
||||
@ -267,4 +268,4 @@ Handlebars.registerHelper('personalizedName', function(property, options) {
|
||||
return name;
|
||||
}
|
||||
return Em.String.i18n('you');
|
||||
});
|
||||
});
|
||||
|
@ -36,7 +36,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><label>Must be unique, no spaces. People can mention you as @username.</label></td>
|
||||
<td><label>{{i18n user.username.instructions}}</label></td>
|
||||
</tr>
|
||||
|
||||
{{#if view.passwordRequired}}
|
||||
@ -50,7 +50,7 @@
|
||||
{{/if}}
|
||||
|
||||
<tr class="password-confirmation">
|
||||
<td><label for='new-account-password-confirmation'>Password Again</label></td>
|
||||
<td><label for='new-account-password-confirmation'>{{i18n user.password_confirmation.title}}</label></td>
|
||||
<td>
|
||||
{{view Ember.TextField valueBinding="view.accountPasswordConfirm" type="password" id="new-account-password-confirmation"}}
|
||||
{{view Ember.TextField valueBinding="view.accountChallenge" id="new-account-challenge"}}
|
||||
|
@ -13,8 +13,8 @@
|
||||
{{#linkTo "preferences.username" class="btn pad-left"}}{{i18n user.change_username.action}}{{/linkTo}}
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{{i18n user.username.instructions username="content.username"}}}
|
||||
</div>
|
||||
{{{i18n user.username.short_instructions username="content.username"}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{i18n user.name.instructions}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.password.title}}</label>
|
||||
<label class="control-label">{{i18n user.password.title}}</label>
|
||||
<div class="controls">
|
||||
<a href="#" {{action changePassword target="controller"}} class='btn'>{{i18n user.change_password}}</a> {{controller.passwordProgress}}
|
||||
</div>
|
||||
@ -52,32 +52,32 @@
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{{i18n user.avatar.instructions}}} {{content.email}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.bio}}</label>
|
||||
<div class="controls">
|
||||
{{view Discourse.PagedownEditor valueBinding="content.bio_raw"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.bio}}</label>
|
||||
<div class="controls">
|
||||
{{view Discourse.PagedownEditor valueBinding="content.bio_raw"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.website}}</label>
|
||||
<div class="controls">
|
||||
{{view Ember.TextField valueBinding="content.website" classNames="input-xxlarge"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.email_settings}}</label>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.email_settings}}</label>
|
||||
<div class="controls">
|
||||
<label>{{view Ember.Checkbox checkedBinding="content.email_digests"}}
|
||||
{{i18n user.email_digests.title}}</label>
|
||||
|
||||
{{#if content.email_digests}}
|
||||
<div class='control-indent'>
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.digestFrequencies" valueBinding="content.digest_after_days"}}
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.digestFrequencies" valueBinding="content.digest_after_days"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<label>{{view Ember.Checkbox checkedBinding="content.email_private_messages"}}
|
||||
@ -87,19 +87,19 @@
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{i18n user.email.frequency}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group other">
|
||||
<label class="control-label">{{i18n user.other_settings}}</label>
|
||||
|
||||
<div class="control-group other">
|
||||
<label class="control-label">{{i18n user.other_settings}}</label>
|
||||
<div class="controls">
|
||||
<label>{{i18n user.auto_track_topics}}</label>
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.autoTrackDurations" valueBinding="content.auto_track_topics_after_msecs"}}
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.autoTrackDurations" valueBinding="content.auto_track_topics_after_msecs"}}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="controls">
|
||||
<label>{{i18n user.new_topic_duration.label}}</label>
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.considerNewTopicOptions" valueBinding="content.new_topic_duration_minutes"}}
|
||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.considerNewTopicOptions" valueBinding="content.new_topic_duration_minutes"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -40,6 +40,7 @@ Discourse.TopicListItemView = Discourse.View.extend({
|
||||
}
|
||||
// highlight new topics that have been loaded from the server or the one we just created
|
||||
else if (this.get('content.highlight')) {
|
||||
this.set('content.highlight', false);
|
||||
this.highlight();
|
||||
}
|
||||
}
|
||||
|
@ -276,8 +276,19 @@ Discourse.CreateAccountView = Discourse.ModalBodyView.extend({
|
||||
_this.set('formSubmitted', false);
|
||||
return _this.flash(Em.String.i18n('create_account.failed'), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
didInsertElement: function(e) {
|
||||
// allows the submission the form when pressing 'ENTER' on *any* text input field
|
||||
// but only when the submit button is enabled
|
||||
var _this = this;
|
||||
return Em.run.next(function() {
|
||||
return $("input[type='text']").keydown(function(e) {
|
||||
if (_this.get('submitDisabled') === false && e.keyCode === 13) {
|
||||
return _this.createAccount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -32,9 +32,15 @@ Discourse.PagedownEditor = Ember.ContainerView.extend({
|
||||
didInsertElement: function() {
|
||||
var $wmdInput = $('#wmd-input');
|
||||
$wmdInput.data('init', true);
|
||||
this.editor = Discourse.Markdown.createEditor();
|
||||
return this.editor.run();
|
||||
}
|
||||
this.set('editor', Discourse.Markdown.createEditor());
|
||||
return this.get('editor').run();
|
||||
},
|
||||
|
||||
observeValue: (function() {
|
||||
var editor = this.get('editor');
|
||||
if (!editor) return;
|
||||
Ember.run.next(null, function() { editor.refreshPreview(); });
|
||||
}).observes('value')
|
||||
|
||||
});
|
||||
|
||||
|
134
app/assets/javascripts/external/humane.js
vendored
134
app/assets/javascripts/external/humane.js
vendored
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Javascript Humane Dates
|
||||
* Copyright (c) 2008 Dean Landolt (deanlandolt.com)
|
||||
* Re-write by Zach Leatherman (zachleat.com)
|
||||
*
|
||||
* Adopted from the John Resig's pretty.js
|
||||
* at http://ejohn.org/blog/javascript-pretty-date
|
||||
* and henrah's proposed modification
|
||||
* at http://ejohn.org/blog/javascript-pretty-date/#comment-297458
|
||||
*
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
function humaneDate(date, compareTo){
|
||||
|
||||
if(!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lang = {
|
||||
ago: 'ago',
|
||||
from: '',
|
||||
now: 'just now',
|
||||
minute: 'minute',
|
||||
minutes: 'minutes',
|
||||
hour: 'hour',
|
||||
hours: 'hours',
|
||||
day: 'day',
|
||||
days: 'days',
|
||||
week: 'week',
|
||||
weeks: 'weeks',
|
||||
month: 'month',
|
||||
months: 'months',
|
||||
year: 'year',
|
||||
years: 'years'
|
||||
},
|
||||
formats = [
|
||||
[60, lang.now],
|
||||
[3600, lang.minute, lang.minutes, 60], // 60 minutes, 1 minute
|
||||
[86400, lang.hour, lang.hours, 3600], // 24 hours, 1 hour
|
||||
[604800, lang.day, lang.days, 86400], // 7 days, 1 day
|
||||
[2628000, lang.week, lang.weeks, 604800], // ~1 month, 1 week
|
||||
[31536000, lang.month, lang.months, 2628000], // 1 year, ~1 month
|
||||
[Infinity, lang.year, lang.years, 31536000] // Infinity, 1 year
|
||||
],
|
||||
isString = typeof date == 'string',
|
||||
date = isString ?
|
||||
new Date(('' + date).replace(/-/g,"/").replace(/[TZ]/g," ")) :
|
||||
date,
|
||||
compareTo = compareTo || new Date,
|
||||
seconds = (compareTo - date +
|
||||
(compareTo.getTimezoneOffset() -
|
||||
// if we received a GMT time from a string, doesn't include time zone bias
|
||||
// if we got a date object, the time zone is built in, we need to remove it.
|
||||
(isString ? 0 : date.getTimezoneOffset())
|
||||
) * 60000
|
||||
) / 1000,
|
||||
token;
|
||||
|
||||
if(seconds < 0) {
|
||||
seconds = Math.abs(seconds);
|
||||
token = lang.from ? ' ' + lang.from : '';
|
||||
} else {
|
||||
token = lang.ago ? ' ' + lang.ago : '';
|
||||
}
|
||||
|
||||
/*
|
||||
* 0 seconds && < 60 seconds Now
|
||||
* 60 seconds 1 Minute
|
||||
* > 60 seconds && < 60 minutes X Minutes
|
||||
* 60 minutes 1 Hour
|
||||
* > 60 minutes && < 24 hours X Hours
|
||||
* 24 hours 1 Day
|
||||
* > 24 hours && < 7 days X Days
|
||||
* 7 days 1 Week
|
||||
* > 7 days && < ~ 1 Month X Weeks
|
||||
* ~ 1 Month 1 Month
|
||||
* > ~ 1 Month && < 1 Year X Months
|
||||
* 1 Year 1 Year
|
||||
* > 1 Year X Years
|
||||
*
|
||||
* Single units are +10%. 1 Year shows first at 1 Year + 10%
|
||||
*/
|
||||
|
||||
function normalize(val, single)
|
||||
{
|
||||
var margin = 0.1;
|
||||
if(val >= single && val <= single * (1+margin)) {
|
||||
return single;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
for(var i = 0, format = formats[0]; formats[i]; format = formats[++i]) {
|
||||
if(seconds < format[0]) {
|
||||
if(i === 0) {
|
||||
// Now
|
||||
return format[1];
|
||||
}
|
||||
|
||||
var val = Math.ceil(normalize(seconds, format[3]) / (format[3]));
|
||||
return val +
|
||||
' ' +
|
||||
(val != 1 ? format[2] : format[1]) +
|
||||
(i > 0 ? token : '');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(typeof jQuery != 'undefined') {
|
||||
jQuery.fn.humaneDates = function(options)
|
||||
{
|
||||
var settings = jQuery.extend({
|
||||
'lowercase': false
|
||||
}, options);
|
||||
|
||||
return this.each(function()
|
||||
{
|
||||
var $t = jQuery(this),
|
||||
date = $t.attr('datetime') || $t.attr('title');
|
||||
|
||||
date = humaneDate(date);
|
||||
|
||||
if(date && settings['lowercase']) {
|
||||
date = date.toLowerCase();
|
||||
}
|
||||
|
||||
if(date && $t.html() != date) {
|
||||
// don't modify the dom if we don't have to
|
||||
$t.html(date);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
36
app/assets/javascripts/locales/date_locales.js
Normal file
36
app/assets/javascripts/locales/date_locales.js
Normal file
@ -0,0 +1,36 @@
|
||||
// fix EN locale
|
||||
Date.getLocale('en').short_no_year = '{d} {Mon}';
|
||||
|
||||
// create CS locale
|
||||
Date.addLocale('cs', {
|
||||
'plural': true,
|
||||
'capitalizeUnit': false,
|
||||
'months': 'ledna,února,března,dubna,května,června,července,srpna,září,října,listopadu,prosince',
|
||||
'weekdays': 'neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota',
|
||||
'units': 'milisekund:a|y||ou|ami,sekund:a|y||ou|ami,minut:a|y||ou|ami,hodin:a|y||ou|ami,den|dny|dnů|dnem|dny,týden|týdny|týdnů|týdnem|týdny,měsíc:|e|ů|em|emi,rok|roky|let|rokem|lety',
|
||||
'short': '{d}. {month} {yyyy}',
|
||||
'short_no_year': '{d}. {month}',
|
||||
'long': '{d}. {month} {yyyy} {H}:{mm}',
|
||||
'full': '{weekday} {d}. {month} {yyyy} {H}:{mm}:{ss}',
|
||||
'relative': function(num, unit, ms, format) {
|
||||
var numberWithUnit, last = num.toString().slice(-1);
|
||||
var mult;
|
||||
if (format === 'past' || format === 'future') {
|
||||
if (num === 1) mult = 3;
|
||||
else mult = 4;
|
||||
} else {
|
||||
if (num === 1) mult = 0;
|
||||
else if (num >= 2 && num <= 4) mult = 1;
|
||||
else mult = 2;
|
||||
}
|
||||
numberWithUnit = num + ' ' + this.units[(mult * 8) + unit];
|
||||
switch(format) {
|
||||
case 'duration': return numberWithUnit;
|
||||
case 'past': return 'před ' + numberWithUnit;
|
||||
case 'future': return 'za ' + numberWithUnit;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// set the current date locale
|
||||
Date.setLocale(I18n.locale);
|
@ -1,6 +1,7 @@
|
||||
// these are the styles associated with the Discourse admin section
|
||||
@import "foundation/variables";
|
||||
@import "foundation/mixins";
|
||||
@import "foundation/helpers";
|
||||
|
||||
.admin-content {
|
||||
margin-bottom: 50px;
|
||||
@ -318,4 +319,107 @@ table {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.commits-widget {
|
||||
border: solid 1px #ccc;
|
||||
width: 500px;
|
||||
height: 160px;
|
||||
|
||||
ul, li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
a {
|
||||
color: #222;
|
||||
text-decoration: none
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
height: 30px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
|
||||
background-color:#e1e1e1;
|
||||
background-image:-moz-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-ms-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-o-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-webkit-gradient(linear, left top, left bottom, from(#f1f1f1), to(#e1e1e1));
|
||||
background-image:-webkit-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:linear-gradient(center top, #f1f1f1 0%, #e1e1e1 100%);
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#f1f1f1', EndColorStr='#e1e1e1');
|
||||
cursor: pointer;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 5px 0 0 8px;
|
||||
display: inline-block;
|
||||
line-height: 1.0em;
|
||||
}
|
||||
}
|
||||
|
||||
.header:hover h1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.commits-list {
|
||||
height: 129px;
|
||||
overflow-y:auto;
|
||||
|
||||
li {
|
||||
@extend .clearfix;
|
||||
line-height: 1.0em;
|
||||
padding: 6px 8px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
background-color:#eee;
|
||||
background-image:-moz-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-ms-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-o-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-webkit-gradient(linear, left top, left bottom, from(#fafafa), to(#eee));
|
||||
background-image:-webkit-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:linear-gradient(center top, #fafafa 0%, #eee 100%);
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#f1f1f1', EndColorStr='#e1e1e1');
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
.right {
|
||||
margin-left: 52px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 2px;
|
||||
border: solid 1px #ccc;
|
||||
padding: 2px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.commit-meta {
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.committer-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
li:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -103,11 +103,11 @@ class TopicViewSerializer < ApplicationSerializer
|
||||
end
|
||||
|
||||
def can_reply_as_new_topic
|
||||
scope.can_reply_as_new_topic?(object.topic)
|
||||
true
|
||||
end
|
||||
|
||||
def include_can_reply_as_new_topic?
|
||||
scope.can_create?(Post, object.topic)
|
||||
scope.can_reply_as_new_topic?(object.topic)
|
||||
end
|
||||
|
||||
def can_create_post
|
||||
|
@ -88,11 +88,9 @@ module Discourse
|
||||
# Our templates shouldn't start with 'discourse/templates'
|
||||
config.handlebars.templates_root = 'discourse/templates'
|
||||
|
||||
require 'discourse_redis'
|
||||
# Use redis for our cache
|
||||
redis_config = YAML::load(File.open("#{Rails.root}/config/redis.yml"))[Rails.env]
|
||||
redis_store = ActiveSupport::Cache::RedisStore.new "redis://#{redis_config['host']}:#{redis_config['port']}/#{redis_config['cache_db']}"
|
||||
redis_store.options[:namespace] = -> { DiscourseRedis.namespace }
|
||||
config.cache_store = redis_store
|
||||
config.cache_store = DiscourseRedis.new_redis_store
|
||||
|
||||
# Test with rack::cache disabled. Nginx does this for us
|
||||
config.action_dispatch.rack_cache = nil
|
||||
|
@ -33,12 +33,6 @@ Discourse::Application.configure do
|
||||
config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external/ember.js"
|
||||
config.handlebars.precompile = false
|
||||
|
||||
# a bit hacky but works
|
||||
config.after_initialize do
|
||||
config.middleware.delete Airbrake::UserInformer
|
||||
config.middleware.delete Airbrake::Rack
|
||||
end
|
||||
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
|
||||
config.action_mailer.raise_delivery_errors = true
|
||||
|
20
config/initializers/01-redis.rb
Normal file
20
config/initializers/01-redis.rb
Normal file
@ -0,0 +1,20 @@
|
||||
require "#{Rails.root}/lib/discourse_redis"
|
||||
|
||||
$redis = DiscourseRedis.new
|
||||
|
||||
if Rails.env.development? && !ENV['DO_NOT_FLUSH_REDIS']
|
||||
puts "Flushing redis (development mode)"
|
||||
$redis.flushall
|
||||
end
|
||||
|
||||
if defined?(PhusionPassenger)
|
||||
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
||||
if forked
|
||||
# We're in smart spawning mode.
|
||||
$redis = DiscourseRedis.new
|
||||
Discourse::Application.config.cache_store.reconnect
|
||||
else
|
||||
# We're in conservative spawning mode. We don't need to do anything.
|
||||
end
|
||||
end
|
||||
end
|
14
config/initializers/airbrake.rb.sample
Normal file
14
config/initializers/airbrake.rb.sample
Normal file
@ -0,0 +1,14 @@
|
||||
# Internally Dicourse uses Errbit for error logging,
|
||||
# you can to by setting up an instance and amending this file
|
||||
require 'airbrake'
|
||||
Airbrake.configure do |config|
|
||||
config.api_key = 'YOUR API KEY'
|
||||
config.host = 'errors.example.com'
|
||||
config.port = 80
|
||||
config.secure = config.port == 443
|
||||
|
||||
|
||||
# IP Spoof errors can be ignored
|
||||
config.ignore << "ActionDispatch::RemoteIp::IpSpoofAttackError"
|
||||
end
|
||||
|
@ -1,16 +1,7 @@
|
||||
require "#{Rails.root}/lib/discourse_redis"
|
||||
|
||||
$redis = DiscourseRedis.new
|
||||
|
||||
if Rails.env.development? && !ENV['DO_NOT_FLUSH_REDIS']
|
||||
puts "Flushing redis (development mode)"
|
||||
$redis.flushall
|
||||
end
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = { :url => $redis.url, :namespace => 'sidekiq' }
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = { :url => $redis.url, :namespace => 'sidekiq' }
|
||||
end
|
||||
end
|
||||
|
@ -24,6 +24,7 @@ cs:
|
||||
you: "Vy"
|
||||
ok: "ok"
|
||||
or: "nebo"
|
||||
now: "právě teď"
|
||||
|
||||
suggested_topics:
|
||||
title: "Doporučená témata"
|
||||
@ -279,7 +280,6 @@ cs:
|
||||
show_preview: 'zobrazit náhled »'
|
||||
hide_preview: '« skrýt náhled'
|
||||
|
||||
quote_title: "Citovat příspěvek"
|
||||
bold_title: "Tučně"
|
||||
bold_text: "tučný text"
|
||||
italic_title: "Kurzíva"
|
||||
|
@ -24,6 +24,7 @@ en:
|
||||
you: "You"
|
||||
ok: "ok"
|
||||
or: "or"
|
||||
now: "just now"
|
||||
|
||||
suggested_topics:
|
||||
title: "Suggested Topics"
|
||||
@ -91,7 +92,8 @@ en:
|
||||
ok: "Your name looks good."
|
||||
username:
|
||||
title: "Username"
|
||||
instructions: "People can mention you as @{{username}}."
|
||||
instructions: "Must be unique, no spaces. People can mention you as @username."
|
||||
short_instructions: "People can mention you as @{{username}}."
|
||||
available: "Your username is available."
|
||||
global_match: "Email matches the registered username."
|
||||
global_mismatch: "Already registered. Try {{suggestion}}?"
|
||||
@ -101,6 +103,9 @@ en:
|
||||
checking: "Checking username availability..."
|
||||
enter_email: 'Username found. Enter matching email.'
|
||||
|
||||
password_confirmation:
|
||||
title: "Password Again"
|
||||
|
||||
last_posted: "Last Post"
|
||||
last_emailed: "Last Emailed"
|
||||
last_seen: "Last Seen"
|
||||
@ -279,7 +284,6 @@ en:
|
||||
show_preview: 'show preview »'
|
||||
hide_preview: '« hide preview'
|
||||
|
||||
quote_title: "Quote Post"
|
||||
bold_title: "Strong"
|
||||
bold_text: "strong text"
|
||||
italic_title: "Emphasis"
|
||||
|
@ -95,7 +95,8 @@ fr:
|
||||
ok: "Votre nom à l'air sympa !."
|
||||
username:
|
||||
title: "Pseudo"
|
||||
instructions: "Les gens peuvent vous mentionner avec @{{username}}."
|
||||
instructions: "Doit être unique et ne pas contenir d'espace. Les gens pourrons vous mentionner avec @pseudo."
|
||||
short_instructions: "Les gens peuvent vous mentionner avec @{{username}}."
|
||||
available: "Votre pseudo est disponible."
|
||||
global_match: "L'adresse email correspond au pseudo enregistré."
|
||||
global_mismatch: "Déjà enregistré. Essayez {{suggestion}} ?"
|
||||
@ -105,6 +106,9 @@ fr:
|
||||
checking: "Vérification de la disponibilité de votre pseudo..."
|
||||
enter_email: "Pseudo trouvé. Entrez l'adresse email correspondante."
|
||||
|
||||
password_confirmation:
|
||||
title: "Confirmation"
|
||||
|
||||
last_posted: "Dernier message"
|
||||
last_emailed: "Dernier mail"
|
||||
last_seen: "Dernier vu"
|
||||
@ -272,7 +276,6 @@ fr:
|
||||
create_topic: "Créer une discussion"
|
||||
create_pm: "Créer un message privé."
|
||||
|
||||
quote_title: "Citer un message"
|
||||
bold_title: "Gras"
|
||||
bold_text: "texte en gras"
|
||||
italic_title: "Italique"
|
||||
|
@ -285,7 +285,6 @@ zh_CN:
|
||||
show_preview: '显示预览 »'
|
||||
hide_preview: '« 隐藏预览'
|
||||
|
||||
quote_title: "引用帖子"
|
||||
bold_title: "加粗"
|
||||
bold_text: "加粗文字"
|
||||
italic_title: "斜体"
|
||||
|
@ -286,8 +286,8 @@ cs:
|
||||
new_topics_rollup: "Kolik nových témat může být vloženo do seznamu než budou tato témata shrnuta do číselné hodnoty"
|
||||
onebox_max_chars: "Maximální počet znaků, které může 'onebox' naimportovat z externího webu"
|
||||
|
||||
logo_url: "Logo vašeho webu, např. http://xyz.com/x.png"
|
||||
logo_small_url: "Malé logo, které se použije pokud odskrolujete dolů v tématu, např. http://xyz.com/x-small.png"
|
||||
logo_url: "Logo vašeho webu, např. http://example.com/logo.png"
|
||||
logo_small_url: "Malé logo, které se použije pokud odskrolujete dolů v tématu, např. http://example.com/logo-small.png"
|
||||
favicon_url: "Favicona vašeho webu, viz http://en.wikipedia.org/wiki/Favicon"
|
||||
notification_email: "Návratová emailová adresa, která se použije u systémových emailů, jako jsou notifikace o zapomenutém heslu, nových účtech, atd."
|
||||
use_ssl: "Má být web přístupný přes SSL?"
|
||||
|
@ -286,8 +286,8 @@ de:
|
||||
new_topics_rollup: "Zahl der Themen, die vor dem Aufrollen der Themenliste hinzugefügt werden."
|
||||
onebox_max_chars: "Maximale Zahl der Zeichen, die eine Onebox von einer externen Webseite in einen Beitrag lädt."
|
||||
|
||||
logo_url: "Das Logo Deiner Seite, zum Beispiel: http://xyz.com/x.png"
|
||||
logo_small_url: "Kleines Logo Deiner Seite, das beim Herunterscrollen in einem Thema gezeigt wird, zum Beispiel: http://xyz.com/x-small.png"
|
||||
logo_url: "Das Logo Deiner Seite, zum Beispiel: http://example.com/logo.png"
|
||||
logo_small_url: "Kleines Logo Deiner Seite, das beim Herunterscrollen in einem Thema gezeigt wird, zum Beispiel: http://example.com/logo-small.png"
|
||||
favicon_url: "Das Favicon Deiner Seite, siehe http://de.wikipedia.org/wiki/Favicon"
|
||||
notification_email: "Die Antwortadresse, die in Systemmails (zum Beispiel zur Passwortwiederherstellung, neuen Konten, etc.) eingetragen wird."
|
||||
use_ssl: "Soll die Seite via SSL nutzbar sein?"
|
||||
|
@ -293,8 +293,8 @@ fr:
|
||||
category_post_template: "Le modèle de message de définition d'une catégorie utilisé lorsque vous créez une nouvelle catégorie"
|
||||
onebox_max_chars: "Nombre maximal de caractères qu'une boîte peut importer en blob de texte."
|
||||
|
||||
logo_url: "Le logo de votre site, par exemple: http://xyz.com/x.png"
|
||||
logo_small_url: "La version minifiée du logo de votre site (affichée sur les pages de discussions) ex: http://xyz.com/x-min.png"
|
||||
logo_url: "Le logo de votre site, par exemple: http://example.com/logo.png"
|
||||
logo_small_url: "La version minifiée du logo de votre site (affichée sur les pages de discussions) ex: http://example.com/logo-small.png"
|
||||
favicon_url: "Le favicon de votre site"
|
||||
notification_email: "L'adresse email utilisée pour notifier les utilisateurs de mots de passe perdus, d'activation de compte, etc."
|
||||
use_ssl: "Le site doit-il être accessible via SSL?"
|
||||
|
@ -254,8 +254,8 @@ nl:
|
||||
post_onebox_maxlength: "Maximale lengte van een 'oneboxed' discourse post."
|
||||
new_topics_rollup: "Hoeveel topics er aan een topic-lijst kunnen worden toegevoegd voordat de lijst oprolt."
|
||||
onebox_max_chars: "Het maximaal aantal karakters dat een 'onebox' zal importeren in een lap tekst."
|
||||
logo_url: "Het logo van je site bijv: http://xyz.com/x.png"
|
||||
logo_small_url: "Het kleine logo van je site (wordt weergegeven op topic-pagina's) bijv: http://xyz.com/x.png"
|
||||
logo_url: "Het logo van je site bijv: http://example.com/logo.png"
|
||||
logo_small_url: "Het kleine logo van je site (wordt weergegeven op topic-pagina's) bijv: http://example.com/logo-small.png"
|
||||
favicon_url: "Een favicon voor je site"
|
||||
notification_email: "Het email-adres waarmee gebruikers op de hoogte worden gesteld van verloren wachtwoorden, nieuwe accounts etc."
|
||||
use_ssl: "Moet de site toegankelijk zijn via SSL?"
|
||||
|
@ -257,8 +257,8 @@ pt:
|
||||
category_post_template: "O template para um post que aparece quando crias uma categoria."
|
||||
new_topics_rollup: "Quantos tópicos podem ser inseridos na lista de tópicos antes de serem puxados."
|
||||
onebox_max_chars: "Máximo número de caracteres que um onebox vai importar num único pedaço."
|
||||
logo_url: "O logo para o teu site eg: http://xyz.com/x.png"
|
||||
logo_small_url: "O logo em pequeno para o teu site (aparece nas páginas dos tópicos) eg: http://xyz.com/x.png"
|
||||
logo_url: "O logo para o teu site eg: http://example.com/logo.png"
|
||||
logo_small_url: "O logo em pequeno para o teu site (aparece nas páginas dos tópicos) eg: http://example.com/logo-small.png"
|
||||
favicon_url: "Um favicon para o teu site"
|
||||
notification_email: "O endereço de email a ser usado para notificar os utilizadores de password esquecida, novas contas, etc."
|
||||
use_ssl: "Deverá o site estár acessivel via SSL?"
|
||||
|
@ -286,8 +286,8 @@ zh_CN:
|
||||
new_topics_rollup: "主题列表卷起显示为主题数量前,可以往主题列表中插入多少新主题"
|
||||
onebox_max_chars: "从外部网站导入到一个单厢帖(Onebox post)的最大字符数"
|
||||
|
||||
logo_url: "你的站点标志图片,例如:http://xyz.com/x.png"
|
||||
logo_small_url: "你的站点的小号标志图片,例如:http://xyz.com/x-small.png,用于卷起主题列表时显示"
|
||||
logo_url: "你的站点标志图片,例如:http://example.com/logo.png"
|
||||
logo_small_url: "你的站点的小号标志图片,例如:http://example.com/logo-small.png,用于卷起主题列表时显示"
|
||||
favicon_url: "你的站点图标(favicon),参考 http://zh.wikipedia.org/wiki/Favicon"
|
||||
notification_email: "邮件回复地址,当发送系统邮件,例如通知用户找回密码、新用户注册等等时,所使用的发信人地址"
|
||||
use_ssl: "使用 SSL 安全套接层来访问本站吗?"
|
||||
|
9
db/migrate/20130311181327_remove_extra_spam_record.rb
Normal file
9
db/migrate/20130311181327_remove_extra_spam_record.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class RemoveExtraSpamRecord < ActiveRecord::Migration
|
||||
def up
|
||||
execute "UPDATE post_actions SET post_action_type_id = 7 where post_action_type_id = 8"
|
||||
execute "DELETE FROM post_action_types WHERE id = 8"
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
@ -4644,4 +4644,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130221215017');
|
||||
|
||||
INSERT INTO schema_migrations (version) VALUES ('20130226015336');
|
||||
|
||||
INSERT INTO schema_migrations (version) VALUES ('20130306180148');
|
||||
INSERT INTO schema_migrations (version) VALUES ('20130306180148');
|
||||
|
||||
INSERT INTO schema_migrations (version) VALUES ('20130311181327');
|
@ -3,18 +3,7 @@ module AgeWords
|
||||
def self.age_words(secs)
|
||||
return "—" if secs.blank?
|
||||
|
||||
mins = (secs / 60.0)
|
||||
hours = (mins / 60.0)
|
||||
days = (hours / 24.0)
|
||||
months = (days / 30.0)
|
||||
years = (months / 12.0)
|
||||
|
||||
return "#{years.floor}y" if years > 1
|
||||
return "#{months.floor}mo" if months > 1
|
||||
return "#{days.floor}d" if days > 1
|
||||
return "#{hours.floor}h" if hours > 1
|
||||
return "< 1m" if mins < 1
|
||||
return "#{mins.floor}m"
|
||||
return FreedomPatches::Rails4.distance_of_time_in_words(Time.now, Time.now + secs)
|
||||
end
|
||||
|
||||
end
|
@ -35,6 +35,13 @@ class DiscourseRedis
|
||||
RailsMultisite::ConnectionManagement.current_db
|
||||
end
|
||||
|
||||
def self.new_redis_store
|
||||
redis_config = YAML::load(File.open("#{Rails.root}/config/redis.yml"))[Rails.env]
|
||||
redis_store = ActiveSupport::Cache::RedisStore.new "redis://#{redis_config['host']}:#{redis_config['port']}/#{redis_config['cache_db']}"
|
||||
redis_store.options[:namespace] = -> { DiscourseRedis.namespace }
|
||||
redis_store
|
||||
end
|
||||
|
||||
def url
|
||||
"redis://#{@config['host']}:#{@config['port']}/#{@config['db']}"
|
||||
end
|
||||
|
@ -98,7 +98,7 @@ module Search
|
||||
return nil if term.blank?
|
||||
|
||||
# We are stripping only symbols taking place in FTS and simply sanitizing the rest.
|
||||
sanitized_term = PG::Connection.escape_string(term.gsub(/[:()&!]/,''))
|
||||
sanitized_term = PG::Connection.escape_string(term.gsub(/[:()&!]/,''))
|
||||
|
||||
# really short terms are totally pointless
|
||||
return nil if sanitized_term.blank? || sanitized_term.length < min_search_term_length
|
||||
@ -155,7 +155,11 @@ module Search
|
||||
type = row.delete('type')
|
||||
|
||||
# Add the slug for topics
|
||||
row['url'].gsub!('slug', Slug.for(row['title'])) if type == 'topic'
|
||||
if type == 'topic'
|
||||
new_slug = Slug.for(row['title'])
|
||||
new_slug = "topic" if new_slug.blank?
|
||||
row['url'].gsub!('slug', new_slug)
|
||||
end
|
||||
|
||||
# Remove attributes when we know they don't matter
|
||||
row.delete('id')
|
||||
|
@ -192,7 +192,7 @@ class TopicQuery
|
||||
if @user_id.present?
|
||||
result = result.order(TopicQuery.order_nocategory_with_pinned_sql)
|
||||
else
|
||||
result = result.order(TopicQuery.order_basic_bumped)
|
||||
result = result.order(TopicQuery.order_nocategory_basic_bumped)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
|
||||
<meta name="description" content="Discourse.org github commits widget" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="stylesheets/stylesheet.css">
|
||||
|
||||
<title>Discourse.org Latest Commits Widget</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="widget-container">
|
||||
<div class="header">
|
||||
<h1>Latest Changes</h4>
|
||||
</div>
|
||||
<ul class="commits-list"></ul>
|
||||
</div>
|
||||
|
||||
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
|
||||
<script src="javascripts/jquery.timeago.js"></script>
|
||||
<script src="javascripts/commits-widget.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Parameters:
|
||||
* limit: (integer) How many commits to render, starting with the most recent commit
|
||||
* width: (integer) Width of the widget
|
||||
* height: (integer) Height of the widget
|
||||
* heading: (string) Text in the header of the widget
|
||||
*/
|
||||
$(function(){
|
||||
var $commitsList = $('.commits-list');
|
||||
var keyValuePairs = window.location.href.slice(window.location.href.indexOf("?") + 1).split("&");
|
||||
var x, params = {};
|
||||
$.each(keyValuePairs, function(i, keyValue){
|
||||
x = keyValue.split('=');
|
||||
params[x[0]] = x[1];
|
||||
});
|
||||
|
||||
if( params.width ) {
|
||||
$('.widget-container').css('width', params.width + 'px');
|
||||
}
|
||||
if( params.height ) {
|
||||
$('.widget-container').css('height', params.height + 'px');
|
||||
$('.widget-container .commits-list').css('height', (params.height - 31) + 'px');
|
||||
}
|
||||
if( params.heading ) {
|
||||
$('.widget-container h1').text( decodeURIComponent(params.heading) );
|
||||
}
|
||||
|
||||
$('.widget-container .header').click(function(){
|
||||
window.open('https://github.com/discourse/discourse');
|
||||
});
|
||||
|
||||
$.ajax( "https://api.github.com/repos/discourse/discourse/commits?callback=callback", {
|
||||
dataType: 'jsonp',
|
||||
type: 'get',
|
||||
data: {
|
||||
per_page: params.limit || 10
|
||||
},
|
||||
success: function(response, textStatus, jqXHR) {
|
||||
var data = response.data;
|
||||
$.each(data, function(i, commit){
|
||||
var $li = $('<li></li>').appendTo( $commitsList );
|
||||
if( commit.sha && commit.commit && commit.commit.message && commit.commit.author && commit.commit.committer && commit.commit.committer.date ) {
|
||||
if( commit.author && commit.author.gravatar_id ) {
|
||||
$('<div class="left"><img src="https://www.gravatar.com/avatar/' + commit.author.gravatar_id + '.png?s=38&r=pg&d=identicon"></div>').appendTo( $li );
|
||||
} else {
|
||||
$('<div class="left"><img src="https://www.gravatar.com/avatar/b30fff48d257cdd17c4437afac19fd30.png?s=38&r=pg&d=identicon"></div>').appendTo( $li );
|
||||
}
|
||||
$right = $('<div class="right"></div>').appendTo( $li );
|
||||
$('<span class="commit-message"><a href="https://github.com/discourse/discourse/commit/' + commit.sha + '" target="_blank">' + commit.commit.message + '</a></span><br/>').appendTo( $right );
|
||||
$('<span class="commit-meta">by <span class="committer-name">' + commit.commit.author.name + '</span> - <span class="commit-time">' + $.timeago(commit.commit.committer.date) + '</span></span>').appendTo( $right );
|
||||
$('<div class="clearfix"></div>').appendTo( $li );
|
||||
} else {
|
||||
// Render nothing. Or render a message:
|
||||
// $('<div class="left"> </div>').appendTo( $li );
|
||||
// $right = $('<div class="right"></div>').appendTo( $li );
|
||||
// $('<span class="commit-meta">this commit cannot be rendered</span>').appendTo( $right );
|
||||
// $('<div class="clearfix"></div>').appendTo( $li );
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Timeago is a jQuery plugin that makes it easy to support automatically
|
||||
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
||||
*
|
||||
* @name timeago
|
||||
* @version 1.0.2
|
||||
* @requires jQuery v1.2.3+
|
||||
* @author Ryan McGeary
|
||||
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* For usage and examples, visit:
|
||||
* http://timeago.yarp.com/
|
||||
*
|
||||
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
||||
*/
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
$.timeago = function(timestamp) {
|
||||
if (timestamp instanceof Date) {
|
||||
return inWords(timestamp);
|
||||
} else if (typeof timestamp === "string") {
|
||||
return inWords($.timeago.parse(timestamp));
|
||||
} else if (typeof timestamp === "number") {
|
||||
return inWords(new Date(timestamp));
|
||||
} else {
|
||||
return inWords($.timeago.datetime(timestamp));
|
||||
}
|
||||
};
|
||||
var $t = $.timeago;
|
||||
|
||||
$.extend($.timeago, {
|
||||
settings: {
|
||||
refreshMillis: 60000,
|
||||
allowFuture: false,
|
||||
strings: {
|
||||
prefixAgo: null,
|
||||
prefixFromNow: null,
|
||||
suffixAgo: "ago",
|
||||
suffixFromNow: "from now",
|
||||
seconds: "less than a minute",
|
||||
minute: "about a minute",
|
||||
minutes: "%d minutes",
|
||||
hour: "about an hour",
|
||||
hours: "about %d hours",
|
||||
day: "a day",
|
||||
days: "%d days",
|
||||
month: "about a month",
|
||||
months: "%d months",
|
||||
year: "about a year",
|
||||
years: "%d years",
|
||||
wordSeparator: " ",
|
||||
numbers: []
|
||||
}
|
||||
},
|
||||
inWords: function(distanceMillis) {
|
||||
var $l = this.settings.strings;
|
||||
var prefix = $l.prefixAgo;
|
||||
var suffix = $l.suffixAgo;
|
||||
if (this.settings.allowFuture) {
|
||||
if (distanceMillis < 0) {
|
||||
prefix = $l.prefixFromNow;
|
||||
suffix = $l.suffixFromNow;
|
||||
}
|
||||
}
|
||||
|
||||
var seconds = Math.abs(distanceMillis) / 1000;
|
||||
var minutes = seconds / 60;
|
||||
var hours = minutes / 60;
|
||||
var days = hours / 24;
|
||||
var years = days / 365;
|
||||
|
||||
function substitute(stringOrFunction, number) {
|
||||
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
||||
var value = ($l.numbers && $l.numbers[number]) || number;
|
||||
return string.replace(/%d/i, value);
|
||||
}
|
||||
|
||||
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
||||
seconds < 90 && substitute($l.minute, 1) ||
|
||||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
||||
minutes < 90 && substitute($l.hour, 1) ||
|
||||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
||||
hours < 42 && substitute($l.day, 1) ||
|
||||
days < 30 && substitute($l.days, Math.round(days)) ||
|
||||
days < 45 && substitute($l.month, 1) ||
|
||||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
||||
years < 1.5 && substitute($l.year, 1) ||
|
||||
substitute($l.years, Math.round(years));
|
||||
|
||||
var separator = $l.wordSeparator || "";
|
||||
if ($l.wordSeparator === undefined) { separator = " "; }
|
||||
return $.trim([prefix, words, suffix].join(separator));
|
||||
},
|
||||
parse: function(iso8601) {
|
||||
var s = $.trim(iso8601);
|
||||
s = s.replace(/\.\d+/,""); // remove milliseconds
|
||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||
return new Date(s);
|
||||
},
|
||||
datetime: function(elem) {
|
||||
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
||||
return $t.parse(iso8601);
|
||||
},
|
||||
isTime: function(elem) {
|
||||
// jQuery's `is()` doesn't play well with HTML5 in IE
|
||||
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
||||
}
|
||||
});
|
||||
|
||||
$.fn.timeago = function() {
|
||||
var self = this;
|
||||
self.each(refresh);
|
||||
|
||||
var $s = $t.settings;
|
||||
if ($s.refreshMillis > 0) {
|
||||
setInterval(function() { self.each(refresh); }, $s.refreshMillis);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
var data = prepareData(this);
|
||||
if (!isNaN(data.datetime)) {
|
||||
$(this).text(inWords(data.datetime));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function prepareData(element) {
|
||||
element = $(element);
|
||||
if (!element.data("timeago")) {
|
||||
element.data("timeago", { datetime: $t.datetime(element) });
|
||||
var text = $.trim(element.text());
|
||||
if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
||||
element.attr("title", text);
|
||||
}
|
||||
}
|
||||
return element.data("timeago");
|
||||
}
|
||||
|
||||
function inWords(date) {
|
||||
return $t.inWords(distance(date));
|
||||
}
|
||||
|
||||
function distance(date) {
|
||||
return (new Date().getTime() - date.getTime());
|
||||
}
|
||||
|
||||
// fix for IE6 suckage
|
||||
document.createElement("abbr");
|
||||
document.createElement("time");
|
||||
}));
|
@ -1,162 +0,0 @@
|
||||
/*******************************************************************************
|
||||
MeyerWeb Reset
|
||||
*******************************************************************************/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clearfix:before, .clearfix:after {
|
||||
display: table;
|
||||
content: " ";
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Theme Styles
|
||||
*******************************************************************************/
|
||||
|
||||
.widget-container {
|
||||
border: solid 1px #ccc;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
width: 500px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.widget-container .header {
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
height: 30px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
|
||||
background-color:#e1e1e1;
|
||||
background-image:-moz-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-ms-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-o-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:-webkit-gradient(linear, left top, left bottom, from(#f1f1f1), to(#e1e1e1));
|
||||
background-image:-webkit-linear-gradient(top, #f1f1f1, #e1e1e1);
|
||||
background-image:linear-gradient(center top, #f1f1f1 0%, #e1e1e1 100%);
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#f1f1f1', EndColorStr='#e1e1e1');
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget-container .header:hover h1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.widget-container .header h1 {
|
||||
font-size: 18px;
|
||||
margin: 3px 0 0 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.widget-container .header .github-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 3px 0 0 5px;
|
||||
vertical-align: top;
|
||||
background: url(../images/github-icon.png) no-repeat 0 0;
|
||||
opacity: .65;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.widget-container .commits-list {
|
||||
height: 169px;
|
||||
overflow-y:auto;
|
||||
line-height: 0.85em;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li {
|
||||
padding: 6px 8px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
background-color:#eee;
|
||||
background-image:-moz-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-ms-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-o-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:-webkit-gradient(linear, left top, left bottom, from(#fafafa), to(#eee));
|
||||
background-image:-webkit-linear-gradient(top, #fafafa, #eee);
|
||||
background-image:linear-gradient(center top, #fafafa 0%, #eee 100%);
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#f1f1f1', EndColorStr='#e1e1e1');
|
||||
}
|
||||
.widget-container .commits-list li:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li .left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li .right {
|
||||
margin-left: 52px;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li img {
|
||||
margin-top: 2px;
|
||||
border: solid 1px #ccc;
|
||||
padding: 2px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li .commit-message {
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li .commit-meta {
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.widget-container .commits-list li .committer-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.widget-container a {
|
||||
color: #222;
|
||||
text-decoration: none
|
||||
}
|
||||
.widget-container a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
@ -16,7 +16,7 @@ describe Search do
|
||||
context 'post indexing observer' do
|
||||
before do
|
||||
@category = Fabricate(:category, name: 'america')
|
||||
@topic = Fabricate(:topic, title: 'sam test topic', category: @category)
|
||||
@topic = Fabricate(:topic, title: 'sam saffron test topic', category: @category)
|
||||
@post = Fabricate(:post, topic: @topic, raw: 'this <b>fun test</b> <img src="bla" title="my image">')
|
||||
@indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"]
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
Fabricator(:topic) do
|
||||
user
|
||||
title { sequence(:title) { |i| "Test topic #{i}" } }
|
||||
title { sequence(:title) { |i| "This is a test topic #{i}" } }
|
||||
end
|
||||
|
||||
Fabricator(:deleted_topic, from: :topic) do
|
||||
@ -12,7 +12,7 @@ end
|
||||
|
||||
Fabricator(:private_message_topic, from: :topic) do
|
||||
user
|
||||
title { sequence(:title) { |i| "Private Message #{i}" } }
|
||||
title { sequence(:title) { |i| "This is a private message #{i}" } }
|
||||
archetype "private_message"
|
||||
topic_allowed_users{|t| [
|
||||
Fabricate.build(:topic_allowed_user, user_id: t[:user].id),
|
||||
|
@ -102,10 +102,7 @@ describe Report do
|
||||
|
||||
it 'should cache the data set' do
|
||||
$redis.expects(:setex).with do |key, expiry, string|
|
||||
key == 'signups:data' and
|
||||
expiry == Report.cache_expiry # and
|
||||
string.include? "#{1.days.ago.to_date.to_s},1" and
|
||||
string.include? "#{0.days.ago.to_date.to_s},2"
|
||||
string =~ /(\d)+-(\d)+-(\d)+,1/ and string =~ /(\d)+-(\d)+-(\d)+,2/
|
||||
end
|
||||
report()
|
||||
end
|
||||
|
@ -193,12 +193,12 @@ describe Topic do
|
||||
it "enqueues a job to notify users" do
|
||||
topic.stubs(:add_moderator_post)
|
||||
Jobs.expects(:enqueue).with(:notify_moved_posts, post_ids: [p1.id, p4.id], moved_by_id: user.id)
|
||||
topic.move_posts(user, "new topic name", [p1.id, p4.id])
|
||||
topic.move_posts(user, "new testing topic name", [p1.id, p4.id])
|
||||
end
|
||||
|
||||
it "adds a moderator post at the location of the first moved post" do
|
||||
topic.expects(:add_moderator_post).with(user, instance_of(String), has_entries(post_number: 2))
|
||||
topic.move_posts(user, "new topic name", [p2.id, p4.id])
|
||||
topic.move_posts(user, "new testing topic name", [p2.id, p4.id])
|
||||
end
|
||||
|
||||
end
|
||||
@ -206,11 +206,11 @@ describe Topic do
|
||||
context "errors" do
|
||||
|
||||
it "raises an error when one of the posts doesn't exist" do
|
||||
lambda { topic.move_posts(user, "new topic name", [1003]) }.should raise_error(Discourse::InvalidParameters)
|
||||
lambda { topic.move_posts(user, "new testing topic name", [1003]) }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "raises an error if no posts were moved" do
|
||||
lambda { topic.move_posts(user, "new topic name", []) }.should raise_error(Discourse::InvalidParameters)
|
||||
lambda { topic.move_posts(user, "new testing topic name", []) }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
end
|
||||
@ -221,7 +221,7 @@ describe Topic do
|
||||
TopicUser.update_last_read(user, topic.id, p4.post_number, 0)
|
||||
end
|
||||
|
||||
let!(:new_topic) { topic.move_posts(user, "new topic name", [p2.id, p4.id]) }
|
||||
let!(:new_topic) { topic.move_posts(user, "new testing topic name", [p2.id, p4.id]) }
|
||||
|
||||
|
||||
it "updates the user's last_read_post_number" do
|
||||
|
@ -9,8 +9,10 @@ class MessageBus::Rack::Middleware
|
||||
def self.start_listener
|
||||
unless @started_listener
|
||||
MessageBus.subscribe do |msg|
|
||||
EM.next_tick do
|
||||
@@connection_manager.notify_clients(msg) if @@connection_manager
|
||||
if EM.reactor_running?
|
||||
EM.next_tick do
|
||||
@@connection_manager.notify_clients(msg) if @@connection_manager
|
||||
end
|
||||
end
|
||||
end
|
||||
@started_listener = true
|
||||
@ -73,7 +75,7 @@ class MessageBus::Rack::Middleware
|
||||
|
||||
if backlog.length > 0
|
||||
[200, headers, [self.class.backlog_to_json(backlog)] ]
|
||||
elsif MessageBus.long_polling_enabled? && env['QUERY_STRING'] !~ /dlp=t/
|
||||
elsif MessageBus.long_polling_enabled? && env['QUERY_STRING'] !~ /dlp=t/ && EM.reactor_running?
|
||||
response = Thin::AsyncResponse.new(env)
|
||||
response.headers["Cache-Control"] = "must-revalidate, private, max-age=0"
|
||||
response.headers["Content-Type"] ="application/json; charset=utf-8"
|
||||
|
Loading…
Reference in New Issue
Block a user