Merge remote-tracking branch 'upstream/master' into mywork

This commit is contained in:
tangramor 2013-03-12 10:15:06 +08:00
commit 3a6a4c628d
53 changed files with 438 additions and 706 deletions

View File

@ -12,7 +12,7 @@ gem 'redcarpet', require: false
gem 'activerecord-postgres-hstore' gem 'activerecord-postgres-hstore'
gem 'acts_as_paranoid' gem 'acts_as_paranoid'
gem 'active_attr' # until we get ActiveModel::Model with Rails 4 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 'clockwork', require: false
gem 'em-redis' gem 'em-redis'
gem 'eventmachine' gem 'eventmachine'

View File

@ -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');
}
});

View 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;
}
});

View File

@ -8,12 +8,9 @@
**/ **/
Discourse.AdminDashboardRoute = Discourse.Route.extend({ Discourse.AdminDashboardRoute = Discourse.Route.extend({
setupController: function(c) { setupController: function(c) {
if( !c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt') ) {
this.checkVersion(c); this.checkVersion(c);
}
if( !c.get('reportsCheckedAt') || Date.create('1 hour ago') > c.get('reportsCheckedAt') ) {
this.fetchReports(c); this.fetchReports(c);
} this.fetchGithubCommits(c);
}, },
renderTemplate: function() { renderTemplate: function() {
@ -21,7 +18,7 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
}, },
checkVersion: function(c) { 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()); c.set('versionCheckedAt', new Date());
Discourse.VersionCheck.find().then(function(vc) { Discourse.VersionCheck.find().then(function(vc) {
c.set('versionCheck', vc); c.set('versionCheck', vc);
@ -31,11 +28,20 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
}, },
fetchReports: function(c) { fetchReports: function(c) {
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. // TODO: use one request to get all reports, or maybe one request for all dashboard data including version check.
c.set('reportsCheckedAt', new Date()); c.set('reportsCheckedAt', new Date());
['visits', 'signups', 'topics', 'posts'].each(function(reportType){ ['visits', 'signups', 'topics', 'posts'].each(function(reportType){
c.set(reportType, Discourse.Report.find(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());
}
}
}); });

View 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>

View File

@ -37,7 +37,7 @@
</div> </div>
<div class="version-check-right"> <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>
<div class='clearfix'></div> <div class='clearfix'></div>

View File

@ -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'
});

View File

@ -24,6 +24,8 @@
//= require ./discourse/helpers/i18n_helpers //= require ./discourse/helpers/i18n_helpers
//= require ./discourse //= require ./discourse
//= require ./locales/date_locales.js
// Stuff we need to load first // Stuff we need to load first
//= require_tree ./discourse/mixins //= require_tree ./discourse/mixins
//= require ./discourse/views/view //= require ./discourse/views/view

View File

@ -29,50 +29,44 @@ Discourse.Development = {
}, },
after: function(data, owner, args) { 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) { if (owner && data.time > 10) {
f = function(name, data) { f = function(name, data) {
if (data && data.count) return name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms"; 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)); console.group(f(name, data));
} else { } else {
console.log(""); console.log("");
console.log(f(name, data)); console.log(f(name, data));
} }
ary = []; var ary = [];
_ref = window.probes; for (n in window.probes) {
for (n in _ref) { v = window.probes[n];
v = _ref[n]; if (n === name || v.time < 1) continue;
if (n === name || v.time < 1) { ary.push({ k: n, v: v });
continue;
}
ary.push({
k: n,
v: v
});
} }
ary.sortBy(function(item) { ary.sortBy(function(item) {
if (item.v && item.v.time) { if (item.v && item.v.time) return -item.v.time;
return -item.v.time;
} else {
return 0; return 0;
}
}).each(function(item) { }).each(function(item) {
var output = f("" + item.k, item.v); var output = f("" + item.k, item.v);
if (output) { if (output) {
return console.log(output); console.log(output);
} }
}); });
if (typeof console !== "undefined" && console !== null) {
if (typeof console.groupEnd === "function") { if (console.group) {
console.groupEnd(); console.groupEnd();
} }
} window.probes.clear();
return window.probes.clear();
} }
} }
}); });

