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

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({
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());
}
}
});

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

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
//= require ./locales/date_locales.js
// Stuff we need to load first
//= require_tree ./discourse/mixins
//= require ./discourse/views/view

View File

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

View File

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

View File

@ -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("&mdash;");
}
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');
});
});

View File

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

View File

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

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
else if (this.get('content.highlight')) {
this.set('content.highlight', false);
this.highlight();
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -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 &raquo;'
hide_preview: '&laquo; skrýt náhled'
quote_title: "Citovat příspěvek"
bold_title: "Tučně"
bold_text: "tučný text"
italic_title: "Kurzíva"

View File

@ -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 &raquo;'
hide_preview: '&laquo; hide preview'
quote_title: "Quote Post"
bold_title: "Strong"
bold_text: "strong text"
italic_title: "Emphasis"

View File

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

View File

@ -285,7 +285,6 @@ zh_CN:
show_preview: '显示预览 &raquo;'
hide_preview: '&laquo; 隐藏预览'
quote_title: "引用帖子"
bold_title: "加粗"
bold_text: "加粗文字"
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"
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?"

View File

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

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"
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?"

View File

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

View File

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

View File

@ -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 安全套接层来访问本站吗?"

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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