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

{{breakUp username}}

+ + {{#if showName}} +

{{breakUp name}}

+ {{/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('.'); } });