View File

@ -92,8 +92,7 @@
start = now(); start = now();
callStart = start; callStart = start;
} }
else if(after) else if(after) {
{
callStart = now(); callStart = now();
} }

View File

@ -1,5 +1,3 @@
/*global humaneDate:true */
/** /**
Breaks up a long string Breaks up a long string
@ -162,7 +160,7 @@ Handlebars.registerHelper('avatar', function(user, options) {
Handlebars.registerHelper('unboundDate', function(property, options) { Handlebars.registerHelper('unboundDate', function(property, options) {
var dt; var dt;
dt = new Date(Ember.Handlebars.get(this, property, options)); 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)); dt = Date.create(Ember.Handlebars.get(this, property, options));
yesterday = new Date() - (60 * 60 * 24 * 1000); yesterday = new Date() - (60 * 60 * 24 * 1000);
if (yesterday > dt.getTime()) { if (yesterday > dt.getTime()) {
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}"); return dt.format("long");
} else { } else {
return humaneDate(dt); return dt.relative();
} }
}); });
@ -215,7 +213,7 @@ Handlebars.registerHelper('number', function(property, options) {
@for Handlebars @for Handlebars
**/ **/
Handlebars.registerHelper('date', function(property, options) { 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) {
if (property.hash.leaveAgo) { if (property.hash.leaveAgo) {
leaveAgo = property.hash.leaveAgo === "true"; leaveAgo = property.hash.leaveAgo === "true";
@ -229,23 +227,26 @@ Handlebars.registerHelper('date', function(property, options) {
return new Handlebars.SafeString("&mdash;"); return new Handlebars.SafeString("&mdash;");
} }
dt = new Date(val); dt = new Date(val);
fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}"); fullReadable = dt.format("long");
displayDate = ""; displayDate = "";
fiveDaysAgo = (new Date()) - 432000000; 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()) { if ((new Date()).getFullYear() !== dt.getFullYear()) {
displayDate = dt.format("{d} {Mon} '{yy}"); displayDate = dt.format("short");
} else { } else {
displayDate = dt.format("{d} {Mon}"); displayDate = dt.format("short_no_year");
} }
} else { } else {
humanized = humaneDate(dt); humanized = dt.relative();
if (!humanized) { if (!humanized) {
return ""; return "";
} }
displayDate = humanized; displayDate = humanized;
if (!leaveAgo) { if (!leaveAgo) {
displayDate = displayDate.replace(' ago', ''); displayDate = (dt.millisecondsAgo()).duration();
} }
} }
return new Handlebars.SafeString("<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>"); return new Handlebars.SafeString("<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>");

View File

@ -36,7 +36,7 @@
</tr> </tr>
<tr> <tr>
<td></td> <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> </tr>
{{#if view.passwordRequired}} {{#if view.passwordRequired}}
@ -50,7 +50,7 @@
{{/if}} {{/if}}
<tr class="password-confirmation"> <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> <td>
{{view Ember.TextField valueBinding="view.accountPasswordConfirm" type="password" id="new-account-password-confirmation"}} {{view Ember.TextField valueBinding="view.accountPasswordConfirm" type="password" id="new-account-password-confirmation"}}
{{view Ember.TextField valueBinding="view.accountChallenge" id="new-account-challenge"}} {{view Ember.TextField valueBinding="view.accountChallenge" id="new-account-challenge"}}

View File

@ -13,7 +13,7 @@
{{#linkTo "preferences.username" class="btn pad-left"}}{{i18n user.change_username.action}}{{/linkTo}} {{#linkTo "preferences.username" class="btn pad-left"}}{{i18n user.change_username.action}}{{/linkTo}}
</div> </div>
<div class='instructions'> <div class='instructions'>
{{{i18n user.username.instructions username="content.username"}}} {{{i18n user.username.short_instructions username="content.username"}}}
</div> </div>
</div> </div>

View File

@ -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 // highlight new topics that have been loaded from the server or the one we just created
else if (this.get('content.highlight')) { else if (this.get('content.highlight')) {
this.set('content.highlight', false);
this.highlight(); this.highlight();
} }
} }

View File

@ -276,8 +276,19 @@ Discourse.CreateAccountView = Discourse.ModalBodyView.extend({
_this.set('formSubmitted', false); _this.set('formSubmitted', false);
return _this.flash(Em.String.i18n('create_account.failed'), 'error'); 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();
}
});
});
} }
}); });

