mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
merging local with master
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
70fd4f7f6b
62 changed files with 1081 additions and 737 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
</label>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{textField value=newFilter}}
|
||||
<button class="btn btn-primary" {{action changeFilter}}>{{i18n filter}}</button>
|
||||
{{textField value=filter placeholderKey="type_to_filter"}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -248,4 +248,3 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
|||
});
|
||||
|
||||
Discourse.Router = Discourse.Router.reopen({ location: 'discourse_location' });
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
<div {{bindAttr class=":contents byTopicCreator:topic-creator :trigger-expansion"}}>
|
||||
<a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{avatar this imageSize="large"}}</a>
|
||||
<h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{breakUp username}}</a></h3>
|
||||
|
||||
{{#if showName}}
|
||||
<h3><a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{breakUp name}}</a></h3>
|
||||
{{/if}}
|
||||
|
||||
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -8,5 +8,3 @@ window.ENV = {
|
|||
|
||||
window.Discourse = {};
|
||||
Discourse.SiteSettings = {};
|
||||
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
//
|
||||
// --------------------------------------------------
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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]; };
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';
|
||||
Discourse.BaseUrl = '<%= RailsMultisite::ConnectionManagement.current_hostname %>';
|
||||
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';
|
||||
Discourse.BaseUrl = '<%= RailsMultisite::ConnectionManagement.current_hostname %>';
|
||||
Discourse.BaseUri = '<%= Discourse::base_uri "/" %>';
|
||||
Discourse.Environment = '<%= Rails.env %>';
|
||||
Discourse.SiteSettings = PreloadStore.get('siteSettings');
|
||||
Discourse.Router.map(function() {
|
||||
Discourse.routeBuilder.call(this);
|
||||
});
|
||||
Discourse.Router.map(function() { Discourse.routeBuilder.call(this); });
|
||||
Discourse.start()
|
||||
</script>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
<link rel="icon" type="image/png" href="<%=SiteSetting.favicon_url%>">
|
||||
<link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>">
|
||||
|
||||
<%= javascript_include_tag "preload_store" %>
|
||||
|
||||
<%= javascript_include_tag "locales/#{I18n.locale}" %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 관리자'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -969,6 +969,7 @@ pseudo:
|
|||
ƀřóŵšéř íš ťóó ółď ťó ŵóřǩ óɳ ťĥíš Ďíščóůřšé ƒóřůɱ</á>. Рłéášé <á ĥřéƒ="ĥťťƿ://ƀřóŵšéĥáƿƿý.čóɱ">ůƿǧřáďé
|
||||
ýóůř ƀřóŵšéř</á>. ]]'
|
||||
admin_js:
|
||||
type_to_filter: '[[ ťýƿé ťó ƒíłťéř... ]]'
|
||||
admin:
|
||||
title: '[[ Ďíščóůřšé Áďɱíɳ ]]'
|
||||
moderator: '[[ Ϻóďéřáťóř ]]'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1085,6 +1085,7 @@ ru:
|
|||
create_post: 'Отвечать / Просматривать'
|
||||
readonly: Просматривать
|
||||
admin_js:
|
||||
type_to_filter: 'Введите текст для фильтрации...'
|
||||
admin:
|
||||
title: 'Discourse Admin'
|
||||
moderator: Модератор
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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: "选择内容类型以开始编辑。"
|
||||
|
|
|
@ -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: '論道 管理'
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
241
lib/autospec/manager.rb
Normal file
241
lib/autospec/manager.rb
Normal file
|
@ -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
|
150
lib/autospec/qunit_runner.rb
Normal file
150
lib/autospec/qunit_runner.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
43
lib/autospec/rspec_runner.rb
Normal file
43
lib/autospec/rspec_runner.rb
Normal file
|
@ -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
|
175
lib/autospec/run-qunit.js
Normal file
175
lib/autospec/run-qunit.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC
|
||||
|
||||
if (phantom.args.length != 1) {
|
||||
console.log("Usage: " + phantom.scriptName + " <URL>");
|
||||
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;
|
||||
});
|
||||
|
||||
};
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
142
lib/demon/base.rb
Normal file
142
lib/demon/base.rb
Normal file
|
@ -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
|
25
lib/demon/rails_autospec.rb
Normal file
25
lib/demon/rails_autospec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
20
lib/tasks/assets.rake
Normal file
20
lib/tasks/assets.rake
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,7 +28,6 @@ describe UserSerializer do
|
|||
end
|
||||
|
||||
it "has a name" do
|
||||
puts json[:name]
|
||||
json[:name].should be_blank
|
||||
end
|
||||
end
|
||||
|
|
24
vendor/assets/javascripts/run-qunit.js
vendored
24
vendor/assets/javascripts/run-qunit.js
vendored
|
@ -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('.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue