diff --git a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js
index 924654a2b..e3d2dacd5 100644
--- a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js
@@ -26,7 +26,9 @@ Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.P
}
var adminSettingsController = this;
- return this.get('content').filter(function(item, index, enumerable) {
+
+ var maxResults = Em.isNone(filter) ? this.get('content.length') : 20;
+ return _.first(this.get('content').filter(function(item, index, enumerable) {
if (adminSettingsController.get('onlyOverridden') && !item.get('overridden')) return false;
if (filter) {
if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true;
@@ -36,20 +38,11 @@ Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.P
}
return true;
- });
+ }), maxResults);
}.property('filter', 'content.@each', 'onlyOverridden'),
actions: {
- /**
- Changes the currently active filter
-
- @method changeFilter
- **/
- changeFilter: function() {
- this.set('filter', this.get('newFilter'));
- },
-
/**
Reset a setting to its default value
diff --git a/app/assets/javascripts/admin/templates/site_settings.js.handlebars b/app/assets/javascripts/admin/templates/site_settings.js.handlebars
index 25885c157..ff6e21ab7 100644
--- a/app/assets/javascripts/admin/templates/site_settings.js.handlebars
+++ b/app/assets/javascripts/admin/templates/site_settings.js.handlebars
@@ -6,8 +6,7 @@
- {{textField value=newFilter}}
-
+ {{textField value=filter placeholderKey="type_to_filter"}}
diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js
index 094c4e9bf..36d5edfa2 100644
--- a/app/assets/javascripts/discourse.js
+++ b/app/assets/javascripts/discourse.js
@@ -248,4 +248,3 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
});
Discourse.Router = Discourse.Router.reopen({ location: 'discourse_location' });
-
diff --git a/app/assets/javascripts/discourse/controllers/quote_button_controller.js b/app/assets/javascripts/discourse/controllers/quote_button_controller.js
index 8c1b9110a..c6e4ceb7e 100644
--- a/app/assets/javascripts/discourse/controllers/quote_button_controller.js
+++ b/app/assets/javascripts/discourse/controllers/quote_button_controller.js
@@ -45,9 +45,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
cloned = range.cloneRange(),
$ancestor = $(range.commonAncestorContainer);
- // don't display the "quote reply" button if you select text spanning two posts
- // note: the ".contents" is here to prevent selection of the topic summary
- if ($ancestor.closest('.topic-body > .contents').length === 0) {
+ if ($ancestor.closest('.cooked').length === 0) {
this.set('buffer', '');
return;
}
diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js
index e2e306094..45a712d13 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js
+++ b/app/assets/javascripts/discourse/lib/utilities.js
@@ -88,18 +88,18 @@ Discourse.Utilities = {
var html = '';
if (typeof window.getSelection !== "undefined") {
- var sel = window.getSelection();
- if (sel.rangeCount) {
- var container = document.createElement("div");
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- container.appendChild(sel.getRangeAt(i).cloneContents());
- }
- html = container.innerHTML;
+ var sel = window.getSelection();
+ if (sel.rangeCount) {
+ var container = document.createElement("div");
+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+ container.appendChild(sel.getRangeAt(i).cloneContents());
}
+ html = container.innerHTML;
+ }
} else if (typeof document.selection !== "undefined") {
- if (document.selection.type === "Text") {
- html = document.selection.createRange().htmlText;
- }
+ if (document.selection.type === "Text") {
+ html = document.selection.createRange().htmlText;
+ }
}
// Strip out any .click elements from the HTML before converting it to text
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index 4696996dd..7cae814f9 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -32,6 +32,11 @@ Discourse.Post = Discourse.Model.extend({
notDeleted: Em.computed.not('deleted'),
userDeleted: Em.computed.empty('user_id'),
+ showName: function() {
+ var name = this.get('name');
+ return name && (name !== this.get('username')) && Discourse.SiteSettings.display_name_on_posts;
+ }.property('name', 'username'),
+
postDeletedBy: function() {
if (this.get('firstPost')) { return this.get('topic.deleted_by'); }
return this.get('deleted_by');
diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars
index 95da0cd1d..641d43d93 100644
--- a/app/assets/javascripts/discourse/templates/post.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/post.js.handlebars
@@ -21,6 +21,11 @@
{{avatar this imageSize="large"}}
+
+ {{#if showName}}
+
+ {{/if}}
+
{{#if user_title}}
{{user_title}}
{{/if}}
{{else}}
diff --git a/app/assets/javascripts/discourse/views/quote_button_view.js b/app/assets/javascripts/discourse/views/quote_button_view.js
index 9c6d02949..7741ac087 100644
--- a/app/assets/javascripts/discourse/views/quote_button_view.js
+++ b/app/assets/javascripts/discourse/views/quote_button_view.js
@@ -64,8 +64,8 @@ Discourse.QuoteButtonView = Discourse.View.extend({
})
.on('selectionchange', function() {
// there is no need to handle this event when the mouse is down
- // or if there is not a touch in progress
- if (view.get('isMouseDown') || !view.get('isTouchInProgress')) return;
+ // or if there a touch in progress
+ if (view.get('isMouseDown') || view.get('isTouchInProgress')) return;
// `selection.anchorNode` is used as a target
view.selectText(window.getSelection().anchorNode, controller);
});
diff --git a/app/assets/javascripts/env.js b/app/assets/javascripts/env.js
index 98996e8c0..a8781abcb 100644
--- a/app/assets/javascripts/env.js
+++ b/app/assets/javascripts/env.js
@@ -8,5 +8,3 @@ window.ENV = {
window.Discourse = {};
Discourse.SiteSettings = {};
-
-
diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js
index 3e187afae..d8591f327 100644
--- a/app/assets/javascripts/locales/i18n.js
+++ b/app/assets/javascripts/locales/i18n.js
@@ -60,8 +60,7 @@ I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
-I18n.fallbackRules = {
-};
+I18n.fallbackRules = {};
I18n.pluralizationRules = {
en: function (n) {
@@ -207,7 +206,7 @@ I18n.translate = function(scope, options) {
} else {
return this.interpolate(translation, options);
}
- } catch(err) {
+ } catch (error) {
return this.missingTranslation(scope);
}
};
@@ -485,15 +484,9 @@ I18n.findAndTranslateValidNode = function(keys, translation) {
I18n.pluralize = function(count, scope, options) {
var translation;
- try {
- translation = this.lookup(scope, options);
- } catch (error) {}
+ try { translation = this.lookup(scope, options); } catch (error) {}
+ if (!translation) { return this.missingTranslation(scope); }
- if (!translation) {
- return this.missingTranslation(scope);
- }
-
- var message;
options = this.prepareOptions(options);
options.count = count.toString();
@@ -501,24 +494,16 @@ I18n.pluralize = function(count, scope, options) {
var key = pluralizer(Math.abs(count));
var keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
- message = this.findAndTranslateValidNode(keys, translation);
+ var message = this.findAndTranslateValidNode(keys, translation);
if (message == null) message = this.missingTranslation(scope, keys[0]);
return this.interpolate(message, options);
};
-I18n.missingTranslation = function() {
- var message = '[missing "' + this.currentLocale()
- , count = arguments.length
- ;
-
- for (var i = 0; i < count; i++) {
- message += "." + arguments[i];
- }
-
- message += '" translation]';
-
- return message;
+I18n.missingTranslation = function(scope, key) {
+ var message = '[' + this.currentLocale() + "." + scope;
+ if (key) { message += "." + key; }
+ return message + ']';
};
I18n.currentLocale = function() {
diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss
index dbc8b5c07..830072b3a 100644
--- a/app/assets/stylesheets/common/foundation/mixins.scss
+++ b/app/assets/stylesheets/common/foundation/mixins.scss
@@ -55,23 +55,13 @@
// Border radius
@mixin border-radius-all($radius) {
- border-radius: $radius;
-}
-
-@mixin border-radius-top($radius) {
- border-top-right-radius: $radius;
- border-top-left-radius: $radius;
-}
-
-@mixin border-radius-bottom($radius) {
- border-bottom-right-radius: $radius;
- border-bottom-left-radius: $radius;
+ border-radius: $radius;
}
// Box shadow
@mixin box-shadow($shadow) {
- box-shadow: $shadow;
+ box-shadow: $shadow;
}
// Linear gradient
@@ -81,32 +71,6 @@
background-image: linear-gradient(to bottom, $start-color, $end-color);
}
-// Background size
-
-@mixin background-size($size) {
- background-size: $size;
-}
-
-// Background clip
-
-@mixin background-clip($clip) {
- background-clip: $clip;
-}
-
-// Rotate
-
-@mixin rotate($degrees) {
- -webkit-transform: rotate($degrees);
- transform: rotate($degrees);
-}
-
-// Scale
-
-@mixin scale($ratio) {
- -webkit-transform: scale($ratio);
- transform: scale($ratio);
-}
-
// Transition
@mixin transition($transition) {
@@ -138,12 +102,6 @@
}
}
-@mixin fade-soft($time: 1s) {
- -webkit-transition: opacity $time ease-in-out;
- -ms-transition: opacity $time ease-in-out;
- transition: opacity $time ease-in-out;
-}
-
@mixin visible {
opacity: 1;
visibility: visible;
@@ -151,16 +109,6 @@
transition-delay: 0s;
}
-// Decorations
-// --------------------------------------------------
-
-// Glow
-
-@mixin glow($color) {
- border: 1px solid $color;
- box-shadow: 0 0 5px $color;
-}
-
//
// --------------------------------------------------
diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss
index bbfb05443..05592c8ca 100644
--- a/app/assets/stylesheets/desktop/compose.scss
+++ b/app/assets/stylesheets/desktop/compose.scss
@@ -309,7 +309,7 @@
border: 1px dashed $gray;
overflow: auto;
visibility: visible;
-
+
&.hidden {
width: 0;
visibility: hidden;
@@ -432,8 +432,7 @@ div.ac-wrap {
}
#wmd-input, #wmd-preview {
- box-sizing: border-box;
- -moz-box-sizing: border-box;
+ @include box-sizing(border-box);
width: 100%;
height: 100%;
min-height: 100%;
@@ -453,7 +452,7 @@ div.ac-wrap {
top: 0;
height: 100%;
min-height: 100%;
- box-sizing: border-box;
+ @include box-sizing(border-box);
border: 0;
border-top: 36px solid transparent;
@include border-radius-all(0);
@@ -461,8 +460,7 @@ div.ac-wrap {
}
.textarea-wrapper, .preview-wrapper {
position: relative;
- box-sizing: border-box;
- -moz-box-sizing: border-box;
+ @include box-sizing(border-box);
height: 100%;
min-height: 100%;
margin: 0;
diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss
index 34a5b0b43..ca894819b 100644
--- a/app/assets/stylesheets/desktop/topic-post.scss
+++ b/app/assets/stylesheets/desktop/topic-post.scss
@@ -39,7 +39,7 @@ h1 .topic-statuses .topic-status i {margin-right: 5px;}
margin-left: -8px;
}
-.avoid-tab {
+.avoid-tab {
padding-top: 25px;
.topic-meta-data-inside {margin-top: -30px;}
}
@@ -131,7 +131,7 @@ nav.post-controls {
border: none;
margin-left: 3px;
transition: all linear 0.15s;
- outline: none;
+ outline: none;
&:hover {
background: #eee;
color: #888;
@@ -394,12 +394,12 @@ span.post-count {
#topic-title {
z-index: 1000;
padding: 14px 0 8px 0;
- h1 {
- line-height: 1.2em;
- overflow: hidden;
+ h1 {
+ line-height: 1.2em;
+ overflow: hidden;
}
.topic-statuses {
- margin-top: -2px;
+ margin-top: -2px;
.icon-pushpin {margin-top: -1px;}
}
.star {font-size: 20px; margin-top: 8px;}
@@ -435,7 +435,7 @@ iframe {
}
.extra-info-wrapper {
- float: left;
+ float: left;
width: 78%;
.topic-statuses {margin-right: 5px;}
}
@@ -499,7 +499,7 @@ iframe {
.contents .cooked {
padding-right: 30px;
h1, h2, h3 {margin: 10px 0;}
- ul, ol {margin: 15px 0;}
+ ul, ol {margin: 0 15px;}
li p {margin: 3px 0;}
}
diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss
index 83d1f248e..6778e7f22 100644
--- a/app/assets/stylesheets/desktop/topic.scss
+++ b/app/assets/stylesheets/desktop/topic.scss
@@ -11,13 +11,14 @@
}
.post-actions {
+ @include unselectable;
clear: both;
text-align: right;
- .post-action {
+ .post-action {
display: inline-block;
margin-left: 10px;
margin-top: 10px;
- }
+ }
}
.post-menu-area {
margin-bottom: 10px;
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 505f543ed..fef462b89 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -78,9 +78,7 @@
.btn {
width: 100%;
margin-bottom: 5px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+ @include box-sizing(border-box);
}
}
h2 {
diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss
index f57a0ac0c..b8d2fb34a 100644
--- a/app/assets/stylesheets/mobile/compose.scss
+++ b/app/assets/stylesheets/mobile/compose.scss
@@ -389,8 +389,7 @@ div.ac-wrap {
}
#wmd-input, #wmd-preview {
- box-sizing: border-box;
- -moz-box-sizing: border-box;
+ @include box-sizing(border-box);
width: 100%;
height: 100%;
min-height: 100%;
@@ -410,7 +409,7 @@ div.ac-wrap {
top: 0;
height: 100%;
min-height: 100%;
- box-sizing: border-box;
+ @include box-sizing(border-box);
border: 0;
border-top: 36px solid transparent;
@include border-radius-all(0);
@@ -418,8 +417,7 @@ div.ac-wrap {
}
.textarea-wrapper, .preview-wrapper {
position: relative;
- box-sizing: border-box;
- -moz-box-sizing: border-box;
+ @include box-sizing(border-box);
height: 100%;
min-height: 100%;
margin: 0;
diff --git a/app/assets/stylesheets/mobile/magnific-popup.scss b/app/assets/stylesheets/mobile/magnific-popup.scss
index 872ae8f2b..c2ff7391b 100644
--- a/app/assets/stylesheets/mobile/magnific-popup.scss
+++ b/app/assets/stylesheets/mobile/magnific-popup.scss
@@ -148,8 +148,8 @@ $caption-subtitle-color: #BDBDBD !default;
top: 0;
padding: 0 $popup-padding-left;
-webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
// Vertical centerer helper
@@ -454,8 +454,8 @@ button::-moz-focus-inner {
display: block;
line-height: 0;
-webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
padding: $image-padding-top 0 $image-padding-bottom;
margin: 0 auto;
}
@@ -535,8 +535,8 @@ button::-moz-focus-inner {
padding: 3px 5px;
position: fixed;
-webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
.mfp-img-mobile .mfp-bottom-bar:empty {
padding: 0;
diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss
index 4ac9aeb64..bfdd68e26 100644
--- a/app/assets/stylesheets/mobile/user.scss
+++ b/app/assets/stylesheets/mobile/user.scss
@@ -73,9 +73,7 @@
.btn {
width: 100%;
margin-bottom: 5px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+ @include box-sizing(border-box);
}
}
h2 {
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index a0d510aed..b62922818 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -62,16 +62,12 @@ class Users::OmniauthCallbacksController < ApplicationController
BUILTIN_AUTH.each do |authenticator|
if authenticator.name == name
raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{name}_logins?")
-
return authenticator
end
end
Discourse.auth_providers.each do |provider|
- if provider.name == name
-
- return provider.authenticator
- end
+ return provider.authenticator if provider.name == name
end
raise Discourse::InvalidAccess.new("provider is not found")
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 9a7adf6c2..070b1115c 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -270,6 +270,7 @@ class SiteSetting < ActiveRecord::Base
# hidden setting only used by system
setting(:uncategorized_category_id, -1, hidden: true)
+ client_setting(:display_name_on_posts, false)
client_setting(:enable_names, true)
def self.call_discourse_hub?
diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb
index 151927422..63ca91725 100644
--- a/app/views/common/_discourse_javascript.html.erb
+++ b/app/views/common/_discourse_javascript.html.erb
@@ -7,23 +7,16 @@
'defer/google_diff_match_patch': <%= asset_path('defer/google_diff_match_patch.js').inspect.html_safe %>
};
- var assetPath = function(asset){
- return map[asset];
- };
-
- return assetPath;
+ return function(asset){ return map[asset]; };
})();
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 473ac8ab1..0ef90532c 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -11,6 +11,7 @@
+
<%= javascript_include_tag "preload_store" %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
diff --git a/config/environments/production.rb.sample b/config/environments/production.rb.sample
index 3b19c592f..8257958fa 100644
--- a/config/environments/production.rb.sample
+++ b/config/environments/production.rb.sample
@@ -85,6 +85,8 @@ Discourse::Application.configure do
# a comma delimited list of emails your devs have
# developers have god like rights and may impersonate anyone in the system
# normal admins may only impersonate other moderators (not admins)
- config.developer_emails = []
+ if emails = ENV["DEVELOPER_EMAILS"]
+ config.developer_emails = emails.split(",")
+ end
end
diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml
index 2ade576b9..cce34b003 100644
--- a/config/locales/client.cs.yml
+++ b/config/locales/client.cs.yml
@@ -1062,6 +1062,7 @@ cs:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "zadejte text pro filtrování..."
admin:
title: 'Discourse Administrace'
diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml
index 55a904717..ab1e894c3 100644
--- a/config/locales/client.da.yml
+++ b/config/locales/client.da.yml
@@ -683,6 +683,7 @@ da:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
index b09d24178..8fc524b39 100644
--- a/config/locales/client.de.yml
+++ b/config/locales/client.de.yml
@@ -1049,6 +1049,7 @@ de:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "Tippe etwas ein, um zu filtern..."
admin:
title: 'Discourse Administrator'
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 3775cae92..f51176145 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1074,7 +1074,7 @@ en:
# This section is exported to the javascript for i18n in the admin section
admin_js:
- filter: "filter"
+ type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml
index a38aa47f1..4e04aebfa 100644
--- a/config/locales/client.es.yml
+++ b/config/locales/client.es.yml
@@ -793,6 +793,7 @@ es:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "type to filter..."
admin:
title: 'Administrador'
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
index 44e0b3f94..32a94c5bc 100644
--- a/config/locales/client.fr.yml
+++ b/config/locales/client.fr.yml
@@ -1032,6 +1032,7 @@ fr:
create_post: "Répondre / Voir"
readonly: "Voir"
admin_js:
+ type_to_filter: "Commencez à taper pour filtrer..."
admin:
title: 'Administation Discourse'
moderator: 'Modérateur'
diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml
index 4ffb45691..b2ccba06c 100644
--- a/config/locales/client.id.yml
+++ b/config/locales/client.id.yml
@@ -638,6 +638,7 @@ id:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml
index 7b279a80a..b17490514 100644
--- a/config/locales/client.it.yml
+++ b/config/locales/client.it.yml
@@ -1001,6 +1001,7 @@ it:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "scrivi per filtrare..."
admin:
title: 'Amministrazione Discourse'
diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml
index 722c8a881..2abaab6bc 100644
--- a/config/locales/client.ko.yml
+++ b/config/locales/client.ko.yml
@@ -841,6 +841,7 @@ ko:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "필터를 입력하세요"
admin:
title: 'Discourse 관리자'
diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml
index 3b56e82cc..a18da7acb 100644
--- a/config/locales/client.nb_NO.yml
+++ b/config/locales/client.nb_NO.yml
@@ -914,6 +914,7 @@ nb_NO:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "skriv for å filtrere..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml
index 8d3dc6357..5e64204e5 100644
--- a/config/locales/client.nl.yml
+++ b/config/locales/client.nl.yml
@@ -1014,6 +1014,7 @@ nl:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: typ om te filteren...
admin:
title: Discourse Beheer
diff --git a/config/locales/client.pseudo.yml b/config/locales/client.pseudo.yml
index 5e7f47417..172a7f312 100644
--- a/config/locales/client.pseudo.yml
+++ b/config/locales/client.pseudo.yml
@@ -969,6 +969,7 @@ pseudo:
ƀřóŵšéř íš ťóó ółď ťó ŵóřǩ óɳ ťĥíš Ďíščóůřšé ƒóřůɱá>. Рłéášé <á ĥřéƒ="ĥťťƿ://ƀřóŵšéĥáƿƿý.čóɱ">ůƿǧřáďé
ýóůř ƀřóŵšéřá>. ]]'
admin_js:
+ type_to_filter: '[[ ťýƿé ťó ƒíłťéř... ]]'
admin:
title: '[[ Ďíščóůřšé Áďɱíɳ ]]'
moderator: '[[ Ϻóďéřáťóř ]]'
diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml
index 857a28344..c6a2179cb 100644
--- a/config/locales/client.pt.yml
+++ b/config/locales/client.pt.yml
@@ -602,6 +602,7 @@ pt:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "escreve para filtrar..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml
index aaef72092..3b0e986d5 100644
--- a/config/locales/client.pt_BR.yml
+++ b/config/locales/client.pt_BR.yml
@@ -1069,6 +1069,7 @@ pt_BR:
# Essa seção é para o javascript para i18n no admin
admin_js:
+ type_to_filter: "escreva para filtrar..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
index 1d1373eaf..406c9ab29 100644
--- a/config/locales/client.ru.yml
+++ b/config/locales/client.ru.yml
@@ -1085,6 +1085,7 @@ ru:
create_post: 'Отвечать / Просматривать'
readonly: Просматривать
admin_js:
+ type_to_filter: 'Введите текст для фильтрации...'
admin:
title: 'Discourse Admin'
moderator: Модератор
diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml
index a07376eaa..2fedf9af4 100644
--- a/config/locales/client.sv.yml
+++ b/config/locales/client.sv.yml
@@ -788,6 +788,7 @@ sv:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "skriv för att filtrera..."
admin:
title: 'Discourse Admin'
diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
index 2843bda89..fce4dca35 100644
--- a/config/locales/client.zh_CN.yml
+++ b/config/locales/client.zh_CN.yml
@@ -657,8 +657,7 @@ zh_CN:
auto_close_notice: "本主题将在%{timeLeft}后自动关闭"
auto_close_title: '自动关闭设置'
auto_close_save: "保存"
- auto_close_cancel: "取消"
- auto_close_remove: "不自动关闭该主题"
+ auto_close_remove: "不要自动关闭该主题"
progress:
title: 主题进度
@@ -1075,9 +1074,10 @@ zh_CN:
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ filter: "过滤器"
admin:
- title: '论道 管理'
+ title: 'Discourse管理'
moderator: '版主'
dashboard:
@@ -1259,6 +1259,8 @@ zh_CN:
change_site_setting: "更改站点设置"
change_site_customization: "更改站点自定义"
delete_site_customization: "删除站点自定义"
+ ban_user: "禁止用户"
+ unban_user: "解禁用户"
screened_emails:
title: "被屏蔽的邮件地址"
description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。"
@@ -1330,7 +1332,11 @@ zh_CN:
user:
ban_failed: "禁止此用户时发生了错误 {{error}}"
unban_failed: "解禁此用户时发生了错误 {{error}}"
- ban_duration: "你计划禁止该用户多久?(天)"
+ ban_duration: "你计划禁止该用户多久?"
+ ban_duration_units: "(天)"
+ ban_reason_label: "为什么禁止该用户?当其尝试登入时,会看到这条理由。"
+ ban_reason: "禁止的理由"
+ banned_by: "禁止操作者:"
delete_all_posts: "删除所有帖子"
delete_all_posts_confirm: "你将删除 %{posts} 个帖子和 %{topics} 个主题,确认吗?"
ban: "禁止"
@@ -1387,7 +1393,8 @@ zh_CN:
deactivate_explanation: "已停用的用户必须重新验证他们的电子邮件。"
banned_explanation: "被禁止的用户无法登录。"
block_explanation: "被封禁的用户不能发表主题或者评论。"
- trust_level_change_failed: "改变用户等级时出现了一个问题,"
+ trust_level_change_failed: "改变用户等级时出现了一个问题。"
+ ban_modal_title: "禁止用户"
site_content:
none: "选择内容类型以开始编辑。"
diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml
index 36b940e88..7d913a204 100644
--- a/config/locales/client.zh_TW.yml
+++ b/config/locales/client.zh_TW.yml
@@ -968,6 +968,7 @@ zh_TW:
readonly: "觀看"
# This section is exported to the javascript for i18n in the admin section
admin_js:
+ type_to_filter: "輸入過濾條件……"
admin:
title: '論道 管理'
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index f1449556e..bf95d69ad 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -722,6 +722,7 @@ en:
dominating_topic_minimum_percent: "What percentage of posts a user has to make in a topic before we consider it dominating."
enable_names: "Allow users to show their full names"
+ display_name_on_posts: "Also show a user's full name on their posts"
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"
diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb
index 1eb90b22b..14341fccc 100644
--- a/lib/auth/default_current_user_provider.rb
+++ b/lib/auth/default_current_user_provider.rb
@@ -2,10 +2,9 @@ require_dependency "auth/current_user_provider"
class Auth::DefaultCurrentUserProvider
- CURRENT_USER_KEY = "_DISCOURSE_CURRENT_USER"
- API_KEY = "_DISCOURSE_API"
-
- TOKEN_COOKIE = "_t"
+ CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
+ API_KEY ||= "_DISCOURSE_API"
+ TOKEN_COOKIE ||= "_t"
# do all current user initialization here
def initialize(env)
@@ -64,9 +63,19 @@ class Auth::DefaultCurrentUserProvider
user.save!
end
cookies.permanent[TOKEN_COOKIE] = { value: user.auth_token, httponly: true }
+ make_developer_admin(user)
@env[CURRENT_USER_KEY] = user
end
+ def make_developer_admin(user)
+ if user.active? &&
+ !user.admin &&
+ Rails.configuration.respond_to?(:developer_emails) &&
+ Rails.configuration.developer_emails.include?(user.email)
+ user.update_column(:admin, true)
+ end
+ end
+
def log_off_user(session, cookies)
cookies[TOKEN_COOKIE] = nil
end
diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb
index b3e884c0c..30bd513a7 100644
--- a/lib/autospec/base_runner.rb
+++ b/lib/autospec/base_runner.rb
@@ -1,22 +1,36 @@
module Autospec
+
class BaseRunner
- def run(args, specs)
- end
-
- def abort
- end
-
- def reload
+
+ # used when starting the runner - preloading happens here
+ def start(opts = {})
end
+ # indicates whether tests are running
def running?
true
end
- def start
+ # launch a batch of specs/tests
+ def run(specs)
end
+ # used when we need to reload the whole application
+ def reload
+ end
+
+ # used to abort the current run
+ def abort
+ end
+
+ def failed_specs
+ []
+ end
+
+ # used to stop the runner
def stop
end
+
end
+
end
diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb
index 32bdd8105..d3ec0d892 100644
--- a/lib/autospec/formatter.rb
+++ b/lib/autospec/formatter.rb
@@ -1,39 +1,46 @@
-require "rspec/core/formatters/base_formatter"
+require "rspec/core/formatters/base_text_formatter"
module Autospec; end
-class Autospec::Formatter < RSpec::Core::Formatters::BaseFormatter
- def dump_summary(duration, total, failures, pending)
- # failed_specs = examples.delete_if{|e| e.execution_result[:status] != "failed"}.map{|s| s.metadata[:location]}
+class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
- # # if this fails don't kill everything
- # begin
- # FileUtils.mkdir_p('tmp')
- # File.open("./tmp/rspec_result","w") do |f|
- # f.puts failed_specs.join("\n")
- # end
- # rescue
- # # nothing really we can do, at least don't kill the test runner
- # end
+ RSPEC_RESULT = "./tmp/rspec_result"
+
+ def initialize(output)
super
+ FileUtils.mkdir_p("tmp") unless Dir.exists?("tmp")
end
- def start(count)
- FileUtils.mkdir_p('tmp')
- @fail_file = File.open("./tmp/rspec_result","w")
- super(count)
+ def start(example_count)
+ super
+ File.delete(RSPEC_RESULT) if File.exists?(RSPEC_RESULT)
+ @fail_file = File.open(RSPEC_RESULT,"w")
+ end
+
+ def example_passed(example)
+ super
+ output.print success_color(".")
+ end
+
+ def example_pending(example)
+ super
+ output.print pending_color("*")
+ end
+
+ def example_failed(example)
+ super
+ output.print failure_color("F")
+ @fail_file.puts(example.metadata[:location] + " ")
+ @fail_file.flush
+ end
+
+ def start_dump
+ super
+ output.puts
end
def close
@fail_file.close
- super
end
- def example_failed(example)
- @fail_file.puts example.metadata[:location]
- @fail_file.flush
- super(example)
- end
-
-
end
diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb
new file mode 100644
index 000000000..2d9760833
--- /dev/null
+++ b/lib/autospec/manager.rb
@@ -0,0 +1,241 @@
+require "listen"
+require "thread"
+require "fileutils"
+require "autospec/reload_css"
+require "autospec/base_runner"
+
+module Autospec; end
+
+class Autospec::Manager
+
+ def self.run(opts={})
+ self.new.run(opts)
+ end
+
+ def initialize
+ @queue = []
+ @mutex = Mutex.new
+ @signal = ConditionVariable.new
+ end
+
+ def run(opts = {})
+ @runners = [ruby_runner, javascript_runner]
+
+ Signal.trap("HUP") { stop_runners; exit }
+ Signal.trap("INT") { stop_runners; exit }
+
+ ensure_all_specs_will_run
+ start_runners
+ start_service_queue
+ listen_for_changes
+
+ puts "Press [ENTER] to stop the current run"
+ while @runners.any?(&:running?)
+ STDIN.gets
+ process_queue
+ end
+
+ rescue => e
+ fail(e, "failed in run")
+ ensure
+ stop_runners
+ end
+
+ private
+
+ def ruby_runner
+ if ENV["SPORK"]
+ require "autospec/spork_runner"
+ Autospec::SporkRunner.new
+ else
+ require "autospec/simple_runner"
+ Autospec::SimpleRunner.new
+ end
+ end
+
+ def javascript_runner
+ require "autospec/qunit_runner"
+ Autospec::QunitRunner.new
+ end
+
+ def ensure_all_specs_will_run
+ @runners.each do |runner|
+ @queue << ['spec', 'spec', runner] unless @queue.any? { |f, s, r| s == "spec" && r == runner }
+ end
+ end
+
+ [:start, :stop, :abort].each do |verb|
+ define_method("#{verb}_runners") do
+ @runners.each(&verb)
+ end
+ end
+
+ def start_service_queue
+ Thread.new do
+ while true
+ thread_loop
+ end
+ end
+ end
+
+ # the main loop, will run the specs in the queue till one fails or the queue is empty
+ def thread_loop
+ @mutex.synchronize do
+ current = @queue.first
+ last_failed = false
+ last_failed = process_spec(current) if current
+ # stop & wait for the queue to have at least one item or when there's been a failure
+ @signal.wait(@mutex) if @queue.length == 0 || last_failed
+ end
+ rescue => e
+ fail(e, "failed in main loop")
+ end
+
+ # will actually run the spec and check whether the spec has failed or not
+ def process_spec(current)
+ has_failed = false
+ # retrieve the instance of the runner
+ runner = current[2]
+ # actually run the spec (blocking call)
+ result = runner.run(current[1]).to_i
+
+ if result == 0
+ # remove the spec from the queue
+ @queue.shift
+ else
+ has_failed = true
+ if result > 0
+ focus_on_failed_tests(current)
+ ensure_all_specs_will_run
+ end
+ end
+
+ has_failed
+ end
+
+ def focus_on_failed_tests(current)
+ runner = current[2]
+ # we only want 1 focus in the queue
+ @queue.shift if current[0] == "focus"
+ # focus on the first 10 failed specs
+ failed_specs = runner.failed_specs[0..10]
+ # focus on the failed specs
+ @queue.unshift ["focus", failed_specs.join(" "), runner] if failed_specs.length > 0
+ end
+
+ def listen_for_changes(opts = {})
+ options = {
+ ignore: /^public|^lib\/autospec/,
+ relative_paths: true,
+ }
+
+ if opts[:force_polling]
+ options[:force_polling] = true
+ options[:latency] = opts[:latency] || 3
+ end
+
+ Thread.start do
+ Listen.to('.', options) do |modified, added, removed|
+ process_change([modified, added].flatten.compact)
+ end
+ end
+ end
+
+ def process_change(files)
+ return if files.length == 0
+ specs = []
+ hit = false
+
+ files.each do |file|
+ @runners.each do |runner|
+ # reloaders
+ runner.reloaders.each do |k|
+ if k.match(file)
+ runner.reload
+ return
+ end
+ end
+ # watchers
+ runner.watchers.each do |k,v|
+ if m = k.match(file)
+ hit = true
+ spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
+ specs << [file, spec, runner] if File.exists?(spec) || Dir.exists?(spec)
+ end
+ end
+ end
+ # special watcher for styles/templates
+ Autospec::ReloadCss::WATCHERS.each do |k,v|
+ matches = []
+ matches << file if k.match(file)
+ Autospec::ReloadCss.run_on_change(matches) if matches.present?
+ end
+ end
+
+ queue_specs(specs) if hit
+
+ rescue => e
+ fail(e, "failed in watcher")
+ end
+
+ def queue_specs(specs)
+ if specs.length == 0
+ locked = @mutex.try_lock
+ if locked
+ @signal.signal
+ @mutex.unlock
+ end
+ return
+ else
+ abort_runners
+ end
+
+ @mutex.synchronize do
+ specs.each do |file, spec, runner|
+ # make sure there's no other instance of this spec in the queue
+ @queue.delete_if { |f, s, r| s.strip == spec.strip && r == runner }
+ # deal with focused specs
+ if @queue.first && @queue.first[0] == "focus"
+ focus = @queue.shift
+ @queue.unshift([file, spec, runner])
+ if focus[1].include?(spec) || file != spec
+ @queue.unshift(focus)
+ end
+ else
+ @queue.unshift([file, spec, runner])
+ end
+ end
+ @signal.signal
+ end
+ end
+
+ def process_queue
+ if @queue.length == 0
+ ensure_all_specs_will_run
+ @signal.signal
+ else
+ current = @queue.first
+ runner = current[2]
+ specs = runner.failed_specs
+ puts
+ puts
+ if specs.length == 0
+ puts "No specs have failed yet!"
+ puts
+ else
+ puts "The following specs have failed:"
+ specs.each { |s| puts s }
+ puts
+ specs = specs.map { |s| [s, s, runner] }
+ queue_specs(specs)
+ end
+ end
+ end
+
+ def fail(exception, message = nil)
+ puts message if message
+ puts exception.message
+ puts exception.backtrace.join("\n")
+ end
+
+end
diff --git a/lib/autospec/qunit_runner.rb b/lib/autospec/qunit_runner.rb
new file mode 100644
index 000000000..3dae8a2f2
--- /dev/null
+++ b/lib/autospec/qunit_runner.rb
@@ -0,0 +1,150 @@
+require "demon/rails_autospec"
+
+module Autospec
+
+ class QunitRunner < BaseRunner
+
+ WATCHERS = {}
+ def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
+ def watchers; WATCHERS; end
+
+ # Discourse specific
+ watch(%r{^app/assets/javascripts/discourse/(.+)\.js$}) { |m| "test/javascripts/#{m[1]}_test.js" }
+ watch(%r{^app/assets/javascripts/admin/(.+)\.js$}) { |m| "test/javascripts/admin/#{m[1]}_test.js" }
+ watch(%r{^test/javascripts/.+\.js$})
+
+ RELOADERS = Set.new
+ def self.reload(pattern); RELOADERS << pattern; end
+ def reloaders; RELOADERS; end
+
+ # Discourse specific
+ reload(%r{^test/javascripts/fixtures/.+_fixtures\.js$})
+ reload(%r{^test/javascripts/(helpers|mixins)/.+\.js$})
+ reload("test/javascripts/test_helper.js")
+
+ require "socket"
+
+ class PhantomJsNotInstalled < Exception; end
+
+ def initialize
+ ensure_phantomjs_is_installed
+ end
+
+ def start
+ # ensure we can launch the rails server
+ unless port_available?(port)
+ puts "Port #{port} is not available"
+ puts "Either kill the process using that port or use the `TEST_SERVER_PORT` environment variable"
+ return
+ end
+
+ # start rails
+ start_rails_server
+ @running = true
+ end
+
+ def running?
+ @running
+ end
+
+ def run(specs)
+ puts "Running Qunit: #{specs}"
+
+ abort
+
+ qunit_url = "http://localhost:#{port}/qunit"
+
+ if specs != "spec" && specs.split.length == 1
+ module_name = try_to_find_module_name(specs.strip)
+ qunit_url << "?module=#{module_name}" if module_name
+ end
+
+ cmd = "phantomjs #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\""
+
+ @pid = Process.spawn(cmd)
+ _, status = Process.wait2(@pid)
+
+ status.exitstatus
+ end
+
+ def reload
+ stop_rails_server
+ sleep 1
+ start_rails_server
+ end
+
+ def abort
+ if @pid
+ children_processes(@pid).each { |pid| kill_process(pid) }
+ kill_process(@pid)
+ @pid = nil
+ end
+ end
+
+ def failed_specs
+ specs = []
+ path = './tmp/qunit_result'
+ specs = File.readlines(path) if File.exist?(path)
+ specs
+ end
+
+ def stop
+ # kill phantomjs first
+ abort
+ stop_rails_server
+ @running = false
+ end
+
+ private
+
+ def ensure_phantomjs_is_installed
+ raise PhantomJsNotInstalled.new unless system("command -v phantomjs >/dev/null;")
+ end
+
+ def port_available?(port)
+ TCPServer.open(port).close
+ true
+ rescue Errno::EADDRINUSE
+ false
+ end
+
+ def port
+ @port ||= ENV["TEST_SERVER_PORT"] || 60099
+ end
+
+ def start_rails_server
+ Demon::RailsAutospec.start(1)
+ end
+
+ def stop_rails_server
+ Demon::RailsAutospec.stop
+ end
+
+ def children_processes(base = Process.pid)
+ process_tree = Hash.new { |hash, key| hash[key] = [key] }
+ Hash[*`ps -eo pid,ppid`.scan(/\d+/).map(&:to_i)].each do |pid, ppid|
+ process_tree[ppid] << process_tree[pid]
+ end
+ process_tree[base].flatten - [base]
+ end
+
+ def kill_process(pid)
+ return unless pid
+ Process.kill("INT", pid) rescue nil
+ while (Process.getpgid(pid) rescue nil)
+ sleep 0.001
+ end
+ end
+
+ def try_to_find_module_name(file)
+ return unless File.exists?(file)
+ File.open(file, "r").each_line do |line|
+ if m = /module\(['"]([^'"]+)/i.match(line)
+ return m[1]
+ end
+ end
+ end
+
+ end
+
+end
diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb
index ab4d9f74d..5c69bdf94 100644
--- a/lib/autospec/reload_css.rb
+++ b/lib/autospec/reload_css.rb
@@ -1,23 +1,23 @@
module Autospec; end
+
class Autospec::ReloadCss
- MATCHERS = {}
+ WATCHERS = {}
def self.watch(pattern, &blk)
- MATCHERS[pattern] = blk
+ WATCHERS[pattern] = blk
end
- watch(/tmp\/refresh_browser/)
+ # css, scss, sass or handlebars
watch(/\.css$/)
- watch(/\.css\.erb$/)
- watch(/\.sass$/)
- watch(/\.scss$/)
- watch(/\.sass\.erb$/)
+ watch(/\.ca?ss\.erb$/)
+ watch(/\.s[ac]ss$/)
watch(/\.handlebars$/)
def self.message_bus
MessageBus::Instance.new.tap do |bus|
bus.site_id_lookup do
- # this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different
+ # this is going to be dev the majority of the time
+ # if you have multisite configured in dev stuff may be different
"default"
end
end
@@ -26,13 +26,13 @@ class Autospec::ReloadCss
def self.run_on_change(paths)
paths.map! do |p|
hash = nil
- fullpath = Rails.root.to_s + "/" + p
- hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists? fullpath
+ fullpath = "#{Rails.root}/#{p}"
+ hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists?(fullpath)
p = p.sub /\.sass\.erb/, ""
p = p.sub /\.sass/, ""
p = p.sub /\.scss/, ""
p = p.sub /^app\/assets\/stylesheets/, "assets"
- {name: p, hash: hash}
+ { name: p, hash: hash }
end
message_bus.publish "/file-change", paths
end
diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb
new file mode 100644
index 000000000..fe5f23703
--- /dev/null
+++ b/lib/autospec/rspec_runner.rb
@@ -0,0 +1,43 @@
+module Autospec
+
+ class RspecRunner < BaseRunner
+
+ WATCHERS = {}
+ def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
+ def watchers; WATCHERS; end
+
+ # Discourse specific
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
+
+ # Rails example
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
+ watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
+ watch(%r{^spec/.+_spec\.rb$})
+ watch(%r{^spec/support/.+\.rb$}) { "spec" }
+ watch("app/controllers/application_controller.rb") { "spec/controllers" }
+
+ # Capybara request specs
+ watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
+
+ # Fabrication
+ watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" }
+
+ RELOADERS = Set.new
+ def self.reload(pattern); RELOADERS << pattern; end
+ def reloaders; RELOADERS; end
+
+ # We need to reload the whole app when changing any of these files
+ reload('spec/spec_helper.rb')
+ reload('config/(.*).rb')
+ reload('app/helpers/(.*).rb')
+
+ def failed_specs
+ specs = []
+ path = './tmp/rspec_result'
+ specs = File.readlines(path) if File.exist?(path)
+ specs
+ end
+
+ end
+
+end
diff --git a/lib/autospec/run-qunit.js b/lib/autospec/run-qunit.js
new file mode 100644
index 000000000..15804f269
--- /dev/null
+++ b/lib/autospec/run-qunit.js
@@ -0,0 +1,175 @@
+// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC
+
+if (phantom.args.length != 1) {
+ console.log("Usage: " + phantom.scriptName + " ");
+ phantom.exit(1);
+}
+
+var system = require('system'),
+ fs = require('fs'),
+ page = require('webpage').create(),
+ QUNIT_RESULT = "./tmp/qunit_result";
+
+if (fs.exists(QUNIT_RESULT) && fs.isFile(QUNIT_RESULT)) { fs.remove(QUNIT_RESULT); }
+
+page.onConsoleMessage = function (message) {
+ // filter out Ember's debug messages
+ if (message.slice(0, 8) === "WARNING:") { return; }
+ if (message.slice(0, 6) === "DEBUG:") { return; }
+
+ console.log(message);
+};
+
+page.onCallback = function (message) {
+ // write to the result file
+ if (message.slice(0, 5) === "FILE:") { fs.write(QUNIT_RESULT, message.slice(6), "a"); }
+ // forward the message to the standard output
+ if (message.slice(0, 6) === "PRINT:") { system.stdout.write(message.slice(7)); }
+};
+
+page.start = new Date();
+
+// -----------------------------------WARNING --------------------------------------
+// calling "console.log" BELOW this line will go through the "page.onConsoleMessage"
+// -----------------------------------WARNING --------------------------------------
+page.open(phantom.args[0], function (status) {
+ if (status !== "success") {
+ console.log("\nNO NETWORK :(\n");
+ phantom.exit(1);
+ } else {
+ console.log("QUnit loaded in " + (new Date() - page.start) + " ms");
+
+ page.evaluate(colorizer);
+ page.evaluate(logQUnit);
+
+ // wait up to 60 seconds for QUnit to finish
+ var timeout = 60 * 1000,
+ start = Date.now();
+
+ var interval = setInterval(function() {
+ if (Date.now() - start > timeout) {
+ console.error("\nTIME OUT :(\n");
+ phantom.exit(1);
+ } else {
+ var qunitResult = page.evaluate(function() { return window.qunitResult; });
+ if (qunitResult) {
+ clearInterval(interval);
+ if (qunitResult.failed > 0) {
+ phantom.exit(1);
+ } else {
+ phantom.exit(0);
+ }
+ }
+ }
+ }, 250);
+ }
+});
+
+// https://github.com/jquery/qunit/pull/470
+function colorizer() {
+ window.ANSI = {
+ colorMap: {
+ "red": "\u001b[31m",
+ "green": "\u001b[32m",
+ "blue": "\u001b[34m",
+ "end": "\u001b[0m"
+ },
+ highlightMap: {
+ "red": "\u001b[41m\u001b[37m", // change 37 to 30 for black text
+ "green": "\u001b[42m\u001b[30m",
+ "blue": "\u001b[44m\u001b[37m",
+ "end": "\u001b[0m"
+ },
+
+ highlight: function (text, color) {
+ var colorCode = this.highlightMap[color],
+ colorEnd = this.highlightMap.end;
+
+ return colorCode + text + colorEnd;
+ },
+
+ colorize: function (text, color) {
+ var colorCode = this.colorMap[color],
+ colorEnd = this.colorMap.end;
+
+ return colorCode + text + colorEnd;
+ }
+ };
+};
+
+
+function logQUnit() {
+ // keep track of error messages
+ var errors = {};
+
+ QUnit.begin(function () {
+ console.log("BEGIN");
+ });
+
+ QUnit.log(function (context) {
+ if (!context.result) {
+ var module = context.module,
+ test = context.name;
+
+ var assertion = {
+ message: context.message,
+ expected: context.expected,
+ actual: context.actual
+ };
+
+ if (!errors[module]) { errors[module] = {}; }
+ if (!errors[module][test]) { errors[module][test] = []; }
+ errors[module][test].push(assertion);
+
+ var fileName = context.source
+ .replace(/[^\S\n]+at[^\S\n]+/g, "")
+ .split("\n")[1]
+ .replace(/\?.+$/, "")
+ .replace(/^.+\/assets\//, "test/javascripts/");
+ window.callPhantom("FILE: " + fileName + " ");
+ }
+ });
+
+ QUnit.testDone(function (context) {
+ if (context.failed > 0) {
+ window.callPhantom("PRINT: " + ANSI.colorize("F", "red"));
+ } else {
+ window.callPhantom("PRINT: " + ANSI.colorize(".", "green"));
+ }
+ });
+
+ QUnit.done(function (context) {
+ console.log("\n");
+
+ // display failures
+ if (Object.keys(errors).length > 0) {
+ console.log("Failures:\n");
+ for (m in errors) {
+ var module = errors[m];
+ console.log("Module Failed: " + ANSI.highlight(m, "red"));
+ for (t in module) {
+ var test = module[t];
+ console.log(" Test Failed: " + t);
+ for (var a = 0; a < test.length; a++) {
+ var assertion = test[a];
+ console.log(" Assertion Failed: " + (assertion.message || ""));
+ if (assertion.expected) {
+ console.log(" Expected: " + assertion.expected);
+ console.log(" Actual: " + assertion.actual);
+ }
+ }
+ }
+ }
+ }
+
+ // display summary
+ console.log("\n");
+ console.log("Finished in " + (context.runtime / 1000) + " seconds");
+ var color = context.failed > 0 ? "red" : "green";
+ console.log(ANSI.colorize(context.total + " examples, " + context.failed + " failures", color));
+
+ // we're done
+ window.qunitResult = context;
+ });
+
+};
diff --git a/lib/autospec/runner.rb b/lib/autospec/runner.rb
deleted file mode 100644
index 7ba9cbb52..000000000
--- a/lib/autospec/runner.rb
+++ /dev/null
@@ -1,308 +0,0 @@
-require "drb/drb"
-require "thread"
-require "fileutils"
-require "autospec/reload_css"
-require "autospec/base_runner"
-require "autospec/simple_runner"
-require "autospec/spork_runner"
-
-module Autospec; end
-
-class Autospec::Runner
- MATCHERS = {}
- def self.watch(pattern, &blk)
- MATCHERS[pattern] = blk
- end
-
- watch(%r{^spec/.+_spec\.rb$})
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
-
- # Rails example
- watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
- watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
- watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" }
- watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
- watch("app/controllers/application_controller.rb") { "spec/controllers" }
-
- # Capybara request specs
- watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
-
- # Fabrication
- watch(%r{^spec/fabricators/(.+)_fabricator\.rb$}) { "spec" }
-
- RELOAD_MATCHERS = Set.new
- def self.watch_reload(pattern)
- RELOAD_MATCHERS << pattern
- end
-
- watch_reload('spec/spec_helper.rb')
- watch_reload('config/(.*).rb')
- watch_reload(%r{app/helpers/(.*).rb})
-
- def self.run(opts={})
- self.new.run(opts)
- end
-
- def initialize
- @queue = []
- @mutex = Mutex.new
- @signal = ConditionVariable.new
- start_service_queue
- end
-
- def run(opts = {})
-
- puts "Forced polling (slower) - inotify does not work on network filesystems, use local filesystem to avoid" if opts[:force_polling]
-
- if ENV["SPORK"] == "0"
- puts "Using Simple Runner"
- @runner = Autospec::SimpleRunner.new
- else
- puts "Using Spork Runner"
- @runner = Autospec::SporkRunner.new
- end
- @runner.start
-
- Signal.trap("HUP") {@runner.stop; exit }
- Signal.trap("SIGINT") {@runner.stop; exit }
-
- options = {filter: /^app|^spec|^lib/, relative_paths: true}
-
- if opts[:force_polling]
- options[:force_polling] = true
- options[:latency] = opts[:latency] || 3
- end
-
- Thread.start do
- Listen.to('.', options ) do |modified, added, removed|
- process_change([modified, added].flatten.compact)
- end
- end
-
- @mutex.synchronize do
- @queue << ['spec', 'spec']
- @signal.signal
- end
-
- while @runner.running?
- process_queue
- end
-
- rescue => e
- puts e
- puts e.backtrace
- @runner.stop
- end
-
- def process_queue
- STDIN.gets
-
- if @queue.length == 0
- @queue << ['spec', 'spec']
- @signal.signal
- else
- specs = failed_specs(:delete => false)
- puts
- puts
- if specs.length == 0
- puts "No specs have failed yet!"
- puts
- else
- puts "The following specs have failed: "
- specs.each do |s|
- puts s
- end
- puts
- queue_specs(specs.zip specs)
- end
- end
- end
-
- def wait_for(timeout_milliseconds)
- timeout = (timeout_milliseconds + 0.0) / 1000
- finish = Time.now + timeout
- t = Thread.new do
- while Time.now < finish && !yield
- sleep(0.001)
- end
- end
- t.join rescue nil
- end
-
- def force_polling?
- works = false
-
- begin
- require 'rb-inotify'
- require 'fileutils'
- n = INotify::Notifier.new
- FileUtils.touch('./tmp/test_polling')
-
- n.watch("./tmp", :modify, :attrib){ works = true }
- quit = false
- Thread.new do
- while !works && !quit
- if IO.select([n.to_io], [], [], 0.1)
- n.process
- end
- end
- end
- sleep 0.01
-
- FileUtils.touch('./tmp/test_polling')
- wait_for(100) { works }
- File.unlink('./tmp/test_polling')
- n.stop
- quit = true
- rescue LoadError
- #assume it works (mac)
- works = true
- end
-
- !works
- end
-
-
- def process_change(files)
- return unless files.length > 0
-
- specs = []
- hit = false
- files.each do |file|
- RELOAD_MATCHERS.each do |k|
- if k.match(file)
- @runner.reload
- return
- end
- end
- MATCHERS.each do |k,v|
- if m = k.match(file)
- hit = true
- spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file
- if File.exists?(spec) || Dir.exists?(spec)
- specs << [file, spec]
- end
- end
- end
- Autospec::ReloadCss::MATCHERS.each do |k,v|
- matches = []
- if k.match(file)
- matches << file
- end
- Autospec::ReloadCss.run_on_change(matches) if matches.present?
- end
- end
- queue_specs(specs) if hit
- rescue => e
- p "failed in watcher"
- p e
- p e.backtrace
- end
-
- def queue_specs(specs)
- if specs.length == 0
- locked = @mutex.try_lock
- if locked
- @signal.signal
- @mutex.unlock
- end
- return
- else
- @runner.abort
- end
-
- @mutex.synchronize do
- specs.each do |c,spec|
- @queue.delete([c,spec])
- if @queue.last && @queue.last[0] == "focus"
- focus = @queue.pop
- @queue << [c,spec]
- if focus[1].include?(spec) || c != spec
- @queue << focus
- end
- else
- @queue << [c,spec]
- end
- end
- @signal.signal
- end
- end
-
- def thread_loop
- @mutex.synchronize do
- last_failed = false
- current = @queue.last
- if current
- last_failed = process_spec(current[1])
- end
- wait = @queue.length == 0 || last_failed
- @signal.wait(@mutex) if wait
- end
- rescue => e
- p "DISASTA PASTA"
- puts e
- puts e.backtrace
- end
-
- def process_spec(spec)
- last_failed = false
- result = run_spec(spec)
- if result == 0
- @queue.pop
- else
- last_failed = true
- if result.to_i > 0
- focus_on_failed_tests
- ensure_all_specs_will_run
- end
- end
-
- last_failed
- end
-
- def start_service_queue
- @worker ||= Thread.new do
- while true
- thread_loop
- end
- end
- end
-
- def focus_on_failed_tests
- current = @queue.last
- specs = failed_specs[0..10]
- if current[0] == "focus"
- @queue.pop
- end
- @queue << ["focus", specs.join(" ")]
- end
-
- def ensure_all_specs_will_run
- unless @queue.any?{|s,t| t == 'spec'}
- @queue.unshift(['spec','spec'])
- end
- end
-
- def failed_specs(opts={:delete => true})
- specs = []
- path = './tmp/rspec_result'
- if File.exist?(path)
- specs = File.open(path) { |file| file.read.split("\n") }
- File.delete(path) if opts[:delete]
- end
-
- specs
- end
-
- def run_spec(specs)
- File.delete("tmp/rspec_result") if File.exists?("tmp/rspec_result")
- args = ["-f", "progress", specs.split(" "),
- "-r", "#{File.dirname(__FILE__)}/formatter.rb",
- "-f", "Autospec::Formatter"].flatten
-
- @runner.run(args, specs)
-
- end
-
-
-end
diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb
index d43cb32ab..e94d3d934 100644
--- a/lib/autospec/simple_runner.rb
+++ b/lib/autospec/simple_runner.rb
@@ -1,26 +1,36 @@
+require "autospec/rspec_runner"
+
module Autospec
- class SimpleRunner < BaseRunner
+
+ class SimpleRunner < RspecRunner
+
+ def run(specs)
+ puts "Running Rspec: " << specs
+ # kill previous rspec instance
+ abort
+ # we use our custom rspec formatter
+ args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
+ "-f", "Autospec::Formatter", specs.split].flatten.join(" ")
+ # launch rspec
+ @pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec #{args}")
+ _, status = Process.wait2(@pid)
+ status.exitstatus
+ end
def abort
if @pid
- Process.kill("SIGINT", @pid) rescue nil
- while(Process.getpgid(@pid) rescue nil)
+ Process.kill("INT", @pid) rescue nil
+ while (Process.getpgid(@pid) rescue nil)
sleep 0.001
end
@pid = nil
end
end
- def run(args, spec)
- self.abort
- puts "Running: " << spec
- @pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec " << args.join(" "))
- pid, status = Process.wait2(@pid)
- status
+ def stop
+ abort
end
- def stop
- self.abort
- end
end
+
end
diff --git a/lib/autospec/spork_runner.rb b/lib/autospec/spork_runner.rb
index 95b352541..fbd05d93e 100644
--- a/lib/autospec/spork_runner.rb
+++ b/lib/autospec/spork_runner.rb
@@ -1,5 +1,9 @@
+require "drb/drb"
+require "autospec/rspec_runner"
+
module Autospec
- class SporkRunner < BaseRunner
+
+ class SporkRunner < RspecRunner
def start
if already_running?(pid_file)
@@ -13,33 +17,38 @@ module Autospec
end
def running?
+ # launch a thread that will wait for spork to die
@monitor_thread ||=
Thread.new do
Process.wait(@spork_pid)
@spork_running = false
end
+
@spork_running
end
- def stop
- stop_spork
- end
-
- def run(args,specs)
+ def run(specs)
+ args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
+ "-f", "Autospec::Formatter", specs.split].flatten
spork_service.run(args,$stderr,$stdout)
end
- def abort
- spork_service.abort
- end
-
def reload
stop_spork
sleep 1
start_spork
end
+ def abort
+ spork_service.abort
+ end
+
+ def stop
+ stop_spork
+ end
+
private
+
def spork_pid_file
Rails.root + "tmp/pids/spork.pid"
end
@@ -55,7 +64,7 @@ module Autospec
end
end
- def write_pid_file(file,pid)
+ def write_pid_file(file, pid)
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(file,'w') do |f|
f.write(pid)
@@ -67,25 +76,18 @@ module Autospec
end
def spork_service
-
unless @drb_listener_running
begin
DRb.start_service("druby://127.0.0.1:0")
rescue SocketError, Errno::EADDRNOTAVAIL
DRb.start_service("druby://:0")
end
-
@drb_listener_running = true
end
@spork_service ||= DRbObject.new_with_uri("druby://127.0.0.1:8989")
end
- def stop_spork
- pid = File.read(spork_pid_file).to_i
- Process.kill("SIGTERM",pid)
- end
-
def start_spork
if already_running?(spork_pid_file)
puts "Killing old orphan spork instance"
@@ -101,7 +103,13 @@ module Autospec
running = spork_running?
sleep 0.01
end
-
end
+
+ def stop_spork
+ pid = File.read(spork_pid_file).to_i
+ Process.kill("SIGTERM", pid) rescue nil
+ end
+
end
+
end
diff --git a/lib/demon/base.rb b/lib/demon/base.rb
new file mode 100644
index 000000000..ad059c887
--- /dev/null
+++ b/lib/demon/base.rb
@@ -0,0 +1,142 @@
+module Demon; end
+
+# intelligent fork based demonizer
+class Demon::Base
+
+ def self.start(count)
+ @demons ||= {}
+ count.times do |i|
+ (@demons["#{prefix}_#{i}"] ||= new(i)).start
+ end
+ end
+
+ def self.stop
+ return unless @demons
+ @demons.values.each do |demon|
+ demon.stop
+ end
+ end
+
+ def initialize(index)
+ @index = index
+ @pid = nil
+ @parent_pid = Process.pid
+ @monitor = nil
+ end
+
+ def pid_file
+ "#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
+ end
+
+ def stop
+ if @monitor
+ @monitor.kill
+ @monitor.join
+ @monitor = nil
+ end
+
+ if @pid
+ Process.kill("HUP",@pid)
+ @pid = nil
+ end
+ end
+
+ def start
+ if existing = already_running?
+ # should not happen ... so kill violently
+ Process.kill("TERM",existing)
+ end
+
+ return if @pid
+
+ if @pid = fork
+ write_pid_file
+ monitor_child
+ return
+ end
+
+ monitor_parent
+ establish_app
+ after_fork
+ end
+
+ def already_running?
+ if File.exists? pid_file
+ pid = File.read(pid_file).to_i
+ if alive?(pid)
+ return pid
+ end
+ end
+
+ nil
+ end
+
+ private
+
+ def monitor_child
+ @monitor ||= Thread.new do
+ while true
+ sleep 5
+ unless alive?(@pid)
+ STDERR.puts "#{@pid} died, restarting the process"
+ @pid = nil
+ start
+ end
+ end
+ end
+ end
+
+ def write_pid_file
+ FileUtils.mkdir_p(Rails.root + "tmp/pids")
+ File.open(pid_file,'w') do |f|
+ f.write(@pid)
+ end
+ end
+
+ def delete_pid_file
+ File.delete(pid_file)
+ end
+
+ def monitor_parent
+ Thread.new do
+ while true
+ unless alive?(@parent_pid)
+ Process.kill "QUIT", Process.pid
+ end
+ sleep 1
+ end
+ end
+ end
+
+ def alive?(pid)
+ begin
+ Process.getpgid(pid)
+ true
+ rescue Errno::ESRCH
+ false
+ end
+ end
+
+ def establish_app
+ ActiveRecord::Base.connection_handler.clear_active_connections!
+ ActiveRecord::Base.establish_connection
+ $redis.client.reconnect
+ Rails.cache.reconnect
+ MessageBus.after_fork
+
+ Signal.trap("HUP") do
+ begin
+ delete_pid_file
+ ensure
+ exit
+ end
+ end
+
+ # keep stuff simple for now
+ $stdout.reopen("/dev/null", "w")
+ $stderr.reopen("/dev/null", "w")
+ end
+
+ def after_fork
+ end
+end
diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb
new file mode 100644
index 000000000..92d15a8fb
--- /dev/null
+++ b/lib/demon/rails_autospec.rb
@@ -0,0 +1,25 @@
+require "demon/base"
+
+class Demon::RailsAutospec < Demon::Base
+
+ def self.prefix
+ "rails-autospec"
+ end
+
+ private
+
+ def after_fork
+ require "rack"
+ ENV["RAILS_ENV"] = "test"
+ Rack::Server.start(
+ :config => "config.ru",
+ :AccessLog => [],
+ :Port => ENV["TEST_SERVER_PORT"] || 60099,
+ )
+ rescue => e
+ STDERR.puts e.message
+ STDERR.puts e.backtrace.join("\n")
+ exit 1
+ end
+
+end
diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb
index f4820f35c..392128e1e 100644
--- a/lib/demon/sidekiq.rb
+++ b/lib/demon/sidekiq.rb
@@ -1,148 +1,7 @@
-module Demon; end
-
-# intelligent fork based demonizer for sidekiq
-class Demon::Base
-
- def self.start(count)
- @demons ||= {}
- count.times do |i|
- (@demons["#{prefix}_#{i}"] ||= new(i)).start
- end
- end
-
- def self.stop
- @demons.values.each do |demon|
- demon.stop
- end
- end
-
- def initialize(index)
- @index = index
- @pid = nil
- @parent_pid = Process.pid
- @monitor = nil
- end
-
- def pid_file
- "#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
- end
-
- def stop
- if @monitor
- @monitor.kill
- @monitor.join
- @monitor = nil
- end
-
- if @pid
- Process.kill("SIGHUP",@pid)
- @pid = nil
- end
- end
-
- def start
- if existing = already_running?
- # should not happen ... so kill violently
- Process.kill("SIGTERM",existing)
- end
-
- return if @pid
-
- if @pid = fork
- write_pid_file
- monitor_child
- return
- end
-
- monitor_parent
- establish_app
- after_fork
- end
-
- def already_running?
- if File.exists? pid_file
- pid = File.read(pid_file).to_i
- if alive?(pid)
- return pid
- end
- end
-
- nil
- end
-
- private
-
- def monitor_child
- @monitor ||= Thread.new do
- while true
- sleep 5
- unless alive?(@pid)
- STDERR.puts "#{@pid} died, restarting sidekiq"
- @pid = nil
- start
- end
- end
- end
- end
-
- def write_pid_file
- FileUtils.mkdir_p(Rails.root + "tmp/pids")
- File.open(pid_file,'w') do |f|
- f.write(@pid)
- end
- end
-
- def delete_pid_file
- File.delete(pid_file)
- end
-
- def monitor_parent
- Thread.new do
- while true
- unless alive?(@parent_pid)
- Process.kill "QUIT", Process.pid
- end
- sleep 1
- end
- end
- end
-
- def alive?(pid)
- begin
- Process.getpgid(pid)
- true
- rescue Errno::ESRCH
- false
- end
- end
-
- def establish_app
-
-
- ActiveRecord::Base.connection_handler.clear_active_connections!
- ActiveRecord::Base.establish_connection
- $redis.client.reconnect
- Rails.cache.reconnect
- MessageBus.after_fork
-
- Signal.trap("HUP") do
- begin
- delete_pid_file
- ensure
- exit
- end
- end
-
- # keep stuff simple for now
- $stdout.reopen("/dev/null", "w")
- # $stderr.reopen("/dev/null", "w")
- end
-
- def after_fork
- end
-end
+require "demon/base"
class Demon::Sidekiq < Demon::Base
+
def self.prefix
"sidekiq"
end
@@ -151,18 +10,15 @@ class Demon::Sidekiq < Demon::Base
def after_fork
require 'sidekiq/cli'
- begin
- # Reload initializer cause it needs to run after sidekiq/cli
- # was required
- load Rails.root + "config/initializers/sidekiq.rb"
- cli = Sidekiq::CLI.instance
- cli.parse([])
- cli.run
- rescue => e
- STDERR.puts e.message
- STDERR.puts e.backtrace.join("\n")
- exit 1
- end
-
+ # Reload initializer cause it needs to run after sidekiq/cli was required
+ load Rails.root + "config/initializers/sidekiq.rb"
+ cli = Sidekiq::CLI.instance
+ cli.parse([])
+ cli.run
+ rescue => e
+ STDERR.puts e.message
+ STDERR.puts e.backtrace.join("\n")
+ exit 1
end
+
end
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake
new file mode 100644
index 000000000..ad1c6b28c
--- /dev/null
+++ b/lib/tasks/assets.rake
@@ -0,0 +1,20 @@
+task 'assets:precompile' => 'environment' do
+ # see: https://github.com/rails/sprockets-rails/issues/49
+ # a decision was made no longer to copy non-digested assets
+ # this breaks stuff like the emoji plugin. We could fix it,
+ # but its a major pain with little benefit.
+ if rails4?
+ puts "Copying non-digested versions of assets"
+ assets = Dir.glob(File.join(Rails.root, 'public/assets/**/*'))
+ regex = /(-{1}[a-z0-9]{32}*\.{1}){1}/
+ assets.each do |file|
+ next if File.directory?(file) || file !~ regex
+
+ source = file.split('/')
+ source.push(source.pop.gsub(regex, '.'))
+
+ non_digested = File.join(source)
+ FileUtils.cp(file, non_digested)
+ end
+ end
+end
diff --git a/lib/tasks/autospec.rake b/lib/tasks/autospec.rake
index c4ca2008c..43455ac85 100644
--- a/lib/tasks/autospec.rake
+++ b/lib/tasks/autospec.rake
@@ -4,22 +4,17 @@
desc "Run all specs automatically as needed"
task "autospec" => :environment do
+ require 'autospec/manager'
- if RUBY_PLATFORM.include?('linux')
- require 'rb-inotify'
- end
-
- require 'listen'
-
- puts "If file watching is not working you can force polling with: bundle exec rake autospec p l=3"
- require 'autospec/runner'
-
- force_polling = ARGV.any?{|a| a == "p" || a == "polling"}
- latency = ((ARGV.find{|a| a =~ /l=|latency=/}||"").split("=")[1] || 3).to_i
+ force_polling = ARGV.any?{ |a| a == "p" || a == "polling" }
+ latency = ((ARGV.find{ |a| a =~ /l=|latency=/ } || "").split("=")[1] || 3).to_i
if force_polling
- puts "polling has been forced (slower) checking every #{latency} #{"second".pluralize(latency)}"
+ puts "Polling has been forced (slower) - checking every #{latency} #{"second".pluralize(latency)}"
+ else
+ puts "If file watching is not working, you can force polling with: bundle exec rake autospec p l=3"
end
- Autospec::Runner.run(force_polling: force_polling, latency: latency)
+ Autospec::Manager.run(force_polling: force_polling, latency: latency)
+
end
diff --git a/lib/user_destroyer.rb b/lib/user_destroyer.rb
index 8f27154a9..dcc2bb0bd 100644
--- a/lib/user_destroyer.rb
+++ b/lib/user_destroyer.rb
@@ -42,6 +42,19 @@ class UserDestroyer
b.record_match! if b
end
Post.with_deleted.where(user_id: user.id).update_all("user_id = NULL")
+
+ # If this user created categories, fix those up:
+ categories = Category.where(user_id: user.id).all
+ categories.each do |c|
+ c.user_id = Discourse.system_user.id
+ c.save!
+ if topic = Topic.with_deleted.where(id: c.topic_id).first
+ topic.try(:recover!)
+ topic.user_id = Discourse.system_user.id
+ topic.save!
+ end
+ end
+
StaffActionLogger.new(@staff).log_user_deletion(user, opts.slice(:context))
DiscourseHub.unregister_nickname(user.username) if SiteSetting.call_discourse_hub?
MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id]
diff --git a/spec/components/user_destroyer_spec.rb b/spec/components/user_destroyer_spec.rb
index bb5a48986..a04629579 100644
--- a/spec/components/user_destroyer_spec.rb
+++ b/spec/components/user_destroyer_spec.rb
@@ -238,6 +238,17 @@ describe UserDestroyer do
UserDestroyer.new(@admin).destroy(@user, {block_ip: true})
end
end
+
+ context 'user created a category' do
+ let!(:category) { Fabricate(:category, user: @user) }
+
+ it "assigns the system user to the categories" do
+ UserDestroyer.new(@admin).destroy(@user, {delete_posts: true})
+ category.reload.user_id.should == Discourse.system_user.id
+ category.topic.should be_present
+ category.topic.user_id.should == Discourse.system_user.id
+ end
+ end
end
end
diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb
index aac9a5228..26c24722b 100644
--- a/spec/serializers/user_serializer_spec.rb
+++ b/spec/serializers/user_serializer_spec.rb
@@ -28,7 +28,6 @@ describe UserSerializer do
end
it "has a name" do
- puts json[:name]
json[:name].should be_blank
end
end
diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js
index 469c77059..6a0dd2487 100644
--- a/vendor/assets/javascripts/run-qunit.js
+++ b/vendor/assets/javascripts/run-qunit.js
@@ -8,27 +8,21 @@ if (args.length < 1 || args.length > 2) {
phantom.exit(1);
}
-var fs = require('fs');
-function print(str) {
- fs.write('/dev/stdout', str, 'w');
-}
-
-var page = require('webpage').create();
+var system = require("system"),
+ page = require('webpage').create();
page.onConsoleMessage = function(msg) {
if (msg.slice(0,8) === 'WARNING:') { return; }
if (msg.slice(0,6) === 'DEBUG:') { return; }
- // Hack to access the print method
- // If there's a better way to do this, please change
- if (msg.slice(0,6) === 'PRINT:') {
- print(msg.slice(7));
- return;
- }
-
console.log(msg);
};
+page.onCallback = function (message) {
+ // forward the message to the standard output
+ system.stdout.write(message);
+};
+
page.open(args[0], function(status) {
if (status !== 'success') {
console.error("Unable to access network");
@@ -80,9 +74,9 @@ function logQUnit() {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
testErrors.push(msg);
assertionErrors = [];
- console.log('PRINT: F');
+ window.callPhantom('F');
} else {
- console.log('PRINT: .');
+ window.callPhantom('.');
}
});