View File

@ -32,9 +32,15 @@ Discourse.PagedownEditor = Ember.ContainerView.extend({
didInsertElement: function() { didInsertElement: function() {
var $wmdInput = $('#wmd-input'); var $wmdInput = $('#wmd-input');
$wmdInput.data('init', true); $wmdInput.data('init', true);
this.editor = Discourse.Markdown.createEditor(); this.set('editor', Discourse.Markdown.createEditor());
return this.editor.run(); return this.get('editor').run();
} },
observeValue: (function() {
var editor = this.get('editor');
if (!editor) return;
Ember.run.next(null, function() { editor.refreshPreview(); });
}).observes('value')
}); });

View File

@ -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);
}
});
};
}

View 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);

View File

@ -1,6 +1,7 @@
// these are the styles associated with the Discourse admin section // these are the styles associated with the Discourse admin section
@import "foundation/variables"; @import "foundation/variables";
@import "foundation/mixins"; @import "foundation/mixins";
@import "foundation/helpers";
.admin-content { .admin-content {
margin-bottom: 50px; margin-bottom: 50px;
@ -319,3 +320,106 @@ table {
} }
} }
} }
.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;
}
}
}

View File

@ -103,11 +103,11 @@ class TopicViewSerializer < ApplicationSerializer
end end
def can_reply_as_new_topic def can_reply_as_new_topic
scope.can_reply_as_new_topic?(object.topic) true
end end
def include_can_reply_as_new_topic? def include_can_reply_as_new_topic?
scope.can_create?(Post, object.topic) scope.can_reply_as_new_topic?(object.topic)
end end
def can_create_post def can_create_post

View File

@ -88,11 +88,9 @@ module Discourse
# Our templates shouldn't start with 'discourse/templates' # Our templates shouldn't start with 'discourse/templates'
config.handlebars.templates_root = 'discourse/templates' config.handlebars.templates_root = 'discourse/templates'
require 'discourse_redis'
# Use redis for our cache # Use redis for our cache
redis_config = YAML::load(File.open("#{Rails.root}/config/redis.yml"))[Rails.env] config.cache_store = DiscourseRedis.new_redis_store
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
# Test with rack::cache disabled. Nginx does this for us # Test with rack::cache disabled. Nginx does this for us
config.action_dispatch.rack_cache = nil config.action_dispatch.rack_cache = nil

View File

@ -33,12 +33,6 @@ Discourse::Application.configure do
config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external/ember.js" config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external/ember.js"
config.handlebars.precompile = false 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.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 } config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
config.action_mailer.raise_delivery_errors = true config.action_mailer.raise_delivery_errors = true

View 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

View 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

View File

@ -1,12 +1,3 @@
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| Sidekiq.configure_server do |config|
config.redis = { :url => $redis.url, :namespace => 'sidekiq' } config.redis = { :url => $redis.url, :namespace => 'sidekiq' }
end end

View File

