diff --git a/README.md b/README.md index d51a68981f..77a16ffff4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,17 @@ Whenever you need ... If you're interested in helping us develop Discourse, please start with our **[Discourse Developer Install Guide](https://github.com/discourse/discourse/blob/master/DEVELOPMENT.md)**, which includes instructions to get up and running in a development environment. -We also have a **[Discourse "Quick-and-Dirty" Install Guide](https://github.com/discourse/discourse/blob/master/INSTALL.md)**. +### The quick and easy setup + +``` +git clone git@github.com:discourse/discourse.git +cd discourse +rake db:create +rake db:migrate +rake db:seed_fu +redis-cli flushall +thin start +``` ## Vision diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee index c1f0e781b8..99f8e3439f 100644 --- a/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee @@ -7,6 +7,13 @@ window.Discourse.AdminFlagsController = Ember.Controller.extend bootbox.alert("something went wrong") ) + deletePost: (item) -> + item.deletePost().then (=> + @content.removeObject(item) + ), (-> + bootbox.alert("something went wrong") + ) + adminOldFlagsView: (-> @query == 'old' ).property('query') diff --git a/app/assets/javascripts/admin/models/flagged_post.js.coffee b/app/assets/javascripts/admin/models/flagged_post.js.coffee index 6d3f0fa0bc..cc35232c7b 100644 --- a/app/assets/javascripts/admin/models/flagged_post.js.coffee +++ b/app/assets/javascripts/admin/models/flagged_post.js.coffee @@ -28,6 +28,25 @@ window.Discourse.FlaggedPost = Discourse.Post.extend @get('topic_visible') == 'f' ).property('topic_hidden') + deletePost: -> + promise = new RSVP.Promise() + if @get('post_number') == "1" + $.ajax "/t/#{@topic_id}", + type: 'DELETE' + cache: false + success: -> + promise.resolve() + error: (e)-> + promise.reject() + else + $.ajax "/posts/#{@id}", + type: 'DELETE' + cache: false + success: -> + promise.resolve() + error: (e)-> + promise.reject() + clearFlags: -> promise = new RSVP.Promise() $.ajax "/admin/flags/clear/#{@id}", diff --git a/app/assets/javascripts/admin/models/site_customization.js.coffee b/app/assets/javascripts/admin/models/site_customization.js.coffee index 46a8622d11..fe8780d7e4 100644 --- a/app/assets/javascripts/admin/models/site_customization.js.coffee +++ b/app/assets/javascripts/admin/models/site_customization.js.coffee @@ -7,20 +7,20 @@ window.Discourse.SiteCustomization = Discourse.Model.extend trackedProperties: ['enabled','name', 'stylesheet', 'header', 'override_default_style'] description: (-> - "#{@.name}#{if @.enabled then ' (*)' else ''}" + "#{@name}#{if @enabled then ' (*)' else ''}" ).property('selected', 'name') changed: (-> - return false unless @.originals + return false unless @originals @trackedProperties.any (p)=> - @.originals[p] != @get(p) + @originals[p] != @get(p) ).property('override_default_style','enabled','name', 'stylesheet', 'header', 'originals') # TODO figure out how to call with apply startTrackingChanges: -> @set('originals',{}) @trackedProperties.each (p)=> - @.originals[p] = @get(p) + @originals[p] = @get(p) true previewUrl: (-> diff --git a/app/assets/javascripts/admin/templates/flags.js.handlebars b/app/assets/javascripts/admin/templates/flags.js.handlebars index cf567f3231..929076b5ce 100644 --- a/app/assets/javascripts/admin/templates/flags.js.handlebars +++ b/app/assets/javascripts/admin/templates/flags.js.handlebars @@ -21,14 +21,15 @@ {{#each content}} - {{avatar user imageSize="small"}} + {{avatar user imageSize="small"}} {{#if topicHidden}} {{/if}}

{{title}}


