mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 18:03:38 +08:00
Merge remote-tracking branch 'discourse/master'
This commit is contained in:
commit
d5a2518fef
12
README.md
12
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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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: (->
|
||||
|
|
|
@ -21,14 +21,15 @@
|
|||
<tbody>
|
||||
{{#each content}}
|
||||
<tr {{bindAttr class="hiddenClass"}}>
|
||||
<td class='user'>{{avatar user imageSize="small"}}</td>
|
||||
<td class='user'><a href="/admin{{unbound user.path}}">{{avatar user imageSize="small"}}</a></td>
|
||||
<td class='excerpt'>{{#if topicHidden}}<i title='this topic is invisible' class='icon icon-eye-close'></i> {{/if}}<h3><a href='{{unbound url}}'>{{title}}</a></h3><br>{{{excerpt}}}
|
||||
</td>
|
||||
<td class='flaggers'>{{#each flaggers}}{{avatar this imageSize="small"}}{{/each}}</td>
|
||||
<td class='flaggers'>{{#each flaggers}}<a href="/admin{{unbound path}}">{{avatar this imageSize="small"}}</a>{{/each}}</td>
|
||||
<td class='last-flagged'>{{date lastFlagged}}</td>
|
||||
<td class='action'>
|
||||
{{#if controller.adminActiveFlagsView}}
|
||||
<button title='dismiss all flags on this post (will unhide hidden posts)' class='btn' {{action clearFlags this}}>Clear Flags</button>
|
||||
<button title='{{i18n admin.flags.clear_title}}' class='btn' {{action clearFlags this}}>{{i18n admin.flags.clear}}</button>
|
||||
<button title='{{i18n admin.flags.delete_title}}' class='btn' {{action deletePost this}}>{{i18n admin.flags.delete}}</button>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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("<div class='ac-wrap clearfix'/>").parent()
|
||||
|
||||
wrap = @.wrap("<div class='ac-wrap clearfix'/>").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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -49,6 +49,14 @@
|
|||
</tr>
|
||||
{{/if}}
|
||||
|
||||
<tr class="password-confirmation">
|
||||
<td><label for='new-account-password-confirmation'>Password Again</label></td>
|
||||
<td>
|
||||
{{view Ember.TextField valueBinding="view.accountPasswordConfirm" type="password" id="new-account-password-confirmation"}}
|
||||
{{view Ember.TextField valueBinding="view.accountChallenge" id="new-account-challenge"}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -28,8 +28,8 @@
|
|||
|
||||
<div class='topic-meta-data span2'>
|
||||
<div class='contents'>
|
||||
<a href='/users/{{unbound username}}' class='excerptable' data-excerpt-position="right" data-excerpt-size="small">{{avatar this imageSize="large"}}</a>
|
||||
<h3><a href='/users/{{unbound username}}'>{{breakUp username}}</a></h3>
|
||||
<a href='/users/{{unbound username}}' class='excerptable' data-excerpt-position="right" data-excerpt-size="small" >{{avatar this imageSize="large"}}</a>
|
||||
<h3 {{bindAttr class="moderator"}}><a href='/users/{{unbound username}}'>{{breakUp username}}</a></h3>
|
||||
|
||||
<div class='post-info'>
|
||||
<a href='#' class='post-date' {{bindAttr data-share-url="url"}}>{{date created_at}}</a>
|
||||
|
|
|
@ -35,12 +35,20 @@ window.Discourse.ActionsHistoryView = Em.View.extend Discourse.Presence,
|
|||
|
||||
if c.get('can_undo')
|
||||
alsoName = Em.String.i18n("post.actions.undo", alsoName: c.get('actionType.alsoNameLower'))
|
||||
buffer.push(" <a href='#' data-undo='#{c.get('id')}'>#{alsoName}</a>.")
|
||||
buffer.push(" <a href='#' data-undo='#{c.get('id')}'>#{alsoName}</a>.")
|
||||
|
||||
if c.get('can_clear_flags')
|
||||
buffer.push(" <a href='#' data-clear-flags='#{c.get('id')}'>#{Em.String.i18n("post.actions.clear_flags",count: c.count)}</a>.")
|
||||
|
||||
buffer.push("</div>")
|
||||
|
||||
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
|
||||
false
|
||||
|
|
|
@ -192,7 +192,7 @@ window.Discourse.ComposerView = window.Discourse.View.extend
|
|||
done: (e, data) =>
|
||||
@set('loadingImage', false)
|
||||
upload = data.result
|
||||
html = "<img src='#{upload.url}' width='#{upload.width}' height='#{upload.height}'>"
|
||||
html = "<img src=\"#{upload.url}\" width=\"#{upload.width}\" height=\"#{upload.height}\">"
|
||||
@addMarkdown(html)
|
||||
|
||||
fail: (e, data) =>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -152,6 +152,9 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.password-confirmation {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#move-selected {
|
||||
|
|
|
@ -320,6 +320,12 @@
|
|||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
h3.moderator a {
|
||||
background-color: #ffe;
|
||||
border: 1px solid #ffd;
|
||||
}
|
||||
|
||||
div {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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$/, '')
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
module ForumHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module ListHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module NotificationsHelper
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
module UserNotificationsHelper
|
||||
|
||||
end
|
|
@ -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={})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class PostActionType < ActiveRecord::Base
|
||||
|
||||
attr_accessible :id, :is_flag, :name_key, :icon
|
||||
|
||||
def self.ordered
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
<article style="border: none" id="terms">
|
||||
<h1>Terms of Service</h1>
|
||||
<p>
|
||||
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 <a href="http://example.com/tos">TOS</a> 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 <a href="/tos">TOS</a> 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.
|
||||
</p>
|
||||
<div class="more">
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
31
lib/multisite_i18n.rb
Normal file
31
lib/multisite_i18n.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
56
lib/text_sentinel.rb
Normal file
56
lib/text_sentinel.rb
Normal file
|
@ -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
|
37
spec/components/multisite_i18n_spec.rb
Normal file
37
spec/components/multisite_i18n_spec.rb
Normal file
|
@ -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
|
|
@ -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(','))
|
||||
|
|
|
@ -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 <b>fun test</b> <img src="bla" title="my image">')
|
||||
@indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"]
|
||||
end
|
||||
|
@ -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"]
|
||||
|
||||
|
|
96
spec/components/text_sentinel_spec.rb
Normal file
96
spec/components/text_sentinel_spec.rb
Normal file
|
@ -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
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user