@ -24,6 +24,7 @@ cs:
you: "Vy" you: "Vy"
ok: "ok" ok: "ok"
or: "nebo" or: "nebo"
now: "právě teď"
suggested_topics: suggested_topics:
title: "Doporučená témata" title: "Doporučená témata"
@ -279,7 +280,6 @@ cs:
show_preview: 'zobrazit náhled &raquo;' show_preview: 'zobrazit náhled &raquo;'
hide_preview: '&laquo; skrýt náhled' hide_preview: '&laquo; skrýt náhled'
quote_title: "Citovat příspěvek"
bold_title: "Tučně" bold_title: "Tučně"
bold_text: "tučný text" bold_text: "tučný text"
italic_title: "Kurzíva" italic_title: "Kurzíva"

View File

@ -24,6 +24,7 @@ en:
you: "You" you: "You"
ok: "ok" ok: "ok"
or: "or" or: "or"
now: "just now"
suggested_topics: suggested_topics:
title: "Suggested Topics" title: "Suggested Topics"
@ -91,7 +92,8 @@ en:
ok: "Your name looks good." ok: "Your name looks good."
username: username:
title: "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." available: "Your username is available."
global_match: "Email matches the registered username." global_match: "Email matches the registered username."
global_mismatch: "Already registered. Try {{suggestion}}?" global_mismatch: "Already registered. Try {{suggestion}}?"
@ -101,6 +103,9 @@ en:
checking: "Checking username availability..." checking: "Checking username availability..."
enter_email: 'Username found. Enter matching email.' enter_email: 'Username found. Enter matching email.'
password_confirmation:
title: "Password Again"
last_posted: "Last Post" last_posted: "Last Post"
last_emailed: "Last Emailed" last_emailed: "Last Emailed"
last_seen: "Last Seen" last_seen: "Last Seen"
@ -279,7 +284,6 @@ en:
show_preview: 'show preview &raquo;' show_preview: 'show preview &raquo;'
hide_preview: '&laquo; hide preview' hide_preview: '&laquo; hide preview'
quote_title: "Quote Post"
bold_title: "Strong" bold_title: "Strong"
bold_text: "strong text" bold_text: "strong text"
italic_title: "Emphasis" italic_title: "Emphasis"

View File

@ -95,7 +95,8 @@ fr:
ok: "Votre nom à l'air sympa !." ok: "Votre nom à l'air sympa !."
username: username:
title: "Pseudo" 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." available: "Votre pseudo est disponible."
global_match: "L'adresse email correspond au pseudo enregistré." global_match: "L'adresse email correspond au pseudo enregistré."
global_mismatch: "Déjà enregistré. Essayez {{suggestion}} ?" global_mismatch: "Déjà enregistré. Essayez {{suggestion}} ?"
@ -105,6 +106,9 @@ fr:
checking: "Vérification de la disponibilité de votre pseudo..." checking: "Vérification de la disponibilité de votre pseudo..."
enter_email: "Pseudo trouvé. Entrez l'adresse email correspondante." enter_email: "Pseudo trouvé. Entrez l'adresse email correspondante."
password_confirmation:
title: "Confirmation"
last_posted: "Dernier message" last_posted: "Dernier message"
last_emailed: "Dernier mail" last_emailed: "Dernier mail"
last_seen: "Dernier vu" last_seen: "Dernier vu"
@ -272,7 +276,6 @@ fr:
create_topic: "Créer une discussion" create_topic: "Créer une discussion"
create_pm: "Créer un message privé." create_pm: "Créer un message privé."
quote_title: "Citer un message"
bold_title: "Gras" bold_title: "Gras"
bold_text: "texte en gras" bold_text: "texte en gras"
italic_title: "Italique" italic_title: "Italique"

View File

@ -285,7 +285,6 @@ zh_CN:
show_preview: '显示预览 &raquo;' show_preview: '显示预览 &raquo;'
hide_preview: '&laquo; 隐藏预览' hide_preview: '&laquo; 隐藏预览'
quote_title: "引用帖子"
bold_title: "加粗" bold_title: "加粗"
bold_text: "加粗文字" bold_text: "加粗文字"
italic_title: "斜体" italic_title: "斜体"

View File