{{{excerpt}}} - {{#each flaggers}}{{avatar this imageSize="small"}}{{/each}} + {{#each flaggers}}{{avatar this imageSize="small"}}{{/each}} {{date lastFlagged}} {{#if controller.adminActiveFlagsView}} - + + {{/if}} diff --git a/app/assets/javascripts/discourse.js.coffee b/app/assets/javascripts/discourse.js.coffee index aad1f9cb47..4e167c1494 100644 --- a/app/assets/javascripts/discourse.js.coffee +++ b/app/assets/javascripts/discourse.js.coffee @@ -256,9 +256,9 @@ window.Discourse = Ember.Application.createWithMixins @rerender() else $('link').each -> - if @.href.match(me.name) and me.hash - $(@).data('orig', @.href) unless $(@).data('orig') - @.href = $(@).data('orig') + "&hash=" + me.hash + if @href.match(me.name) and me.hash + $(@).data('orig', @href) unless $(@).data('orig') + @href = $(@).data('orig') + "&hash=" + me.hash window.Discourse.Router = Discourse.Router.reopen(location: 'discourse_location') diff --git a/app/assets/javascripts/discourse/components/autocomplete.js.coffee b/app/assets/javascripts/discourse/components/autocomplete.js.coffee index 412e5eb445..26d89d441c 100644 --- a/app/assets/javascripts/discourse/components/autocomplete.js.coffee +++ b/app/assets/javascripts/discourse/components/autocomplete.js.coffee @@ -3,14 +3,14 @@ template = null $.fn.autocomplete = (options)-> - - return if @.length == 0 - - if options && options.cancel && @.data("closeAutocomplete") - @.data("closeAutocomplete")() + + return if @length == 0 + + if options && options.cancel && @data("closeAutocomplete") + @data("closeAutocomplete")() return this - alert "only supporting one matcher at the moment" unless @.length == 1 + alert "only supporting one matcher at the moment" unless @length == 1 autocompleteOptions = null selectedOption = null @@ -47,27 +47,27 @@ if isInput - width = @.width() - height = @.height() + width = @width() + height = @height() + + wrap = @wrap("
").parent() - wrap = @.wrap("
").parent() - wrap.width(width) - @.width(80) - @.attr('name', @.attr('name') + "-renamed") + @width(80) + @attr('name', @attr('name') + "-renamed") + + vals = @val().split(",") - vals = @.val().split(",") - vals.each (x)-> unless x == "" x = options.reverseTransform(x) if options.reverseTransform addInputSelectedItem(x) - @.val("") + @val("") completeStart = 0 wrap.click => - @.focus() + @focus() true diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee b/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee index 550f2cc0ad..ebb0f33f42 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee @@ -259,6 +259,9 @@ Discourse.TopicController = Ember.ObjectController.extend Discourse.Presence, post.toggleProperty('bookmarked') false + clearFlags: (actionType) -> + actionType.clearFlags() + # Who acted on a particular post / action type whoActed: (actionType) -> actionType.loadUsers() diff --git a/app/assets/javascripts/discourse/models/action_summary.js.coffee b/app/assets/javascripts/discourse/models/action_summary.js.coffee index 554b29bdca..43023e50b7 100644 --- a/app/assets/javascripts/discourse/models/action_summary.js.coffee +++ b/app/assets/javascripts/discourse/models/action_summary.js.coffee @@ -18,7 +18,7 @@ window.Discourse.ActionSummary = Em.Object.extend Discourse.Presence, @set('acted', false) @set('count', @get('count') - 1) @set('can_act', true) - @set('can_undo', false) + @set('can_undo', false) # Perform this action act: (opts) -> @@ -52,16 +52,28 @@ window.Discourse.ActionSummary = Em.Object.extend Discourse.Presence, @removeAction() # Remove our post action - jQuery.ajax + jQuery.ajax url: "/post_actions/#{@get('post.id')}" type: 'DELETE' data: - post_action_type_id: @get('id') + post_action_type_id: @get('id') + + clearFlags: -> + $.ajax + url: "/post_actions/clear_flags" + type: "POST" + data: + post_action_type_id: @get('id') + id: @get('post.id') + success: (result) => + @set('post.hidden', result.hidden) + @set('count', 0) + loadUsers: -> $.getJSON "/post_actions/users", id: @get('post.id'), post_action_type_id: @get('id') - (result) => + (result) => @set('users', Em.A()) result.each (u) => @get('users').pushObject(Discourse.User.create(u)) diff --git a/app/assets/javascripts/discourse/models/user.js.coffee b/app/assets/javascripts/discourse/models/user.js.coffee index 681ac2fc91..b90c1ea3aa 100644 --- a/app/assets/javascripts/discourse/models/user.js.coffee +++ b/app/assets/javascripts/discourse/models/user.js.coffee @@ -190,9 +190,9 @@ window.Discourse.User.reopenClass error: (xhr) -> promise.reject(xhr) promise - createAccount: (name, email, password, username) -> + createAccount: (name, email, password, username, passwordConfirm, challenge) -> $.ajax url: '/users' dataType: 'json' - data: {name: name, email: email, password: password, username: username} + data: {name: name, email: email, password: password, username: username, password_confirmation: passwordConfirm, challenge: challenge} type: 'POST' diff --git a/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars b/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars index 737db276fa..7173533803 100644 --- a/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars +++ b/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars @@ -49,6 +49,14 @@ {{/if}} + + + + {{view Ember.TextField valueBinding="view.accountPasswordConfirm" type="password" id="new-account-password-confirmation"}} + {{view Ember.TextField valueBinding="view.accountChallenge" id="new-account-challenge"}} + + +
diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars index a485931a86..69cebae745 100644 --- a/app/assets/javascripts/discourse/templates/post.js.handlebars +++ b/app/assets/javascripts/discourse/templates/post.js.handlebars @@ -28,8 +28,8 @@
- {{avatar this imageSize="large"}} -

{{breakUp username}}

+ {{avatar this imageSize="large"}} +

{{breakUp username}}

") click: (e) -> $target = $(e.target) + if actionTypeId = $target.data('clear-flags') + @get('controller').clearFlags(@content.findProperty('id', actionTypeId)) + return false + # User wants to know who actioned it if actionTypeId = $target.data('who-acted') @get('controller').whoActed(@content.findProperty('id', actionTypeId)) @@ -54,4 +62,4 @@ window.Discourse.ActionsHistoryView = Em.View.extend Discourse.Presence, @get('controller').undoAction(@content.findProperty('id', actionTypeId)) return false - false \ No newline at end of file + false diff --git a/app/assets/javascripts/discourse/views/composer_view.js.coffee b/app/assets/javascripts/discourse/views/composer_view.js.coffee index 611ffd2a04..8e7e0df825 100644 --- a/app/assets/javascripts/discourse/views/composer_view.js.coffee +++ b/app/assets/javascripts/discourse/views/composer_view.js.coffee @@ -192,7 +192,7 @@ window.Discourse.ComposerView = window.Discourse.View.extend done: (e, data) => @set('loadingImage', false) upload = data.result - html = "" + html = "" @addMarkdown(html) fail: (e, data) => diff --git a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee b/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee index 7f6d5098dd..a0f1769560 100644 --- a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee +++ b/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee @@ -3,6 +3,8 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco title: Em.String.i18n('create_account.title') uniqueUsernameValidation: null complete: false + accountPasswordConfirm: 0 + accountChallenge: 0 submitDisabled: (-> @@ -22,6 +24,8 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco # If blank, fail without a reason return Discourse.InputValidation.create(failed: true) if @blank('accountName') + @fetchConfirmationValue() if @get('accountPasswordConfirm') == 0 + # If too short return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.name.too_short')) if @get('accountName').length < 3 @@ -120,13 +124,22 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco ).property('accountPassword') + fetchConfirmationValue: -> + $.ajax + url: '/users/hp.json', + success: (json) => + @set('accountPasswordConfirm', json.value) + @set('accountChallenge', json.challenge.split("").reverse().join("")) + createAccount: -> name = @get('accountName') email = @get('accountEmail') password = @get('accountPassword') username = @get('accountUsername') + passwordConfirm = @get('accountPasswordConfirm') + challenge = @get('accountChallenge') - Discourse.User.createAccount(name, email, password, username).then (result) => + Discourse.User.createAccount(name, email, password, username, passwordConfirm, challenge).then (result) => if result.success @flash(result.message) diff --git a/app/assets/stylesheets/application/modal.css.scss b/app/assets/stylesheets/application/modal.css.scss index 2f62896620..7361893f99 100644 --- a/app/assets/stylesheets/application/modal.css.scss +++ b/app/assets/stylesheets/application/modal.css.scss @@ -152,6 +152,9 @@ margin-bottom: 20px; } } + .password-confirmation { + display: none; + } } #move-selected { diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss index 6c774072b1..fd43495604 100644 --- a/app/assets/stylesheets/application/topic-post.css.scss +++ b/app/assets/stylesheets/application/topic-post.css.scss @@ -320,6 +320,12 @@ font-size: 14px; line-height: 18px; } + + h3.moderator a { + background-color: #ffe; + border: 1px solid #ffd; + } + div { display: block; } diff --git a/app/assets/stylesheets/vendor/bootstrap.css.scss b/app/assets/stylesheets/vendor/bootstrap.css.scss index 813d5da2c0..b4e5717117 100644 --- a/app/assets/stylesheets/vendor/bootstrap.css.scss +++ b/app/assets/stylesheets/vendor/bootstrap.css.scss @@ -1418,7 +1418,13 @@ body { -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); } - :-moz-placeholder, :-ms-input-placeholder, ::-webkit-input-placeholder { + :-moz-placeholder { + color: #999999; + } + ::-webkit-input-placeholder { + color: #999999; + } + :-ms-input-placeholder { color: #999999; } .help-block, .help-inline { diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 2647029c99..4906327de4 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -3,7 +3,6 @@ require_dependency 'sql_builder' class Admin::FlagsController < Admin::AdminController def index - sql = SqlBuilder.new "select p.id, t.title, p.cooked, p.user_id, p.topic_id, p.post_number, p.hidden, t.visible topic_visible from posts p join topics t on t.id = topic_id @@ -72,7 +71,7 @@ from post_actions a sql.where('deleted_at is null') end - actions = sql.exec.each do |action| + sql.exec.each do |action| p = map[action["post_id"]] p[:post_actions] ||= [] p[:post_actions] << action @@ -92,7 +91,6 @@ where id in (?)" } render json: MultiJson.dump({users: users, posts: posts}) - end def clear diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ccdcf23673..18fb2898fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -135,14 +135,33 @@ class ApplicationController < ActionController::Base render json: MultiJson.dump(obj) end + def can_cache_content? + # Don't cache unless we're in production mode + return false unless Rails.env.production? + + # Don't cache logged in users + return false if current_user.present? + + # Don't cache if there's restricted access + return false if SiteSetting.restrict_access? + + true + end + + # Our custom cache method + def discourse_expires_in(time_length) + return unless can_cache_content? + expires_in time_length, public: true + end + # Helper method - if no logged in user (anonymous), use Rails' conditional GET # support. Should be very fast behind a cache. def anonymous_etag(*args) - if current_user.blank? and Rails.env.production? + if can_cache_content? yield if stale?(*args) # Add a one minute expiry - expires_in 1.minute, :public => true + expires_in 1.minute, public: true else yield end diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 005b3d14c0..5d58635024 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -58,8 +58,7 @@ class ListController < ApplicationController draft = Draft.get(current_user, list.draft_key, list.draft_sequence) if current_user list.draft = draft - # Add expiry of 1 minute for anonymous - expires_in 1.minute, :public => true if current_user.blank? + discourse_expires_in 1.minute respond_to do |format| format.html do diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index 9031a4d03e..f9d0e722e7 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -45,6 +45,21 @@ class PostActionsController < ApplicationController render nothing: true end + def clear_flags + requires_parameter(:post_action_type_id) + raise Discourse::InvalidAccess unless guardian.is_admin? + + PostAction.clear_flags!(@post, current_user.id, params[:post_action_type_id].to_i) + @post.reload + + if @post.is_flagged? + render json: {success: true, hidden: true} + else + @post.unhide! + render json: {success: true, hidden: false} + end + end + private def fetch_post_from_params diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 877470553c..82138519c5 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -123,6 +123,12 @@ class UsersController < ApplicationController end def create + + if params[:password_confirmation] != honeypot_value or params[:challenge] != challenge_value.try(:reverse) + # Don't give any indication that we caught you in the honeypot + return render(:json => {success: true, active: false, message: I18n.t("login.activate_email", email: params[:email]) }) + end + user = User.new user.name = params[:name] user.email = params[:email] @@ -183,6 +189,10 @@ class UsersController < ApplicationController render json: {errors: [I18n.t("mothership.access_token_problem")]} end + def get_honeypot_value + render json: {value: honeypot_value, challenge: challenge_value} + end + # all avatars are funneled through here def avatar @@ -320,6 +330,14 @@ class UsersController < ApplicationController private + def honeypot_value + Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{Discourse::Application.config.secret_token}")[0,15] + end + + def challenge_value + '3019774c067cc2b' + end + def fetch_user_from_params username_lower = params[:username].downcase username_lower.gsub!(/\.json$/, '') diff --git a/app/helpers/forum_helper.rb b/app/helpers/forum_helper.rb deleted file mode 100644 index cfbd978b92..0000000000 --- a/app/helpers/forum_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ForumHelper -end diff --git a/app/helpers/list_helper.rb b/app/helpers/list_helper.rb deleted file mode 100644 index eaa1d0e156..0000000000 --- a/app/helpers/list_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ListHelper -end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb deleted file mode 100644 index 7342393a70..0000000000 --- a/app/helpers/notifications_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module NotificationsHelper -end diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb deleted file mode 100644 index 721eba5f46..0000000000 --- a/app/helpers/user_notifications_helper.rb +++ /dev/null @@ -1,3 +0,0 @@ -module UserNotificationsHelper - -end diff --git a/app/models/post.rb b/app/models/post.rb index 1957cd5322..42d7852019 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -38,6 +38,7 @@ class Post < ActiveRecord::Base validates_presence_of :raw, :user_id, :topic_id validates :raw, length: {in: SiteSetting.min_post_length..SiteSetting.max_post_length} + validate :raw_quality validate :max_mention_validator validate :max_images_validator validate :max_links_validator @@ -68,6 +69,18 @@ class Post < ActiveRecord::Base self.raw.strip! if self.raw.present? end + def raw_quality + + sentinel = TextSentinel.new(self.raw, min_entropy: SiteSetting.body_min_entropy) + if sentinel.valid? + # It's possible the sentinel has cleaned up the title a bit + self.raw = sentinel.text + else + errors.add(:raw, I18n.t(:is_invalid)) unless sentinel.valid? + end + end + + # Stop us from posting the same thing too quickly def unique_post_validator return if SiteSetting.unique_posts_mins == 0 @@ -250,6 +263,17 @@ class Post < ActiveRecord::Base result end + def is_flagged? + post_actions.where('post_action_type_id in (?) and deleted_at is null', PostActionType.FlagTypes).count != 0 + end + + def unhide! + self.hidden = false + self.hidden_reason_id = nil + self.topic.update_attributes(visible: true) + self.save + end + # Update the body of a post. Will create a new version when appropriate def revise(updated_by, new_raw, opts={}) diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 5c62edd7da..91e5bc7c98 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -2,6 +2,8 @@ require_dependency 'rate_limiter' require_dependency 'system_message' class PostAction < ActiveRecord::Base + class AlreadyFlagged < StandardError; end + include RateLimiter::OnCreateRecord attr_accessible :deleted_at, :post_action_type_id, :post_id, :user_id, :post, :user, :post_action_type, :message @@ -45,10 +47,14 @@ class PostAction < ActiveRecord::Base user_actions end - def self.clear_flags!(post, moderator_id) + def self.clear_flags!(post, moderator_id, action_type_id = nil) # -1 is the automatic system cleary - actions = moderator_id == -1 ? PostActionType.AutoActionFlagTypes : PostActionType.FlagTypes + actions = if action_type_id + [action_type_id] + else + moderator_id == -1 ? PostActionType.AutoActionFlagTypes : PostActionType.FlagTypes + end PostAction.exec_sql('update post_actions set deleted_at = ?, deleted_by = ? where post_id = ? and deleted_at is null and post_action_type_id in (?)', @@ -115,6 +121,15 @@ class PostAction < ActiveRecord::Base end end + before_create do + if is_flag? + if PostAction.where('user_id = ? and post_id = ? and post_action_type_id in (?) and deleted_at is null', + self.user_id, self.post_id, PostActionType.FlagTypes).exists? + raise AlreadyFlagged + end + end + end + after_save do # Update denormalized counts diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb index 39b58a2a9f..a347c4fba3 100644 --- a/app/models/post_action_type.rb +++ b/app/models/post_action_type.rb @@ -1,4 +1,5 @@ class PostActionType < ActiveRecord::Base + attr_accessible :id, :is_flag, :name_key, :icon def self.ordered diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index f5091b5d66..e7b88bed51 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -126,6 +126,11 @@ class SiteSetting < ActiveRecord::Base setting(:basic_requires_read_posts, 100) setting(:basic_requires_time_spent_mins, 30) + # Entropy checks + setting(:title_min_entropy, 10) + setting(:body_min_entropy, 7) + setting(:max_word_length, 30) + def self.call_mothership? self.enforce_global_nicknames? and self.discourse_org_access_key.present? diff --git a/app/models/topic.rb b/app/models/topic.rb index 13a93a661a..6d6e162a78 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -2,6 +2,7 @@ require_dependency 'slug' require_dependency 'avatar_lookup' require_dependency 'topic_view' require_dependency 'rate_limiter' +require_dependency 'text_sentinel' class Topic < ActiveRecord::Base include RateLimiter::OnCreateRecord @@ -18,12 +19,14 @@ class Topic < ActiveRecord::Base rate_limit :limit_topics_per_day rate_limit :limit_private_messages_per_day + validate :title_quality validates_presence_of :title validates :title, length: {in: SiteSetting.min_topic_title_length..SiteSetting.max_topic_title_length} serialize :meta_data, ActiveRecord::Coders::Hstore validate :unique_title + belongs_to :category has_many :posts @@ -112,6 +115,23 @@ class Topic < ActiveRecord::Base errors.add(:title, I18n.t(:has_already_been_used)) if finder.exists? end + + def title_quality + # We don't care about quality on private messages + return if private_message? + + sentinel = TextSentinel.new(title, + min_entropy: SiteSetting.title_min_entropy, + max_word_length: SiteSetting.max_word_length, + remove_interior_spaces: true) + if sentinel.valid? + # It's possible the sentinel has cleaned up the title a bit + self.title = sentinel.text + else + errors.add(:title, I18n.t(:is_invalid)) unless sentinel.valid? + end + end + def new_version_required? return true if title_changed? diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 45d01eedb2..bf74deea47 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -37,6 +37,7 @@ class PostSerializer < ApplicationSerializer :bookmarked, :raw, :actions_summary, + :moderator?, :avatar_template, :user_id, :draft_sequence, @@ -45,6 +46,10 @@ class PostSerializer < ApplicationSerializer :deleted_at + def moderator? + object.user.has_trust_level?(:moderator) + end + def avatar_template object.user.avatar_template end @@ -140,6 +145,8 @@ class PostSerializer < ApplicationSerializer next if !action_summary[:can_act] && !scope.current_user + action_summary[:can_clear_flags] = scope.is_admin? && PostActionType.FlagTypes.include?(id) + if post_actions.present? and post_actions.has_key?(id) action_summary[:acted] = true action_summary[:can_undo] = scope.can_delete?(post_actions[id]) diff --git a/app/views/static/faq.html.erb b/app/views/static/faq.html.erb index 88b5722382..7c49b771cf 100644 --- a/app/views/static/faq.html.erb +++ b/app/views/static/faq.html.erb @@ -130,7 +130,7 @@

Terms of Service

- Yes, legalese is boring, but we must protect ourselves (and by extension, you and your data) against unfriendly folks. So, like everyone else, we have a Terms of Service TOS describing your (and our) behavior and rights related to content, privacy, and laws. To use this service, you must agree to abide by the TOS. + Yes, legalese is boring, but we must protect ourselves (and by extension, you and your data) against unfriendly folks. So, like everyone else, we have a Terms of Service TOS describing your (and our) behavior and rights related to content, privacy, and laws. To use this service, you must agree to abide by the TOS.

diff --git a/config/application.rb b/config/application.rb index 0f3b40e2ed..85723bf218 100644 --- a/config/application.rb +++ b/config/application.rb @@ -93,10 +93,6 @@ module Discourse # So open id logs somewhere sane config.after_initialize do OpenID::Util.logger = Rails.logger - - # latest possible so earliest in the stack - # require 'rack/message_bus' - # config.middleware.insert(0, Rack::MessageBus) end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4304928806..27c3ff8e3b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,6 +8,8 @@ en: too_many_links: "has too many links" just_posted_that: "is too similar to what you recently posted" has_already_been_used: "has already been used" + invalid_characters: "contains invalid characters" + is_invalid: "is invalid; try to be a little more descriptive" activerecord: attributes: @@ -301,6 +303,10 @@ en: email_time_window_mins: "How many minutes we wait before sending a user mail, to give them a chance to see it first." flush_timings_secs: "How frequently we flush timing data to the server, in seconds." + max_word_length: "The maximum word length in a topic title" + title_min_entropy: "The minimum entropy for a topic title" + body_min_entropy: "The minimum entropy for post body" + # This section is exported to the javascript for i18n in the admin section admin_js: type_to_filter: "Type to Filter..." @@ -313,6 +319,10 @@ en: title: "Flags" old: "Old" active: "Active" + clear: "Clear Flags" + clear_title: "dismiss all flags on this post (will unhide hidden posts)" + delete: "Delete Post" + delete_title: "delete post (if its the first post delete topic)" customize: title: "Customize" @@ -688,7 +698,7 @@ en: no_posted: "You haven't posted in any topics yet." no_popular: "There are no popular topics. That's sad." - topic: + topic: create_in: 'Create {{categoryName}} Topic' create: 'Create Topic' create_long: 'Create a new Topic' @@ -852,6 +862,9 @@ en: actions: flag: 'Flag' + clear_flags: + one: "Clear flag" + other: "Clear flags" it_too: "{{alsoName}} it too" undo: "Undo {{alsoName}}" by_you_and_others: diff --git a/config/routes.rb b/config/routes.rb index 3331be7f08..b0a8db0929 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,7 @@ Discourse::Application.routes.draw do put 'users/password-reset/:token' => 'users#password_reset' get 'users/activate-account/:token' => 'users#activate_account' get 'users/authorize-email/:token' => 'users#authorize_email' + get 'users/hp' => 'users#get_honeypot_value' get 'user_preferences' => 'users#user_preferences_redirect' get 'users/:username/private-messages' => 'user_actions#private_messages', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT} @@ -132,6 +133,7 @@ Discourse::Application.routes.draw do resources :post_actions do collection do get 'users' => 'post_actions#users' + post 'clear_flags' => 'post_actions#clear_flags' end end resources :user_actions diff --git a/db/fixtures/post_action_types.rb b/db/fixtures/post_action_types.rb index 863fc8c3c5..820b05f76f 100644 --- a/db/fixtures/post_action_types.rb +++ b/db/fixtures/post_action_types.rb @@ -27,6 +27,13 @@ PostActionType.seed do |s| s.position = 4 end +PostActionType.seed do |s| + s.id = PostActionType.Types[:vote] + s.name_key = 'vote' + s.is_flag = false + s.position = 5 +end + PostActionType.seed do |s| s.id = PostActionType.Types[:spam] s.name_key = 'spam' diff --git a/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb b/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb index 7f313de0f9..99d646ae78 100644 --- a/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb +++ b/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb @@ -1,6 +1,6 @@ class AddMetaDataToForumThreads < ActiveRecord::Migration def change - execute "CREATE EXTENSION hstore" + execute "CREATE EXTENSION IF NOT EXISTS hstore" add_column :forum_threads, :meta_data, :hstore end end diff --git a/lib/guardian.rb b/lib/guardian.rb index d5d406f65b..72f475a4ee 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -1,4 +1,4 @@ -# The guardian is responsible for confirming access to various site resources and opreations +# The guardian is responsible for confirming access to various site resources and operations class Guardian attr_reader :user diff --git a/lib/multisite_i18n.rb b/lib/multisite_i18n.rb new file mode 100644 index 0000000000..2d59c0c737 --- /dev/null +++ b/lib/multisite_i18n.rb @@ -0,0 +1,31 @@ +# Allow us to override i18n keys based on the current site you're viewing. +module MultisiteI18n + + class << self + + # It would be nice if there was an easier way to detect if a key is missing. + def translation_or_nil(key, opts) + missing_text = "missing multisite translation" + result = I18n.t(key, opts.merge(default: missing_text)) + return nil if result == missing_text + result + end + + def site_translate(current_site, key, opts=nil) + opts ||= {} + translation = MultisiteI18n.translation_or_nil("#{current_site || ""}.#{key}", opts) + if translation.blank? + return I18n.t(key, opts) + else + return translation + end + end + + def t(*args) + MultisiteI18n.site_translate(RailsMultisite::ConnectionManagement.current_db, *args) + end + + alias :translate :t + end + +end diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 8a1bd85999..89e49d9aeb 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -31,8 +31,8 @@ module SiteSettingExtension end # just like a setting, except that it is available in javascript via DiscourseSession - def client_setting(name, defualt = nil, type = nil) - setting(name,defualt,type) + def client_setting(name, default = nil, type = nil) + setting(name,default,type) @@client_settings ||= [] @@client_settings << name end diff --git a/lib/system_message.rb b/lib/system_message.rb index 92aaf39903..ed54356f51 100644 --- a/lib/system_message.rb +++ b/lib/system_message.rb @@ -1,5 +1,6 @@ # Handle sending a message to a user from the system. require_dependency 'post_creator' +require_dependency 'multisite_i18n' class SystemMessage @@ -14,20 +15,20 @@ class SystemMessage def create(type, params = {}) defaults = {site_name: SiteSetting.title, - username: @recipient.username, - user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences", - new_user_tips: I18n.t("system_messages.usage_tips.text_body_template"), - site_password: "", - base_url: Discourse.base_url} + username: @recipient.username, + user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences", + new_user_tips: MultisiteI18n.t("system_messages.usage_tips.text_body_template"), + site_password: "", + base_url: Discourse.base_url} params = defaults.merge(params) if SiteSetting.restrict_access? - params[:site_password] = I18n.t('system_messages.site_password', access_password: SiteSetting.access_password) + params[:site_password] = MultisiteI18n.t('system_messages.site_password', access_password: SiteSetting.access_password) end - title = I18n.t("system_messages.#{type}.subject_template", params) - raw_body = I18n.t("system_messages.#{type}.text_body_template", params) + title = MultisiteI18n.t("system_messages.#{type}.subject_template", params) + raw_body = MultisiteI18n.t("system_messages.#{type}.text_body_template", params) PostCreator.create(SystemMessage.system_user, raw: raw_body, diff --git a/lib/text_sentinel.rb b/lib/text_sentinel.rb new file mode 100644 index 0000000000..3af20efd0f --- /dev/null +++ b/lib/text_sentinel.rb @@ -0,0 +1,56 @@ +require 'iconv' + +# +# Given a string, tell us whether or not is acceptable. Also, remove stuff we don't like +# such as leading / trailing space. +# +class TextSentinel + + attr_accessor :text + + def self.non_symbols_regexp + /[\ -\/\[-\`\:-\@\{-\~]/m + end + + def initialize(text, opts=nil) + if text.present? + @text = Iconv.new('UTF-8//IGNORE', 'UTF-8').iconv(text.dup) + end + + @opts = opts || {} + + if @text.present? + @text.strip! + @text.gsub!(/ +/m, ' ') if @opts[:remove_interior_spaces] + end + end + + # Entropy is a number of how many unique characters the string needs. + def entropy + return 0 if @text.blank? + @entropy ||= @text.each_char.to_a.uniq.size + end + + def valid? + + # Blank strings are not valid + return false if @text.blank? + + # Entropy check if required + return false if @opts[:min_entropy].present? and (entropy < @opts[:min_entropy]) + + # We don't have a comprehensive list of symbols, but this will eliminate some noise + non_symbols = @text.gsub(TextSentinel.non_symbols_regexp, '').size + return false if non_symbols == 0 + + # Don't allow super long strings without spaces + + return false if @opts[:max_word_length] and @text =~ /\w{#{@opts[:max_word_length]},}(\s|$)/ + + # We don't allow all upper case content + return false if @text == @text.upcase + + true + end + +end diff --git a/spec/components/multisite_i18n_spec.rb b/spec/components/multisite_i18n_spec.rb new file mode 100644 index 0000000000..abdedac99d --- /dev/null +++ b/spec/components/multisite_i18n_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require_dependency 'multisite_i18n' + +describe MultisiteI18n do + + before do + I18n.stubs(:t).with('test', {}).returns('default i18n') + MultisiteI18n.stubs(:translation_or_nil).with("default.test", {}).returns(nil) + MultisiteI18n.stubs(:translation_or_nil).with("other_site.test", {}).returns("overwritten i18n") + end + + context "no value for a multisite key" do + it "it returns the default i18n key" do + MultisiteI18n.site_translate('default', 'test').should == "default i18n" + end + end + + context "with a value for the multisite key" do + it "returns the overwritten value" do + MultisiteI18n.site_translate('other_site', 'test').should == "overwritten i18n" + end + end + + context "when we call t, it uses the current site" do + + it "returns the original" do + MultisiteI18n.t('test').should == 'default i18n' + end + + it "returns the overwritten" do + RailsMultisite::ConnectionManagement.stubs(:current_db).returns('other_site') + MultisiteI18n.t('test').should == "overwritten i18n" + end + + end + +end \ No newline at end of file diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 2096062108..5c5b074ecd 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -11,7 +11,7 @@ describe PostCreator do context 'new topic' do let(:category) { Fabricate(:category, user: user) } - let(:basic_topic_params) { {title: 'hello world', raw: 'my name is fred', archetype_id: 1} } + let(:basic_topic_params) { {title: 'hello world topic', raw: 'my name is fred', archetype_id: 1} } let(:image_sizes) { {'http://an.image.host/image.jpg' => {'width' => 111, 'height' => 222}} } let(:creator) { PostCreator.new(user, basic_topic_params) } @@ -83,7 +83,7 @@ describe PostCreator do let(:target_user1) { Fabricate(:coding_horror) } let(:target_user2) { Fabricate(:moderator) } let(:post) do - PostCreator.create(user, title: 'hi there', + PostCreator.create(user, title: 'hi there welcome to my topic', raw: 'this is my awesome message', archetype: Archetype.private_message, target_usernames: [target_user1.username, target_user2.username].join(',')) diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index c2699d9897..717f63b7cf 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -14,7 +14,7 @@ describe Search do context 'post indexing observer' do before do @category = Fabricate(:category, name: 'america') - @topic = Fabricate(:topic, title: 'sam test', category: @category) + @topic = Fabricate(:topic, title: 'sam test topic', category: @category) @post = Fabricate(:post, topic: @topic, raw: 'this fun test ') @indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"] end @@ -29,7 +29,7 @@ describe Search do end it "should pick up on title updates" do - @topic.title = "harpi" + @topic.title = "harpi is the new title" @topic.save! @indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"] diff --git a/spec/components/text_sentinel_spec.rb b/spec/components/text_sentinel_spec.rb new file mode 100644 index 0000000000..7467395145 --- /dev/null +++ b/spec/components/text_sentinel_spec.rb @@ -0,0 +1,96 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'text_sentinel' +require 'iconv' + +describe TextSentinel do + + + context "entropy" do + + + it "returns 0 for an empty string" do + TextSentinel.new("").entropy.should == 0 + end + + it "returns 0 for a nil string" do + TextSentinel.new(nil).entropy.should == 0 + end + + it "returns 1 for a string with many leading spaces" do + TextSentinel.new((" " * 10) + "x").entropy.should == 1 + end + + it "returns 1 for one char, even repeated" do + TextSentinel.new("a" * 10).entropy.should == 1 + end + + it "returns an accurate count of many chars" do + TextSentinel.new("evil trout is evil").entropy.should == 10 + end + + end + + context "cleaning up" do + + it "strips leading or trailing whitespace" do + TextSentinel.new(" \t test \t ").text.should == "test" + end + + it "allows utf-8 chars" do + TextSentinel.new("йȝîûηыეமிᚉ⠛").text.should == "йȝîûηыეமிᚉ⠛" + end + + context "interior spaces" do + + let(:spacey_string) { "hello there's weird spaces here." } + + it "ignores intra spaces by default" do + TextSentinel.new(spacey_string).text.should == spacey_string + end + + it "fixes intra spaces when enabled" do + TextSentinel.new(spacey_string, remove_interior_spaces: true).text.should == "hello there's weird spaces here." + end + + end + + end + + context "validity" do + + let(:valid_string) { "This is a cool topic about Discourse" } + + it "allows a valid string" do + TextSentinel.new(valid_string).should be_valid + end + + it "doesn't allow all caps topics" do + TextSentinel.new(valid_string.upcase).should_not be_valid + end + + it "enforces the minimum entropy" do + TextSentinel.new(valid_string, min_entropy: 16).should be_valid + end + + it "enforces the minimum entropy" do + TextSentinel.new(valid_string, min_entropy: 17).should_not be_valid + end + + it "doesn't allow a long alphanumeric string with no spaces" do + TextSentinel.new("jfewjfoejwfojeojfoejofjeo38493824jfkjewfjeoifijeoijfoejofjeojfoewjfo834988394032jfiejoijofijeojfeojfojeofjewojfojeofjeowjfojeofjeojfoe3898439849032jfeijfwoijfoiewj", + max_word_length: 30).should_not be_valid + end + + it "doesn't except junk symbols as a string" do + TextSentinel.new("[[[").should_not be_valid + TextSentinel.new("<<<").should_not be_valid + TextSentinel.new("{{$!").should_not be_valid + end + + + end + + +end \ No newline at end of file diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index e0494ebdb5..be06ed5052 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -11,11 +11,11 @@ describe TopicQuery do let(:admin) { Fabricate(:moderator) } context 'a bunch of topics' do - let!(:regular_topic) { Fabricate(:topic, title: 'regular', user: creator, bumped_at: 15.minutes.ago) } - let!(:pinned_topic) { Fabricate(:topic, title: 'pinned', user: creator, pinned: true, bumped_at: 10.minutes.ago) } - let!(:archived_topic) { Fabricate(:topic, title: 'archived', user: creator, archived: true, bumped_at: 6.minutes.ago) } - let!(:invisible_topic) { Fabricate(:topic, title: 'invisible', user: creator, visible: false, bumped_at: 5.minutes.ago) } - let!(:closed_topic) { Fabricate(:topic, title: 'closed', user: creator, closed: true, bumped_at: 1.minute.ago) } + let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) } + let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned: true, bumped_at: 10.minutes.ago) } + let!(:archived_topic) { Fabricate(:topic, title: 'this is an archived topic', user: creator, archived: true, bumped_at: 6.minutes.ago) } + let!(:invisible_topic) { Fabricate(:topic, title: 'this is an invisible topic', user: creator, visible: false, bumped_at: 5.minutes.ago) } + let!(:closed_topic) { Fabricate(:topic, title: 'this is a closed topic', user: creator, closed: true, bumped_at: 1.minute.ago) } context 'list_popular' do let(:topics) { topic_query.list_popular.topics } diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 961713e647..f6bac266fd 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -334,9 +334,9 @@ describe TopicsController do end it 'allows a change of title' do - xhr :put, :update, topic_id: @topic.id, slug: @topic.title, title: 'new title' + xhr :put, :update, topic_id: @topic.id, slug: @topic.title, title: 'this is a new title for the topic' @topic.reload - @topic.title.should == 'new title' + @topic.title.should == 'this is a new title for the topic' end it 'triggers a change of category' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index ac076be5ef..b67f6f7af2 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -2,6 +2,11 @@ require 'spec_helper' describe UsersController do + before do + UsersController.any_instance.stubs(:honeypot_value).returns(nil) + UsersController.any_instance.stubs(:challenge_value).returns(nil) + end + describe '.show' do let!(:user) { log_in } @@ -339,7 +344,41 @@ describe UsersController do User.where(username: @user.username).first.active.should be_false end end - + + shared_examples_for 'honeypot fails' do + it 'should not create a new user' do + expect { + xhr :post, :create, create_params + }.to_not change { User.count } + end + + it 'should not send an email' do + User.any_instance.expects(:enqueue_welcome_message).never + xhr :post, :create, create_params + end + + it 'should say it was successful' do + xhr :post, :create, create_params + json = JSON::parse(response.body) + json["success"].should be_true + end + end + + context 'when honeypot value is wrong' do + before do + UsersController.any_instance.stubs(:honeypot_value).returns('abc') + end + let(:create_params) { {:name => @user.name, :username => @user.username, :password => "strongpassword", :email => @user.email, :password_confirmation => 'wrong'} } + it_should_behave_like 'honeypot fails' + end + + context 'when challenge answer is wrong' do + before do + UsersController.any_instance.stubs(:challenge_value).returns('abc') + end + let(:create_params) { {:name => @user.name, :username => @user.username, :password => "strongpassword", :email => @user.email, :challenge => 'abc'} } + it_should_behave_like 'honeypot fails' + end end context '.username' do diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 10c6399000..6eb7827728 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -111,6 +111,13 @@ describe PostAction do describe 'flagging' do + it 'does not allow you to flag stuff with 2 reasons' do + post = Fabricate(:post) + u1 = Fabricate(:evil_trout) + PostAction.act(u1, post, PostActionType.Types[:spam]) + lambda { PostAction.act(u1, post, PostActionType.Types[:off_topic]) }.should raise_error(PostAction::AlreadyFlagged) + end + it 'should update counts when you clear flags' do post = Fabricate(:post) u1 = Fabricate(:evil_trout) diff --git a/spec/models/post_alert_observer_spec.rb b/spec/models/post_alert_observer_spec.rb index a1c9de1ef2..7b6ad2cf35 100644 --- a/spec/models/post_alert_observer_spec.rb +++ b/spec/models/post_alert_observer_spec.rb @@ -30,7 +30,7 @@ describe PostAlertObserver do context 'when editing a post' do it 'notifies a user of the revision' do lambda { - post.revise(evil_trout, "world") + post.revise(evil_trout, "world is the new body of the message") }.should change(post.user.notifications, :count).by(1) end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 682200f1a3..ec698f02e0 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -54,12 +54,24 @@ describe Post do topic.user.trust_level = TrustLevel.Levels[:moderator] Fabricate.build(:post, post_args).should be_valid end - - end end + describe 'flagging helpers' do + it 'isFlagged is accurate' do + post = Fabricate(:post) + user = Fabricate(:coding_horror) + PostAction.act(user, post, PostActionType.Types[:off_topic]) + + post.reload + post.is_flagged?.should == true + + PostAction.remove_act(user, post, PostActionType.Types[:off_topic]) + post.reload + post.is_flagged?.should == false + end + end describe 'message bus' do it 'enqueues the post on the message bus' do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index a65d0350d6..af9e9461b4 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1,11 +1,10 @@ +# encoding: UTF-8 + require 'spec_helper' describe Topic do it { should validate_presence_of :title } - it { should_not allow_value("x" * (SiteSetting.max_topic_title_length + 1)).for(:title) } - it { should_not allow_value("x").for(:title) } - it { should_not allow_value((" " * SiteSetting.min_topic_title_length) + "x").for(:title) } it { should belong_to :category } it { should belong_to :user } @@ -24,6 +23,30 @@ describe Topic do it { should rate_limit } + context '.title_quality' do + + it "strips a title when identifying length" do + Fabricate.build(:topic, title: (" " * SiteSetting.min_topic_title_length) + "x").should_not be_valid + end + + it "doesn't allow a long title" do + Fabricate.build(:topic, title: "x" * (SiteSetting.max_topic_title_length + 1)).should_not be_valid + end + + it "doesn't allow a short title" do + Fabricate.build(:topic, title: "x" * (SiteSetting.min_topic_title_length + 1)).should_not be_valid + end + + it "allows a regular title with a few ascii characters" do + Fabricate.build(:topic, title: "hello this is my cool topic! welcome: all;").should be_valid + end + + it "allows non ascii" do + Fabricate.build(:topic, title: "Iñtërnâtiônàlizætiøn").should be_valid + end + + end + context 'topic title uniqueness' do @@ -816,7 +839,7 @@ describe Topic do context 'changing title' do before do - topic.title = "new title" + topic.title = "new title for the topic" topic.save end