@ -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" 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" 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_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://xyz.com/x-small.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" 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." 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?" use_ssl: "Má být web přístupný přes SSL?"

View File

@ -286,8 +286,8 @@ de:
new_topics_rollup: "Zahl der Themen, die vor dem Aufrollen der Themenliste hinzugefügt werden." 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." 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_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://xyz.com/x-small.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" 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." 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?" use_ssl: "Soll die Seite via SSL nutzbar sein?"

View File

@ -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" 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." 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_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://xyz.com/x-min.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" 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." 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?" use_ssl: "Le site doit-il être accessible via SSL?"

View File

@ -254,8 +254,8 @@ nl:
post_onebox_maxlength: "Maximale lengte van een 'oneboxed' discourse post." 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." 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." 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_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://xyz.com/x.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" 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." 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?" use_ssl: "Moet de site toegankelijk zijn via SSL?"

View File

@ -257,8 +257,8 @@ pt:
category_post_template: "O template para um post que aparece quando crias uma categoria." 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." 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." 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_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://xyz.com/x.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" 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." 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?" use_ssl: "Deverá o site estár acessivel via SSL?"

View File

@ -286,8 +286,8 @@ zh_CN:
new_topics_rollup: "主题列表卷起显示为主题数量前,可以往主题列表中插入多少新主题" new_topics_rollup: "主题列表卷起显示为主题数量前,可以往主题列表中插入多少新主题"
onebox_max_chars: "从外部网站导入到一个单厢帖Onebox post的最大字符数" onebox_max_chars: "从外部网站导入到一个单厢帖Onebox post的最大字符数"
logo_url: "你的站点标志图片例如http://xyz.com/x.png" logo_url: "你的站点标志图片例如http://example.com/logo.png"
logo_small_url: "你的站点的小号标志图片例如http://xyz.com/x-small.png用于卷起主题列表时显示" logo_small_url: "你的站点的小号标志图片例如http://example.com/logo-small.png用于卷起主题列表时显示"
favicon_url: "你的站点图标favicon参考 http://zh.wikipedia.org/wiki/Favicon" favicon_url: "你的站点图标favicon参考 http://zh.wikipedia.org/wiki/Favicon"
notification_email: "邮件回复地址,当发送系统邮件,例如通知用户找回密码、新用户注册等等时,所使用的发信人地址" notification_email: "邮件回复地址,当发送系统邮件,例如通知用户找回密码、新用户注册等等时,所使用的发信人地址"
use_ssl: "使用 SSL 安全套接层来访问本站吗?" use_ssl: "使用 SSL 安全套接层来访问本站吗?"

View 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

View File

@ -4645,3 +4645,5 @@ INSERT INTO schema_migrations (version) VALUES ('20130221215017');
INSERT INTO schema_migrations (version) VALUES ('20130226015336'); 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');

View File

@ -3,18 +3,7 @@ module AgeWords
def self.age_words(secs) def self.age_words(secs)
return "&mdash;" if secs.blank? return "&mdash;" if secs.blank?
mins = (secs / 60.0) return FreedomPatches::Rails4.distance_of_time_in_words(Time.now, Time.now + secs)
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 "&lt; 1m" if mins < 1
return "#{mins.floor}m"
end end
end end

View File

@ -35,6 +35,13 @@ class DiscourseRedis
RailsMultisite::ConnectionManagement.current_db RailsMultisite::ConnectionManagement.current_db
end 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 def url
"redis://#{@config['host']}:#{@config['port']}/#{@config['db']}" "redis://#{@config['host']}:#{@config['port']}/#{@config['db']}"
end end

View File

@ -155,7 +155,11 @@ module Search
type = row.delete('type') type = row.delete('type')
# Add the slug for topics # 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 # Remove attributes when we know they don't matter
row.delete('id') row.delete('id')

View File

@ -192,7 +192,7 @@ class TopicQuery
if @user_id.present? if @user_id.present?
result = result.order(TopicQuery.order_nocategory_with_pinned_sql) result = result.order(TopicQuery.order_nocategory_with_pinned_sql)
else else
result = result.order(TopicQuery.order_basic_bumped) result = result.order(TopicQuery.order_nocategory_basic_bumped)
end end
end end

View File

@ -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>

View File

@ -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">&nbsp;</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 );
}
});
}
});
});

View File

@ -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");
}));

View File

@ -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;
}

View File

@ -16,7 +16,7 @@ describe Search do
context 'post indexing observer' do context 'post indexing observer' do
before do before do
@category = Fabricate(:category, name: 'america') @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">') @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"] @indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"]
end end

View File

@ -1,6 +1,6 @@
Fabricator(:topic) do Fabricator(:topic) do
user user
title { sequence(:title) { |i| "Test topic #{i}" } } title { sequence(:title) { |i| "This is a test topic #{i}" } }
end end
Fabricator(:deleted_topic, from: :topic) do Fabricator(:deleted_topic, from: :topic) do
@ -12,7 +12,7 @@ end
Fabricator(:private_message_topic, from: :topic) do Fabricator(:private_message_topic, from: :topic) do
user user
title { sequence(:title) { |i| "Private Message #{i}" } } title { sequence(:title) { |i| "This is a private message #{i}" } }
archetype "private_message" archetype "private_message"
topic_allowed_users{|t| [ topic_allowed_users{|t| [
Fabricate.build(:topic_allowed_user, user_id: t[:user].id), Fabricate.build(:topic_allowed_user, user_id: t[:user].id),

View File

@ -102,10 +102,7 @@ describe Report do
it 'should cache the data set' do it 'should cache the data set' do
$redis.expects(:setex).with do |key, expiry, string| $redis.expects(:setex).with do |key, expiry, string|
key == 'signups:data' and string =~ /(\d)+-(\d)+-(\d)+,1/ and string =~ /(\d)+-(\d)+-(\d)+,2/
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"
end end
report() report()
end end

View File

@ -193,12 +193,12 @@ describe Topic do
it "enqueues a job to notify users" do it "enqueues a job to notify users" do
topic.stubs(:add_moderator_post) topic.stubs(:add_moderator_post)
Jobs.expects(:enqueue).with(:notify_moved_posts, post_ids: [p1.id, p4.id], moved_by_id: user.id) 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 end
it "adds a moderator post at the location of the first moved post" do 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.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
end end
@ -206,11 +206,11 @@ describe Topic do
context "errors" do context "errors" do
it "raises an error when one of the posts doesn't exist" 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 end
it "raises an error if no posts were moved" do 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
end end
@ -221,7 +221,7 @@ describe Topic do
TopicUser.update_last_read(user, topic.id, p4.post_number, 0) TopicUser.update_last_read(user, topic.id, p4.post_number, 0)
end 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 it "updates the user's last_read_post_number" do

View File

@ -9,10 +9,12 @@ class MessageBus::Rack::Middleware
def self.start_listener def self.start_listener
unless @started_listener unless @started_listener
MessageBus.subscribe do |msg| MessageBus.subscribe do |msg|
if EM.reactor_running?
EM.next_tick do EM.next_tick do
@@connection_manager.notify_clients(msg) if @@connection_manager @@connection_manager.notify_clients(msg) if @@connection_manager
end end
end end
end
@started_listener = true @started_listener = true
end end
end end
@ -73,7 +75,7 @@ class MessageBus::Rack::Middleware
if backlog.length > 0 if backlog.length > 0
[200, headers, [self.class.backlog_to_json(backlog)] ] [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 = Thin::AsyncResponse.new(env)
response.headers["Cache-Control"] = "must-revalidate, private, max-age=0" response.headers["Cache-Control"] = "must-revalidate, private, max-age=0"
response.headers["Content-Type"] ="application/json; charset=utf-8" response.headers["Content-Type"] ="application/json; charset=utf-8"