mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
- FEATURE: revamped poll plugin
- add User.staff scope - inject MessageBus into Ember views (so it can be used by the poll plugin) - REFACTOR: use more accurate is_first_post? method instead of post_number == 1 - FEATURE: add support for JSON-typed custom fields - FEATURE: allow plugins to add validation - FEATURE: add post_custom_fields to PostSerializer - FEATURE: allow plugins to whitelist post_custom_fields - FIX: don't bump when post did not save successfully - FEATURE: polls are supported in any post - FEATURE: allow for multiple polls in the same post - FEATURE: multiple choice polls - FEATURE: rating polls - FEATURE: new dialect allowing users to preview polls in the composer
This commit is contained in:
parent
17dc8b8e4f
commit
a737090442
89 changed files with 1334 additions and 1569 deletions
|
@ -42,7 +42,7 @@ export default {
|
||||||
inject(app, 'currentUser', 'component', 'route', 'controller');
|
inject(app, 'currentUser', 'component', 'route', 'controller');
|
||||||
|
|
||||||
app.register('message-bus:main', window.MessageBus, { instantiate: false });
|
app.register('message-bus:main', window.MessageBus, { instantiate: false });
|
||||||
inject(app, 'messageBus', 'route', 'controller');
|
inject(app, 'messageBus', 'route', 'controller', 'view');
|
||||||
|
|
||||||
app.register('store:main', Store);
|
app.register('store:main', Store);
|
||||||
inject(app, 'store', 'route', 'controller');
|
inject(app, 'store', 'route', 'controller');
|
||||||
|
|
|
@ -259,14 +259,14 @@ Discourse.Markdown = {
|
||||||
// The first time, let's add some more whitelisted tags
|
// The first time, let's add some more whitelisted tags
|
||||||
if (!_decoratedCaja) {
|
if (!_decoratedCaja) {
|
||||||
|
|
||||||
// Add anything whitelisted to the list of elements if it's not in there
|
// Add anything whitelisted to the list of elements if it's not in there already.
|
||||||
// already.
|
|
||||||
var elements = window.html4.ELEMENTS;
|
var elements = window.html4.ELEMENTS;
|
||||||
Object.keys(_validTags).forEach(function(t) {
|
Object.keys(_validTags).forEach(function(t) {
|
||||||
if (!elements[t]) {
|
if (!elements[t]) {
|
||||||
elements[t] = 0;
|
elements[t] = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_decoratedCaja = true;
|
_decoratedCaja = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -234,11 +234,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
|
||||||
},
|
},
|
||||||
key: "@",
|
key: "@",
|
||||||
transformComplete(v) {
|
transformComplete(v) {
|
||||||
if (v.username) {
|
return v.username ? v.username : v.usernames.join(", @");
|
||||||
return v.username;
|
|
||||||
} else {
|
|
||||||
return v.usernames.join(", @");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -263,13 +263,12 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
||||||
}.on('willDestroyElement'),
|
}.on('willDestroyElement'),
|
||||||
|
|
||||||
_postViewInserted: function() {
|
_postViewInserted: function() {
|
||||||
var $post = this.$(),
|
const $post = this.$(),
|
||||||
post = this.get('post'),
|
postNumber = this.get('post').get('post_number');
|
||||||
postNumber = post.get('post_number');
|
|
||||||
|
|
||||||
this._showLinkCounts();
|
this._showLinkCounts();
|
||||||
|
|
||||||
Discourse.ScreenTrack.current().track(this.$().prop('id'), postNumber);
|
Discourse.ScreenTrack.current().track($post.prop('id'), postNumber);
|
||||||
|
|
||||||
this.trigger('postViewInserted', $post);
|
this.trigger('postViewInserted', $post);
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,6 @@
|
||||||
background: dark-light-diff($primary, $secondary, 65%, -75%);
|
background: dark-light-diff($primary, $secondary, 65%, -75%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
&:active {
|
|
||||||
}
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
background: scale-color-diff();
|
background: scale-color-diff();
|
||||||
&:hover { color: scale-color($primary, $lightness: 70%); }
|
&:hover { color: scale-color($primary, $lightness: 70%); }
|
||||||
|
|
|
@ -30,26 +30,24 @@ $base-font-family: Helvetica, Arial, sans-serif !default;
|
||||||
@import "theme_variables";
|
@import "theme_variables";
|
||||||
@import "plugins_variables";
|
@import "plugins_variables";
|
||||||
|
|
||||||
|
// w3c definition of color brightness
|
||||||
@function brightness($color) {
|
@function brightness($color) {
|
||||||
@return ((red($color) * .299) + (green($color) * .587) + (blue($color) * .114)); //w3c definition of color brightness
|
@return ((red($color) * .299) + (green($color) * .587) + (blue($color) * .114));
|
||||||
}
|
}
|
||||||
|
|
||||||
@function dark-light-diff($adjusted-color, $comparison-color, $lightness, $darkness) {
|
@function dark-light-diff($adjusted-color, $comparison-color, $lightness, $darkness) {
|
||||||
@if brightness($adjusted-color) < brightness($comparison-color) {
|
@if brightness($adjusted-color) < brightness($comparison-color) {
|
||||||
@return scale-color($adjusted-color, $lightness: $lightness)
|
@return scale-color($adjusted-color, $lightness: $lightness);
|
||||||
} @else {
|
} @else {
|
||||||
@return scale-color($adjusted-color, $lightness: $darkness)
|
@return scale-color($adjusted-color, $lightness: $darkness);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//phasing out scale-color-diff for dark-light-diff
|
// phasing out scale-color-diff for dark-light-diff
|
||||||
|
|
||||||
@function scale-color-diff() {
|
@function scale-color-diff() {
|
||||||
@if lightness($primary) < lightness($secondary) {
|
@if lightness($primary) < lightness($secondary) {
|
||||||
@return scale-color($primary, $lightness: 90%)
|
@return scale-color($primary, $lightness: 90%);
|
||||||
}
|
} @else {
|
||||||
@else {
|
@return scale-color($primary, $lightness: -60%);
|
||||||
@return scale-color($primary, $lightness: -60%)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ class PostsController < ApplicationController
|
||||||
}
|
}
|
||||||
|
|
||||||
# to stay consistent with the create api, we allow for title & category changes here
|
# to stay consistent with the create api, we allow for title & category changes here
|
||||||
if post.post_number == 1
|
if post.is_first_post?
|
||||||
changes[:title] = params[:title] if params[:title]
|
changes[:title] = params[:title] if params[:title]
|
||||||
changes[:category_id] = params[:post][:category_id] if params[:post][:category_id]
|
changes[:category_id] = params[:post][:category_id] if params[:post][:category_id]
|
||||||
end
|
end
|
||||||
|
@ -135,7 +135,7 @@ class PostsController < ApplicationController
|
||||||
link_counts = TopicLink.counts_for(guardian,post.topic, [post])
|
link_counts = TopicLink.counts_for(guardian,post.topic, [post])
|
||||||
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
|
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
|
||||||
|
|
||||||
result = {post: post_serializer.as_json}
|
result = { post: post_serializer.as_json }
|
||||||
if revisor.category_changed.present?
|
if revisor.category_changed.present?
|
||||||
result[:category] = BasicCategorySerializer.new(revisor.category_changed, scope: guardian, root: false).as_json
|
result[:category] = BasicCategorySerializer.new(revisor.category_changed, scope: guardian, root: false).as_json
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,14 +12,29 @@ module HasCustomFields
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CUSTOM_FIELD_TRUE = ['1', 't', 'true', 'T', 'True', 'TRUE'].freeze unless defined? CUSTOM_FIELD_TRUE
|
CUSTOM_FIELD_TRUE ||= ['1', 't', 'true', 'T', 'True', 'TRUE'].freeze
|
||||||
|
|
||||||
|
def self.get_custom_field_type(types, key)
|
||||||
|
return unless types
|
||||||
|
|
||||||
|
sorted_types = types.keys.select { |k| k.end_with?("*") }
|
||||||
|
.sort_by(&:length)
|
||||||
|
.reverse
|
||||||
|
|
||||||
|
sorted_types.each do |t|
|
||||||
|
return types[t] if key =~ /^#{t}/i
|
||||||
|
end
|
||||||
|
|
||||||
|
types[key]
|
||||||
|
end
|
||||||
|
|
||||||
def self.cast_custom_field(key, value, types)
|
def self.cast_custom_field(key, value, types)
|
||||||
return value unless types && type = types[key]
|
return value unless type = get_custom_field_type(types, key)
|
||||||
|
|
||||||
case type
|
case type
|
||||||
when :boolean then !!CUSTOM_FIELD_TRUE.include?(value)
|
when :boolean then !!CUSTOM_FIELD_TRUE.include?(value)
|
||||||
when :integer then value.to_i
|
when :integer then value.to_i
|
||||||
|
when :json then ::JSON.parse(value)
|
||||||
else
|
else
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
@ -30,8 +45,8 @@ module HasCustomFields
|
||||||
has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
|
has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
|
||||||
after_save :save_custom_fields
|
after_save :save_custom_fields
|
||||||
|
|
||||||
# To avoid n+1 queries, we have this function to retrieve lots of custom fields in one
|
# To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
|
||||||
# go and create a "sideloaded" version for easy querying by id.
|
# and create a "sideloaded" version for easy querying by id.
|
||||||
def self.custom_fields_for_ids(ids, whitelisted_fields)
|
def self.custom_fields_for_ids(ids, whitelisted_fields)
|
||||||
klass = "#{name}CustomField".constantize
|
klass = "#{name}CustomField".constantize
|
||||||
foreign_key = "#{name.underscore}_id".to_sym
|
foreign_key = "#{name.underscore}_id".to_sym
|
||||||
|
@ -39,15 +54,18 @@ module HasCustomFields
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
return result if whitelisted_fields.blank?
|
return result if whitelisted_fields.blank?
|
||||||
klass.where(foreign_key => ids, :name => whitelisted_fields).pluck(foreign_key, :name, :value).each do |cf|
|
|
||||||
|
klass.where(foreign_key => ids, :name => whitelisted_fields)
|
||||||
|
.pluck(foreign_key, :name, :value).each do |cf|
|
||||||
result[cf[0]] ||= {}
|
result[cf[0]] ||= {}
|
||||||
append_custom_field(result[cf[0]], cf[1], cf[2])
|
append_custom_field(result[cf[0]], cf[1], cf[2])
|
||||||
end
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.append_custom_field(target, key, value)
|
def self.append_custom_field(target, key, value)
|
||||||
HasCustomFields::Helpers.append_field(target,key,value,@custom_field_types)
|
HasCustomFields::Helpers.append_field(target, key, value, @custom_field_types)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.register_custom_field_type(name, type)
|
def self.register_custom_field_type(name, type)
|
||||||
|
@ -63,7 +81,6 @@ module HasCustomFields
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def custom_fields
|
def custom_fields
|
||||||
@custom_fields ||= refresh_custom_fields_from_db.dup
|
@custom_fields ||= refresh_custom_fields_from_db.dup
|
||||||
end
|
end
|
||||||
|
@ -73,8 +90,7 @@ module HasCustomFields
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_fields_clean?
|
def custom_fields_clean?
|
||||||
# Check whether the cached version has been
|
# Check whether the cached version has been changed on this model
|
||||||
# changed on this model
|
|
||||||
!@custom_fields || @custom_fields_orig == @custom_fields
|
!@custom_fields || @custom_fields_orig == @custom_fields
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -86,9 +102,8 @@ module HasCustomFields
|
||||||
|
|
||||||
_custom_fields.each do |f|
|
_custom_fields.each do |f|
|
||||||
if dup[f.name].is_a? Array
|
if dup[f.name].is_a? Array
|
||||||
# we need to collect Arrays fully before
|
# we need to collect Arrays fully before we can compare them
|
||||||
# we can compare them
|
if !array_fields.has_key?(f.name)
|
||||||
if !array_fields.has_key? f.name
|
|
||||||
array_fields[f.name] = [f]
|
array_fields[f.name] = [f]
|
||||||
else
|
else
|
||||||
array_fields[f.name] << f
|
array_fields[f.name] << f
|
||||||
|
@ -104,17 +119,18 @@ module HasCustomFields
|
||||||
|
|
||||||
# let's iterate through our arrays and compare them
|
# let's iterate through our arrays and compare them
|
||||||
array_fields.each do |field_name, fields|
|
array_fields.each do |field_name, fields|
|
||||||
if fields.length == dup[field_name].length &&
|
if fields.length == dup[field_name].length && fields.map(&:value) == dup[field_name]
|
||||||
fields.map{|f| f.value} == dup[field_name]
|
|
||||||
dup.delete(field_name)
|
dup.delete(field_name)
|
||||||
else
|
else
|
||||||
fields.each{|f| f.destroy }
|
fields.each(&:destroy)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
dup.each do |k,v|
|
dup.each do |k,v|
|
||||||
if v.is_a? Array
|
if v.is_a? Array
|
||||||
v.each {|subv| _custom_fields.create(name: k, value: subv)}
|
v.each { |subv| _custom_fields.create(name: k, value: subv) }
|
||||||
|
elsif v.is_a? Hash
|
||||||
|
_custom_fields.create(name: k, value: v.to_json)
|
||||||
else
|
else
|
||||||
_custom_fields.create(name: k, value: v)
|
_custom_fields.create(name: k, value: v)
|
||||||
end
|
end
|
||||||
|
|
|
@ -315,6 +315,8 @@ class Post < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_first_post?
|
def is_first_post?
|
||||||
|
post_number.blank? ?
|
||||||
|
topic.try(:highest_post_number) == 0 :
|
||||||
post_number == 1
|
post_number == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -324,7 +326,7 @@ class Post < ActiveRecord::Base
|
||||||
|
|
||||||
def unhide!
|
def unhide!
|
||||||
self.update_attributes(hidden: false, hidden_at: nil, hidden_reason_id: nil)
|
self.update_attributes(hidden: false, hidden_at: nil, hidden_reason_id: nil)
|
||||||
self.topic.update_attributes(visible: true) if post_number == 1
|
self.topic.update_attributes(visible: true) if is_first_post?
|
||||||
save(validate: false)
|
save(validate: false)
|
||||||
publish_change_to_clients!(:acted)
|
publish_change_to_clients!(:acted)
|
||||||
end
|
end
|
||||||
|
@ -372,11 +374,7 @@ class Post < ActiveRecord::Base
|
||||||
def rebake!(opts=nil)
|
def rebake!(opts=nil)
|
||||||
opts ||= {}
|
opts ||= {}
|
||||||
|
|
||||||
new_cooked = cook(
|
new_cooked = cook(raw, topic_id: topic_id, invalidate_oneboxes: opts.fetch(:invalidate_oneboxes, false))
|
||||||
raw,
|
|
||||||
topic_id: topic_id,
|
|
||||||
invalidate_oneboxes: opts.fetch(:invalidate_oneboxes, false)
|
|
||||||
)
|
|
||||||
old_cooked = cooked
|
old_cooked = cooked
|
||||||
|
|
||||||
update_columns(cooked: new_cooked, baked_at: Time.new, baked_version: BAKED_VERSION)
|
update_columns(cooked: new_cooked, baked_at: Time.new, baked_version: BAKED_VERSION)
|
||||||
|
|
|
@ -60,7 +60,7 @@ class SearchObserver < ActiveRecord::Observer
|
||||||
if obj.topic
|
if obj.topic
|
||||||
category_name = obj.topic.category.name if obj.topic.category
|
category_name = obj.topic.category.name if obj.topic.category
|
||||||
SearchObserver.update_posts_index(obj.id, obj.cooked, obj.topic.title, category_name)
|
SearchObserver.update_posts_index(obj.id, obj.cooked, obj.topic.title, category_name)
|
||||||
SearchObserver.update_topics_index(obj.topic_id, obj.topic.title, obj.cooked) if obj.post_number == 1
|
SearchObserver.update_topics_index(obj.topic_id, obj.topic.title, obj.cooked) if obj.is_first_post?
|
||||||
else
|
else
|
||||||
Rails.logger.warn("Orphan post skipped in search_observer, topic_id: #{obj.topic_id} post_id: #{obj.id} raw: #{obj.raw}")
|
Rails.logger.warn("Orphan post skipped in search_observer, topic_id: #{obj.topic_id} post_id: #{obj.id} raw: #{obj.raw}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,6 +111,8 @@ class User < ActiveRecord::Base
|
||||||
# excluding fake users like the system user
|
# excluding fake users like the system user
|
||||||
scope :real, -> { where('id > 0') }
|
scope :real, -> { where('id > 0') }
|
||||||
|
|
||||||
|
scope :staff, -> { where("admin OR moderator") }
|
||||||
|
|
||||||
# TODO-PERF: There is no indexes on any of these
|
# TODO-PERF: There is no indexes on any of these
|
||||||
# and NotifyMailingListSubscribers does a select-all-and-loop
|
# and NotifyMailingListSubscribers does a select-all-and-loop
|
||||||
# may want to create an index on (active, blocked, suspended_till, mailing_list_mode)?
|
# may want to create an index on (active, blocked, suspended_till, mailing_list_mode)?
|
||||||
|
|
|
@ -45,7 +45,7 @@ class UserActionObserver < ActiveRecord::Observer
|
||||||
|
|
||||||
def log_post(model)
|
def log_post(model)
|
||||||
# first post gets nada
|
# first post gets nada
|
||||||
return if model.post_number == 1
|
return if model.is_first_post?
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
action_type: UserAction::REPLY,
|
action_type: UserAction::REPLY,
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
class PostSerializer < BasicPostSerializer
|
class PostSerializer < BasicPostSerializer
|
||||||
|
|
||||||
# To pass in additional information we might need
|
# To pass in additional information we might need
|
||||||
INSTANCE_VARS = [:topic_view,
|
INSTANCE_VARS = [
|
||||||
|
:topic_view,
|
||||||
:parent_post,
|
:parent_post,
|
||||||
:add_raw,
|
:add_raw,
|
||||||
:single_post_link_counts,
|
:single_post_link_counts,
|
||||||
:draft_sequence,
|
:draft_sequence,
|
||||||
:post_actions,
|
:post_actions,
|
||||||
:all_post_actions]
|
:all_post_actions
|
||||||
|
]
|
||||||
|
|
||||||
INSTANCE_VARS.each do |v|
|
INSTANCE_VARS.each do |v|
|
||||||
self.send(:attr_accessor, v)
|
self.send(:attr_accessor, v)
|
||||||
|
@ -268,7 +270,7 @@ class PostSerializer < BasicPostSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_static_doc?
|
def include_static_doc?
|
||||||
object.post_number == 1 && Discourse.static_doc_topic_ids.include?(object.topic_id)
|
object.is_first_post? && Discourse.static_doc_topic_ids.include?(object.topic_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_via_email?
|
def include_via_email?
|
||||||
|
@ -289,4 +291,9 @@ class PostSerializer < BasicPostSerializer
|
||||||
@active_flags ||= (@topic_view.present? && @topic_view.all_active_flags.present?) ? @topic_view.all_active_flags[object.id] : nil
|
@active_flags ||= (@topic_view.present? && @topic_view.all_active_flags.present?) ? @topic_view.all_active_flags[object.id] : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def post_custom_fields
|
||||||
|
@post_custom_fields ||= (@topic_view.present? && @topic_view.post_custom_fields.present?) ? @topic_view.post_custom_fields[object.id] : nil
|
||||||
|
@post_custom_fields ||= object.custom_fields
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,7 @@ class UserDestroyer
|
||||||
|
|
||||||
PostDestroyer.new(@actor.staff? ? @actor : Discourse.system_user, post).destroy
|
PostDestroyer.new(@actor.staff? ? @actor : Discourse.system_user, post).destroy
|
||||||
|
|
||||||
if post.topic and post.post_number == 1
|
if post.topic and post.is_first_post?
|
||||||
Topic.unscoped.where(id: post.topic.id).update_all(user_id: nil)
|
Topic.unscoped.where(id: post.topic.id).update_all(user_id: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -205,7 +205,7 @@ class CookedPostProcessor
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_topic_image(images)
|
def update_topic_image(images)
|
||||||
if @post.post_number == 1
|
if @post.is_first_post?
|
||||||
img = images.first
|
img = images.first
|
||||||
@post.topic.update_column(:image_url, img["src"]) if img["src"].present?
|
@post.topic.update_column(:image_url, img["src"]) if img["src"].present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -116,7 +116,7 @@ module PostGuardian
|
||||||
# Deleting Methods
|
# Deleting Methods
|
||||||
def can_delete_post?(post)
|
def can_delete_post?(post)
|
||||||
# Can't delete the first post
|
# Can't delete the first post
|
||||||
return false if post.post_number == 1
|
return false if post.is_first_post?
|
||||||
|
|
||||||
# Can't delete after post_edit_time_limit minutes have passed
|
# Can't delete after post_edit_time_limit minutes have passed
|
||||||
return false if !is_staff? && post.edit_time_limit_expired?
|
return false if !is_staff? && post.edit_time_limit_expired?
|
||||||
|
|
|
@ -45,29 +45,33 @@ class Plugin::Instance
|
||||||
end
|
end
|
||||||
|
|
||||||
def enabled?
|
def enabled?
|
||||||
return @enabled_site_setting ? SiteSetting.send(@enabled_site_setting) : true
|
@enabled_site_setting ? SiteSetting.send(@enabled_site_setting) : true
|
||||||
end
|
end
|
||||||
|
|
||||||
delegate :name, to: :metadata
|
delegate :name, to: :metadata
|
||||||
|
|
||||||
def add_to_serializer(serializer, attr, &block)
|
def add_to_serializer(serializer, attr, define_include_method=true, &block)
|
||||||
klass = "#{serializer.to_s.classify}Serializer".constantize
|
klass = "#{serializer.to_s.classify}Serializer".constantize
|
||||||
klass.attributes(attr)
|
|
||||||
|
klass.attributes(attr) unless attr.to_s.start_with?("include_")
|
||||||
|
|
||||||
klass.send(:define_method, attr, &block)
|
klass.send(:define_method, attr, &block)
|
||||||
|
|
||||||
|
return unless define_include_method
|
||||||
|
|
||||||
# Don't include serialized methods if the plugin is disabled
|
# Don't include serialized methods if the plugin is disabled
|
||||||
plugin = self
|
plugin = self
|
||||||
klass.send(:define_method, "include_#{attr}?") do
|
klass.send(:define_method, "include_#{attr}?") { plugin.enabled? }
|
||||||
plugin.enabled?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extend a class but check that the plugin is enabled
|
# Extend a class but check that the plugin is enabled
|
||||||
def add_to_class(klass, attr, &block)
|
def add_to_class(klass, attr, &block)
|
||||||
klass = klass.to_s.classify.constantize
|
klass = klass.to_s.classify.constantize
|
||||||
|
|
||||||
hidden_method_name = "#{attr}_without_enable_check".to_sym
|
hidden_method_name = :"#{attr}_without_enable_check"
|
||||||
klass.send(:define_method, hidden_method_name, &block)
|
klass.send(:define_method, hidden_method_name) do |*args|
|
||||||
|
block.call(*args)
|
||||||
|
end
|
||||||
|
|
||||||
plugin = self
|
plugin = self
|
||||||
klass.send(:define_method, attr) do |*args|
|
klass.send(:define_method, attr) do |*args|
|
||||||
|
@ -75,6 +79,15 @@ class Plugin::Instance
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add validation method but check that the plugin is enabled
|
||||||
|
def validate(klass, attr, &block)
|
||||||
|
klass = klass.to_s.classify.constantize
|
||||||
|
klass.send(:define_method, attr, &block)
|
||||||
|
|
||||||
|
plugin = self
|
||||||
|
klass.validate(attr, if: -> { plugin.enabled? })
|
||||||
|
end
|
||||||
|
|
||||||
# will make sure all the assets this plugin needs are registered
|
# will make sure all the assets this plugin needs are registered
|
||||||
def generate_automatic_assets!
|
def generate_automatic_assets!
|
||||||
paths = []
|
paths = []
|
||||||
|
|
|
@ -211,7 +211,7 @@ class PostCreator
|
||||||
return unless @post && @post.errors.count == 0 && @topic && @topic.category_id
|
return unless @post && @post.errors.count == 0 && @topic && @topic.category_id
|
||||||
|
|
||||||
Category.where(id: @topic.category_id).update_all(latest_post_id: @post.id)
|
Category.where(id: @topic.category_id).update_all(latest_post_id: @post.id)
|
||||||
Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id) if @post.post_number == 1
|
Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id) if @post.is_first_post?
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_in_allowed_users
|
def ensure_in_allowed_users
|
||||||
|
@ -293,7 +293,7 @@ class PostCreator
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.user_stat.post_count += 1
|
@user.user_stat.post_count += 1
|
||||||
@user.user_stat.topic_count += 1 if @post.post_number == 1
|
@user.user_stat.topic_count += 1 if @post.is_first_post?
|
||||||
|
|
||||||
# We don't count replies to your own topics
|
# We don't count replies to your own topics
|
||||||
if !@opts[:import_mode] && @user.id != @topic.user_id
|
if !@opts[:import_mode] && @user.id != @topic.user_id
|
||||||
|
|
|
@ -55,7 +55,7 @@ class PostDestroyer
|
||||||
user_recovered
|
user_recovered
|
||||||
end
|
end
|
||||||
topic = Topic.with_deleted.find @post.topic_id
|
topic = Topic.with_deleted.find @post.topic_id
|
||||||
topic.recover! if @post.post_number == 1
|
topic.recover! if @post.is_first_post?
|
||||||
topic.update_statistics
|
topic.update_statistics
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class PostDestroyer
|
||||||
@post.update_flagged_posts_count
|
@post.update_flagged_posts_count
|
||||||
remove_associated_replies
|
remove_associated_replies
|
||||||
remove_associated_notifications
|
remove_associated_notifications
|
||||||
if @post.topic && @post.post_number == 1
|
if @post.topic && @post.is_first_post?
|
||||||
StaffActionLogger.new(@user).log_topic_deletion(@post.topic, @opts.slice(:context)) if @user.id != @post.user_id
|
StaffActionLogger.new(@user).log_topic_deletion(@post.topic, @opts.slice(:context)) if @user.id != @post.user_id
|
||||||
@post.topic.trash!(@user)
|
@post.topic.trash!(@user)
|
||||||
elsif @user.id != @post.user_id
|
elsif @user.id != @post.user_id
|
||||||
|
@ -179,7 +179,7 @@ class PostDestroyer
|
||||||
|
|
||||||
def update_associated_category_latest_topic
|
def update_associated_category_latest_topic
|
||||||
return unless @post.topic && @post.topic.category
|
return unless @post.topic && @post.topic.category
|
||||||
return unless @post.id == @post.topic.category.latest_post_id || (@post.post_number == 1 && @post.topic_id == @post.topic.category.latest_topic_id)
|
return unless @post.id == @post.topic.category.latest_post_id || (@post.is_first_post? && @post.topic_id == @post.topic.category.latest_topic_id)
|
||||||
|
|
||||||
@post.topic.category.update_latest
|
@post.topic.category.update_latest
|
||||||
end
|
end
|
||||||
|
@ -196,7 +196,7 @@ class PostDestroyer
|
||||||
end
|
end
|
||||||
|
|
||||||
author.user_stat.post_count -= 1
|
author.user_stat.post_count -= 1
|
||||||
author.user_stat.topic_count -= 1 if @post.post_number == 1
|
author.user_stat.topic_count -= 1 if @post.is_first_post?
|
||||||
|
|
||||||
# We don't count replies to your own topics
|
# We don't count replies to your own topics
|
||||||
if @topic && author.id != @topic.user_id
|
if @topic && author.id != @topic.user_id
|
||||||
|
|
|
@ -332,7 +332,7 @@ class PostRevisor
|
||||||
end
|
end
|
||||||
|
|
||||||
def bypass_bump?
|
def bypass_bump?
|
||||||
@opts[:bypass_bump] == true
|
!@post_successfully_saved || @opts[:bypass_bump] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_last_post?
|
def is_last_post?
|
||||||
|
@ -347,7 +347,7 @@ class PostRevisor
|
||||||
end
|
end
|
||||||
|
|
||||||
def revise_topic
|
def revise_topic
|
||||||
return unless @post.post_number == 1
|
return unless @post.is_first_post?
|
||||||
|
|
||||||
update_topic_excerpt
|
update_topic_excerpt
|
||||||
update_category_description
|
update_category_description
|
||||||
|
|
|
@ -209,11 +209,13 @@ module PrettyText
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cook(text, opts={})
|
def self.cook(text, opts={})
|
||||||
cloned = opts.dup
|
options = opts.dup
|
||||||
|
|
||||||
# we have a minor inconsistency
|
# we have a minor inconsistency
|
||||||
cloned[:topicId] = opts[:topic_id]
|
options[:topicId] = opts[:topic_id]
|
||||||
sanitized = markdown(text.dup, cloned)
|
|
||||||
sanitized = add_rel_nofollow_to_user_content(sanitized) if !cloned[:omit_nofollow] && SiteSetting.add_rel_nofollow_to_user_content
|
sanitized = markdown(text.dup, options)
|
||||||
|
sanitized = add_rel_nofollow_to_user_content(sanitized) if !options[:omit_nofollow] && SiteSetting.add_rel_nofollow_to_user_content
|
||||||
sanitized
|
sanitized
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ require_dependency 'gaps'
|
||||||
class TopicView
|
class TopicView
|
||||||
|
|
||||||
attr_reader :topic, :posts, :guardian, :filtered_posts, :chunk_size
|
attr_reader :topic, :posts, :guardian, :filtered_posts, :chunk_size
|
||||||
attr_accessor :draft, :draft_key, :draft_sequence, :user_custom_fields
|
attr_accessor :draft, :draft_key, :draft_sequence, :user_custom_fields, :post_custom_fields
|
||||||
|
|
||||||
def self.slow_chunk_size
|
def self.slow_chunk_size
|
||||||
10
|
10
|
||||||
|
@ -16,6 +16,18 @@ class TopicView
|
||||||
20
|
20
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.post_custom_fields_whitelisters
|
||||||
|
@post_custom_fields_whitelisters ||= Set.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.add_post_custom_fields_whitelister(&block)
|
||||||
|
post_custom_fields_whitelisters << block
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.whitelisted_post_custom_fields(user)
|
||||||
|
post_custom_fields_whitelisters.map { |w| w.call(user) }.flatten.uniq
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(topic_id, user=nil, options={})
|
def initialize(topic_id, user=nil, options={})
|
||||||
@user = user
|
@user = user
|
||||||
@guardian = Guardian.new(@user)
|
@guardian = Guardian.new(@user)
|
||||||
|
@ -47,6 +59,11 @@ class TopicView
|
||||||
@user_custom_fields.deep_merge!(User.custom_fields_for_ids(@posts.map(&:user_id), SiteSetting.staff_user_custom_fields.split('|')))
|
@user_custom_fields.deep_merge!(User.custom_fields_for_ids(@posts.map(&:user_id), SiteSetting.staff_user_custom_fields.split('|')))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
whitelisted_fields = TopicView.whitelisted_post_custom_fields(@user)
|
||||||
|
if whitelisted_fields.present? && @posts
|
||||||
|
@post_custom_fields = Post.custom_fields_for_ids(@posts.map(&:id), whitelisted_fields)
|
||||||
|
end
|
||||||
|
|
||||||
@draft_key = @topic.draft_key
|
@draft_key = @topic.draft_key
|
||||||
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Poll plugin
|
|
||||||
|
|
||||||
Allows you to add a poll to the first post of a topic.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
1. Make your topic title start with "Poll: "
|
|
||||||
2. Include a list in your post (the **first list** will be used)
|
|
||||||
|
|
||||||
Important note:
|
|
||||||
|
|
||||||
Make sure you have the "Poll: " prefix in the title right from the start.
|
|
||||||
Editing the title to include it later is not possible atm.
|
|
||||||
|
|
||||||
## Closing the poll
|
|
||||||
|
|
||||||
Change the start of the topic title from "Poll: " to "Closed Poll: ". This feature uses the locale of the user who started the topic.
|
|
||||||
|
|
||||||
_Note: closing a topic will also close the poll._
|
|
||||||
|
|
||||||
## Specifying the list to be used for the poll
|
|
||||||
|
|
||||||
If you have multiple lists in your post and the first list is _not_
|
|
||||||
the one you want to use for the poll, you can identify the
|
|
||||||
list to be used like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
Intro Text
|
|
||||||
|
|
||||||
- Item one
|
|
||||||
- Item two
|
|
||||||
|
|
||||||
Here are your choices:
|
|
||||||
|
|
||||||
[poll]
|
|
||||||
- Option 1
|
|
||||||
- Option 2
|
|
||||||
[/poll]
|
|
||||||
```
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
export default Em.Component.extend({
|
||||||
|
tagName: "li",
|
||||||
|
attributeBindings: ["data-poll-option-id", "data-poll-selected", "style"],
|
||||||
|
|
||||||
|
"data-poll-option-id": Em.computed.alias("option.id"),
|
||||||
|
|
||||||
|
"data-poll-selected": function() {
|
||||||
|
return this.get("option.selected") ? "selected" : false;
|
||||||
|
}.property("option.selected"),
|
||||||
|
|
||||||
|
style: function() {
|
||||||
|
var styles = [];
|
||||||
|
if (this.get("color")) { styles.push("color:" + this.get("color")); }
|
||||||
|
if (this.get("background")) { styles.push("background:" + this.get("background")); }
|
||||||
|
return styles.length > 0 ? styles.join(";") : false;
|
||||||
|
}.property("color", "background"),
|
||||||
|
|
||||||
|
render(buffer) {
|
||||||
|
buffer.push(this.get("option.html"));
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
// ensure we're not clicking on a link
|
||||||
|
if ($(e.target).closest("a").length === 0) {
|
||||||
|
this.sendAction("toggle", this.get("option"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
import round from "discourse/plugins/poll/lib/round";
|
||||||
|
|
||||||
|
export default Em.Component.extend({
|
||||||
|
tagName: "span",
|
||||||
|
|
||||||
|
totalScore: function() {
|
||||||
|
return _.reduce(this.get("poll.options"), function(total, o) {
|
||||||
|
const value = parseInt(o.get("html"), 10),
|
||||||
|
votes = parseInt(o.get("votes"), 10);
|
||||||
|
return total + value * votes;
|
||||||
|
}, 0);
|
||||||
|
}.property("poll.options.@each.{html,votes}"),
|
||||||
|
|
||||||
|
average: function() {
|
||||||
|
return round(this.get("totalScore") / this.get("poll.total_votes"), -2);
|
||||||
|
}.property("totalScore", "poll.total_votes"),
|
||||||
|
|
||||||
|
averageRating: function() {
|
||||||
|
return I18n.t("poll.average_rating", { average: this.get("average") });
|
||||||
|
}.property("average"),
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
export default Em.Component.extend({
|
||||||
|
tagName: "table",
|
||||||
|
classNames: ["results"],
|
||||||
|
|
||||||
|
options: function() {
|
||||||
|
const totalVotes = this.get("poll.total_votes"),
|
||||||
|
backgroundColor = this.get("poll.background");
|
||||||
|
|
||||||
|
this.get("poll.options").forEach(option => {
|
||||||
|
const percentage = Math.floor(100 * option.get("votes") / totalVotes),
|
||||||
|
styles = ["width: " + percentage + "%"];
|
||||||
|
|
||||||
|
if (backgroundColor) { styles.push("background: " + backgroundColor); }
|
||||||
|
|
||||||
|
option.setProperties({
|
||||||
|
percentage: percentage,
|
||||||
|
title: I18n.t("poll.option_title", { count: option.get("votes") }),
|
||||||
|
style: styles.join(";")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.get("poll.options");
|
||||||
|
}.property("poll.total_votes", "poll.options.[]")
|
||||||
|
|
||||||
|
});
|
|
@ -1,48 +1,188 @@
|
||||||
import DiscourseController from 'discourse/controllers/controller';
|
export default Em.Controller.extend({
|
||||||
|
isMultiple: Em.computed.equal("poll.type", "multiple"),
|
||||||
|
isNumber: Em.computed.equal("poll.type", "number"),
|
||||||
|
isRandom : Em.computed.equal("poll.order", "random"),
|
||||||
|
isClosed: Em.computed.equal("poll.status", "closed"),
|
||||||
|
|
||||||
export default DiscourseController.extend({
|
// immediately shows the results when the user has already voted
|
||||||
poll: null,
|
showResults: Em.computed.gt("vote.length", 0),
|
||||||
showResults: Em.computed.oneWay('poll.closed'),
|
|
||||||
disableRadio: Em.computed.any('poll.closed', 'loading'),
|
// shows the results when
|
||||||
showToggleClosePoll: Em.computed.alias('poll.post.topic.details.can_edit'),
|
// - poll is closed
|
||||||
|
// - topic is archived/closed
|
||||||
|
// - user wants to see the results
|
||||||
|
showingResults: Em.computed.or("isClosed", "post.topic.closed", "post.topic.archived", "showResults"),
|
||||||
|
|
||||||
|
showResultsDisabled: Em.computed.equal("poll.total_votes", 0),
|
||||||
|
hideResultsDisabled: Em.computed.alias("isClosed"),
|
||||||
|
|
||||||
|
poll: function() {
|
||||||
|
const poll = this.get("model"),
|
||||||
|
vote = this.get("vote");
|
||||||
|
|
||||||
|
if (poll) {
|
||||||
|
const options = _.map(poll.get("options"), o => Em.Object.create(o));
|
||||||
|
|
||||||
|
if (vote) {
|
||||||
|
options.forEach(o => o.set("selected", vote.indexOf(o.get("id")) >= 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
poll.set("options", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return poll;
|
||||||
|
}.property("model"),
|
||||||
|
|
||||||
|
selectedOptions: function() {
|
||||||
|
return _.map(this.get("poll.options").filterBy("selected"), o => o.get("id"));
|
||||||
|
}.property("poll.options.@each.selected"),
|
||||||
|
|
||||||
|
totalVotesText: function() {
|
||||||
|
return I18n.t("poll.total_votes", { count: this.get("poll.total_votes") });
|
||||||
|
}.property("poll.total_votes"),
|
||||||
|
|
||||||
|
min: function() {
|
||||||
|
let min = parseInt(this.get("poll.min"), 10);
|
||||||
|
if (isNaN(min) || min < 1) { min = 1; }
|
||||||
|
return min;
|
||||||
|
}.property("poll.min"),
|
||||||
|
|
||||||
|
max: function() {
|
||||||
|
let options = this.get("poll.options.length"),
|
||||||
|
max = parseInt(this.get("poll.max"), 10);
|
||||||
|
if (isNaN(max) || max > options) { max = options; }
|
||||||
|
return max;
|
||||||
|
}.property("poll.max", "poll.options.length"),
|
||||||
|
|
||||||
|
multipleHelpText: function() {
|
||||||
|
const options = this.get("poll.options.length"),
|
||||||
|
min = this.get("min"),
|
||||||
|
max = this.get("max");
|
||||||
|
|
||||||
|
if (max > 0) {
|
||||||
|
if (min === max) {
|
||||||
|
if (min > 1) {
|
||||||
|
return I18n.t("poll.multiple.help.x_options", { count: min });
|
||||||
|
}
|
||||||
|
} else if (min > 1) {
|
||||||
|
if (max < options) {
|
||||||
|
return I18n.t("poll.multiple.help.between_min_and_max_options", { min: min, max: max });
|
||||||
|
} else {
|
||||||
|
return I18n.t("poll.multiple.help.at_least_min_options", { count: min });
|
||||||
|
}
|
||||||
|
} else if (max <= options) {
|
||||||
|
return I18n.t("poll.multiple.help.up_to_max_options", { count: max });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.property("min", "max", "poll.options.length"),
|
||||||
|
|
||||||
|
canCastVotes: function() {
|
||||||
|
if (this.get("isClosed") ||
|
||||||
|
this.get("showingResults") ||
|
||||||
|
this.get("loading")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOptionCount = this.get("selectedOptions.length");
|
||||||
|
|
||||||
|
if (this.get("isMultiple")) {
|
||||||
|
return selectedOptionCount >= this.get("min") && selectedOptionCount <= this.get("max");
|
||||||
|
} else {
|
||||||
|
return selectedOptionCount > 0;
|
||||||
|
}
|
||||||
|
}.property("isClosed", "showingResults", "loading",
|
||||||
|
"selectedOptions.length",
|
||||||
|
"isMultiple", "min", "max"),
|
||||||
|
|
||||||
|
castVotesDisabled: Em.computed.not("canCastVotes"),
|
||||||
|
|
||||||
|
canToggleStatus: function() {
|
||||||
|
return this.currentUser &&
|
||||||
|
(this.currentUser.get("id") === this.get("post.user_id") || this.currentUser.get("staff")) &&
|
||||||
|
!this.get("loading") &&
|
||||||
|
!this.get("post.topic.closed") &&
|
||||||
|
!this.get("post.topic.archived");
|
||||||
|
}.property("loading", "post.user_id", "post.topic.{closed,archived}"),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
selectOption(option) {
|
|
||||||
if (this.get('disableRadio')) {
|
toggleOption(option) {
|
||||||
return;
|
if (this.get("isClosed")) { return; }
|
||||||
|
if (!this.currentUser) { return this.send("showLogin"); }
|
||||||
|
|
||||||
|
const wasSelected = option.get("selected");
|
||||||
|
|
||||||
|
if (!this.get("isMultiple")) {
|
||||||
|
this.get("poll.options").forEach(o => o.set("selected", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.get('postController.currentUser.id')) {
|
option.toggleProperty("selected");
|
||||||
this.get('postController').send('showLogin');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set('loading', true);
|
if (!this.get("isMultiple") && !wasSelected) { this.send("castVotes"); }
|
||||||
|
},
|
||||||
|
|
||||||
|
castVotes() {
|
||||||
|
if (!this.get("canCastVotes")) { return; }
|
||||||
|
if (!this.currentUser) { return this.send("showLogin"); }
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this.get('poll').saveVote(option).then(function() {
|
|
||||||
self.setProperties({ loading: false, showResults: true});
|
this.set("loading", true);
|
||||||
|
|
||||||
|
Discourse.ajax("/polls/vote", {
|
||||||
|
type: "PUT",
|
||||||
|
data: {
|
||||||
|
post_id: this.get("post.id"),
|
||||||
|
poll_name: this.get("poll.name"),
|
||||||
|
options: this.get("selectedOptions"),
|
||||||
|
}
|
||||||
|
}).then(function(results) {
|
||||||
|
self.setProperties({ vote: results.vote, showingResults: true });
|
||||||
|
self.set("model", Em.Object.create(results.poll));
|
||||||
|
}).catch(function() {
|
||||||
|
bootbox.alert(I18n.t("poll.error_while_casting_votes"));
|
||||||
|
}).finally(function() {
|
||||||
|
self.set("loading", false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleShowResults() {
|
toggleResults() {
|
||||||
this.toggleProperty('showResults');
|
this.toggleProperty("showResults");
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleClosePoll() {
|
toggleStatus() {
|
||||||
const self = this;
|
if (!this.get("canToggleStatus")) { return; }
|
||||||
|
|
||||||
this.set('loading', true);
|
const self = this,
|
||||||
|
confirm = this.get("isClosed") ? "poll.open.confirm" : "poll.close.confirm";
|
||||||
|
|
||||||
return Discourse.ajax('/poll/toggle_close', {
|
bootbox.confirm(
|
||||||
type: 'PUT',
|
I18n.t(confirm),
|
||||||
data: { post_id: this.get('poll.post.id') }
|
I18n.t("no_value"),
|
||||||
}).then(function(result) {
|
I18n.t("yes_value"),
|
||||||
self.set('poll.post.topic.title', result.basic_topic.title);
|
function(confirmed) {
|
||||||
self.set('poll.post.topic.fancy_title', result.basic_topic.title);
|
if (confirmed) {
|
||||||
self.set('loading', false);
|
self.set("loading", true);
|
||||||
|
|
||||||
|
Discourse.ajax("/polls/toggle_status", {
|
||||||
|
type: "PUT",
|
||||||
|
data: {
|
||||||
|
post_id: self.get("post.id"),
|
||||||
|
poll_name: self.get("poll.name"),
|
||||||
|
status: self.get("isClosed") ? "open" : "closed",
|
||||||
|
}
|
||||||
|
}).then(function(results) {
|
||||||
|
self.set("model", Em.Object.create(results.poll));
|
||||||
|
}).catch(function() {
|
||||||
|
bootbox.alert(I18n.t("poll.error_while_toggling_status"));
|
||||||
|
}).finally(function() {
|
||||||
|
self.set("loading", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{{averageRating}}}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<tbody>
|
||||||
|
{{#each option in options}}
|
||||||
|
<tr>
|
||||||
|
<td class="option">{{{option.html}}}</td>
|
||||||
|
<td class="percentage">{{option.percentage}}%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="bar-back">
|
||||||
|
<div class="bar" {{bind-attr style=option.style}}></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
|
@ -1,37 +1,36 @@
|
||||||
<table>
|
<div class="poll-container">
|
||||||
{{#each po in poll.options}}
|
{{#if showingResults}}
|
||||||
<tr {{bind-attr class="po.checked:active"}} {{action "selectOption" po.option}}>
|
{{#if isNumber}}
|
||||||
<td class="radio">
|
{{poll-results-number poll=poll}}
|
||||||
<input type="radio" name="poll" {{bind-attr checked="po.checked" disabled="disableRadio"}}>
|
{{else}}
|
||||||
</td>
|
{{poll-results-standard poll=poll}}
|
||||||
<td class="option">
|
|
||||||
<div class="option">{{{po.option}}}</div>
|
|
||||||
{{#if showResults}}
|
|
||||||
<div class="result">{{i18n 'poll.voteCount' count=po.votes}}</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
{{else}}
|
||||||
</tr>
|
<ul>
|
||||||
|
{{#each option in poll.options}}
|
||||||
|
{{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</ul>
|
||||||
|
|
||||||
<div class='row'>
|
|
||||||
<button {{action "toggleShowResults"}} class="btn btn-small show-results">
|
|
||||||
{{#if showResults}}
|
|
||||||
{{fa-icon "eye-slash"}} {{i18n 'poll.results.hide'}}
|
|
||||||
{{else}}
|
|
||||||
{{fa-icon "eye"}} {{i18n 'poll.results.show'}}
|
|
||||||
{{/if}}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{{#if showToggleClosePoll}}
|
|
||||||
<button {{action "toggleClosePoll"}} class="btn btn-small toggle-poll">
|
|
||||||
{{#if poll.closed}}
|
|
||||||
{{fa-icon "unlock-alt"}} {{i18n 'poll.open_poll'}}
|
|
||||||
{{else}}
|
|
||||||
{{fa-icon "lock"}} {{i18n 'poll.close_poll'}}
|
|
||||||
{{/if}}
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{loading-spinner condition=loading}}
|
<p>{{totalVotesText}}</p>
|
||||||
|
|
||||||
|
{{#if isMultiple}}
|
||||||
|
<p>{{multipleHelpText}}</p>
|
||||||
|
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if showingResults}}
|
||||||
|
{{d-button class="toggle-results" title="poll.hide-results.title" label="poll.hide-results.label" icon="eye-slash" disabled=hideResultsDisabled action="toggleResults"}}
|
||||||
|
{{else}}
|
||||||
|
{{d-button class="toggle-results" title="poll.show-results.title" label="poll.show-results.label" icon="eye" disabled=showResultsDisabled action="toggleResults"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canToggleStatus}}
|
||||||
|
{{#if isClosed}}
|
||||||
|
{{d-button class="toggle-status" title="poll.open.title" label="poll.open.label" icon="unlock-alt" action="toggleStatus"}}
|
||||||
|
{{else}}
|
||||||
|
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import PostView from "discourse/views/post";
|
||||||
|
|
||||||
|
function createPollView(container, post, poll, vote) {
|
||||||
|
const controller = container.lookup("controller:poll", { singleton: false }),
|
||||||
|
view = container.lookup("view:poll");
|
||||||
|
|
||||||
|
controller.set("vote", vote);
|
||||||
|
|
||||||
|
controller.setProperties({
|
||||||
|
model: Em.Object.create(poll),
|
||||||
|
post: post,
|
||||||
|
});
|
||||||
|
|
||||||
|
view.set("controller", controller);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "extend-for-poll",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
|
||||||
|
// overwrite polls
|
||||||
|
PostView.reopen({
|
||||||
|
_createPollViews: function($post) {
|
||||||
|
const self = this,
|
||||||
|
post = this.get("post"),
|
||||||
|
polls = post.get("polls"),
|
||||||
|
votes = post.get("polls_votes") || {};
|
||||||
|
|
||||||
|
// don't even bother when there's no poll
|
||||||
|
if (!polls) { return; }
|
||||||
|
|
||||||
|
const pollViews = {};
|
||||||
|
|
||||||
|
// iterate over all polls
|
||||||
|
$(".poll", $post).each(function() {
|
||||||
|
const $div = $("<div>"),
|
||||||
|
$poll = $(this),
|
||||||
|
pollName = $poll.data("poll-name"),
|
||||||
|
pollView = createPollView(container, post, polls[pollName], votes[pollName]);
|
||||||
|
|
||||||
|
$poll.replaceWith($div);
|
||||||
|
pollView.constructor.renderer.replaceIn(pollView, $div[0]);
|
||||||
|
pollViews[pollName] = pollView;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messageBus.subscribe("/polls/" + this.get("post.id"), results => {
|
||||||
|
pollViews[results.poll.name].get("controller").set("model", Em.Object.create(results.poll));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set("pollViews", pollViews);
|
||||||
|
}.on("postViewInserted"),
|
||||||
|
|
||||||
|
_cleanUpPollViews: function() {
|
||||||
|
this.messageBus.unsubscribe("/polls/*");
|
||||||
|
|
||||||
|
if (this.get("pollViews")) {
|
||||||
|
_.forEach(this.get("pollViews"), v => v.destroy());
|
||||||
|
}
|
||||||
|
}.on("willClearRender")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
import Poll from "discourse/plugins/poll/models/poll";
|
|
||||||
import PollView from "discourse/plugins/poll/views/poll";
|
|
||||||
import PollController from "discourse/plugins/poll/controllers/poll";
|
|
||||||
|
|
||||||
import PostView from "discourse/views/post";
|
|
||||||
|
|
||||||
function initializePollView(self) {
|
|
||||||
const post = self.get('post'),
|
|
||||||
pollDetails = post.get('poll_details');
|
|
||||||
|
|
||||||
let poll = Poll.create({ post: post });
|
|
||||||
poll.updateFromJson(pollDetails);
|
|
||||||
|
|
||||||
const pollController = PollController.create({
|
|
||||||
poll: poll,
|
|
||||||
showResults: pollDetails["selected"],
|
|
||||||
postController: self.get('controller')
|
|
||||||
});
|
|
||||||
|
|
||||||
return self.createChildView(PollView, { controller: pollController });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'poll',
|
|
||||||
|
|
||||||
initialize: function() {
|
|
||||||
PostView.reopen({
|
|
||||||
createPollUI: function($post) {
|
|
||||||
if (!this.get('post').get('poll_details')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = initializePollView(this),
|
|
||||||
pollContainer = $post.find(".poll-ui:first");
|
|
||||||
|
|
||||||
if (pollContainer.length === 0) {
|
|
||||||
pollContainer = $post.find("ul:first");
|
|
||||||
}
|
|
||||||
|
|
||||||
let $div = $('<div>');
|
|
||||||
pollContainer.replaceWith($div);
|
|
||||||
view.constructor.renderer.appendTo(view, $div[0]);
|
|
||||||
this.set('pollView', view);
|
|
||||||
}.on('postViewInserted'),
|
|
||||||
|
|
||||||
clearPollView: function() {
|
|
||||||
if (this.get('pollView')) { this.get('pollView').destroy(); }
|
|
||||||
}.on('willClearRender')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
16
plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6
Normal file
16
plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
|
||||||
|
|
||||||
|
export default function(type, value, exp) {
|
||||||
|
// If the exp is undefined or zero...
|
||||||
|
if (typeof exp === 'undefined' || +exp === 0) { return Math[type](value); }
|
||||||
|
value = +value;
|
||||||
|
exp = +exp;
|
||||||
|
// If the value is not a number or the exp is not an integer...
|
||||||
|
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { return NaN; }
|
||||||
|
// Shift
|
||||||
|
value = value.toString().split('e');
|
||||||
|
value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
|
||||||
|
// Shift back
|
||||||
|
value = value.toString().split('e');
|
||||||
|
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
|
||||||
|
}
|
5
plugins/poll/assets/javascripts/lib/round.js.es6
Normal file
5
plugins/poll/assets/javascripts/lib/round.js.es6
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import decimalAdjust from "discourse/plugins/poll/lib/decimal-adjust";
|
||||||
|
|
||||||
|
export default function(value, exp) {
|
||||||
|
return decimalAdjust("round", value, exp);
|
||||||
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
export default Discourse.Model.extend({
|
|
||||||
post: null,
|
|
||||||
options: [],
|
|
||||||
closed: false,
|
|
||||||
|
|
||||||
postObserver: function() {
|
|
||||||
this.updateFromJson(this.get('post.poll_details'));
|
|
||||||
}.observes('post.poll_details'),
|
|
||||||
|
|
||||||
fetchNewPostDetails: Discourse.debounce(function() {
|
|
||||||
this.get('post.topic.postStream').triggerChangedPost(this.get('post.id'), this.get('post.topic.updated_at'));
|
|
||||||
}, 250).observes('post.topic.title'),
|
|
||||||
|
|
||||||
updateFromJson(json) {
|
|
||||||
const selectedOption = json["selected"];
|
|
||||||
let options = [];
|
|
||||||
|
|
||||||
Object.keys(json["options"]).forEach(function(option) {
|
|
||||||
options.push(Ember.Object.create({
|
|
||||||
option: option,
|
|
||||||
votes: json["options"][option],
|
|
||||||
checked: option === selectedOption
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setProperties({ options: options, closed: json.closed });
|
|
||||||
},
|
|
||||||
|
|
||||||
saveVote(option) {
|
|
||||||
this.get('options').forEach(function(opt) {
|
|
||||||
opt.set('checked', opt.get('option') === option);
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
return Discourse.ajax("/poll", {
|
|
||||||
type: "PUT",
|
|
||||||
data: { post_id: this.get('post.id'), option: option }
|
|
||||||
}).then(function(newJSON) {
|
|
||||||
self.updateFromJson(newJSON);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
Discourse.Dialect.inlineBetween({
|
|
||||||
start: '[poll]',
|
|
||||||
stop: '[/poll]',
|
|
||||||
rawContents: true,
|
|
||||||
emitter: function(contents) {
|
|
||||||
var list = Discourse.Dialect.cook(contents, {});
|
|
||||||
return ['div', { class: 'poll-ui' }, list];
|
|
||||||
}
|
|
||||||
});
|
|
149
plugins/poll/assets/javascripts/poll_dialect.js
Normal file
149
plugins/poll/assets/javascripts/poll_dialect.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
const DATA_PREFIX = "data-poll-";
|
||||||
|
const DEFAULT_POLL_NAME = "poll";
|
||||||
|
|
||||||
|
const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "color", "background", "status"];
|
||||||
|
const WHITELISTED_STYLES = ["color", "background"];
|
||||||
|
|
||||||
|
const ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=[^\\s\\]]+", "g");
|
||||||
|
|
||||||
|
Discourse.Dialect.replaceBlock({
|
||||||
|
start: /\[poll([^\]]*)\]([\s\S]*)/igm,
|
||||||
|
stop: /\[\/poll\]/igm,
|
||||||
|
|
||||||
|
emitter: function(blockContents, matches, options) {
|
||||||
|
// post-process inside block contents
|
||||||
|
var contents = [];
|
||||||
|
|
||||||
|
if (blockContents.length) {
|
||||||
|
var self = this, b;
|
||||||
|
while ((b = blockContents.shift()) !== undefined) {
|
||||||
|
this.processBlock(b, blockContents).forEach(function (bc) {
|
||||||
|
if (typeof bc === "string" || bc instanceof String) {
|
||||||
|
var processed = self.processInline(String(bc));
|
||||||
|
if (processed.length) {
|
||||||
|
contents.push(["p"].concat(processed));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contents.push(bc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default poll attributes
|
||||||
|
var attributes = { "class": "poll" };
|
||||||
|
attributes[DATA_PREFIX + "status"] = "open";
|
||||||
|
attributes[DATA_PREFIX + "name"] = DEFAULT_POLL_NAME;
|
||||||
|
|
||||||
|
// extract poll attributes
|
||||||
|
(matches[1].match(ATTRIBUTES_REGEX) || []).forEach(function(m) {
|
||||||
|
var attr = m.split("=");
|
||||||
|
attributes[DATA_PREFIX + attr[0]] = attr[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
// we might need these values later...
|
||||||
|
var min = parseInt(attributes[DATA_PREFIX + "min"], 10),
|
||||||
|
max = parseInt(attributes[DATA_PREFIX + "max"], 10),
|
||||||
|
step = parseInt(attributes[DATA_PREFIX + "step"], 10);
|
||||||
|
|
||||||
|
// generate the options when the type is "number"
|
||||||
|
if (attributes[DATA_PREFIX + "type"] === "number") {
|
||||||
|
// default values
|
||||||
|
if (isNaN(min)) { min = 1; }
|
||||||
|
if (isNaN(max)) { max = 10; }
|
||||||
|
if (isNaN(step)) { step = 1; }
|
||||||
|
// dynamically generate options
|
||||||
|
contents.push(["bulletlist"]);
|
||||||
|
for (var o = min; o <= max; o += step) {
|
||||||
|
contents[0].push(["listitem", String(o)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the first child is a list with at least 1 option
|
||||||
|
if (contents.length === 0 || contents[0].length <= 1 || (contents[0][0] !== "numberlist" && contents[0][0] !== "bulletlist")) {
|
||||||
|
return ["div"].concat(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove non whitelisted content
|
||||||
|
|
||||||
|
// generate <li> styles (if any)
|
||||||
|
var styles = [];
|
||||||
|
WHITELISTED_STYLES.forEach(function(style) {
|
||||||
|
if (attributes[DATA_PREFIX + style]) {
|
||||||
|
styles.push(style + ":" + attributes[DATA_PREFIX + style]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var style = styles.join(";");
|
||||||
|
|
||||||
|
// add option id (hash) + style
|
||||||
|
for (var o = 1; o < contents[0].length; o++) {
|
||||||
|
// break as soon as the list is done
|
||||||
|
if (contents[0][o][0] !== "listitem") { break; }
|
||||||
|
|
||||||
|
var attr = {};
|
||||||
|
// apply styles if any
|
||||||
|
if (style.length > 0) { attr["style"] = style; }
|
||||||
|
// compute md5 hash of the content of the option
|
||||||
|
attr[DATA_PREFIX + "option-id"] = md5(JSON.stringify(contents[0][o].slice(1)));
|
||||||
|
// store options attributes
|
||||||
|
contents[0][o].splice(1, 0, attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// that's our poll!
|
||||||
|
var result = ["div", attributes].concat(contents);
|
||||||
|
|
||||||
|
// add a small paragraph displaying the total number of votes
|
||||||
|
result.push(["p", I18n.t("poll.total_votes", { count: 0 })]);
|
||||||
|
|
||||||
|
// add some information when type is "multiple"
|
||||||
|
if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
||||||
|
var optionCount = contents[0].length - 1;
|
||||||
|
|
||||||
|
// default values
|
||||||
|
if (isNaN(min) || min < 1) { min = 1; }
|
||||||
|
if (isNaN(max) || max > optionCount) { max = optionCount; }
|
||||||
|
|
||||||
|
// add some help text
|
||||||
|
var help;
|
||||||
|
|
||||||
|
if (max > 0) {
|
||||||
|
if (min === max) {
|
||||||
|
if (min > 1) {
|
||||||
|
help = I18n.t("poll.multiple.help.x_options", { count: min });
|
||||||
|
}
|
||||||
|
} else if (min > 1) {
|
||||||
|
if (max < optionCount) {
|
||||||
|
help = I18n.t("poll.multiple.help.between_min_and_max_options", { min: min, max: max });
|
||||||
|
} else {
|
||||||
|
help = I18n.t("poll.multiple.help.at_least_min_options", { count: min });
|
||||||
|
}
|
||||||
|
} else if (max <= optionCount) {
|
||||||
|
help = I18n.t("poll.multiple.help.up_to_max_options", { count: max });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (help) { result.push(["p", help]); }
|
||||||
|
|
||||||
|
// add "cast-votes" button
|
||||||
|
result.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add "toggle-results" button
|
||||||
|
result.push(["a", { "class": "button toggle-results", "title": I18n.t("poll.show-results.title") }, I18n.t("poll.show-results.label")]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Discourse.Markdown.whiteListTag("div", "class", "poll");
|
||||||
|
Discourse.Markdown.whiteListTag("div", "data-*");
|
||||||
|
|
||||||
|
Discourse.Markdown.whiteListTag("a", "class", /^button (cast-votes|toggle-results)/);
|
||||||
|
|
||||||
|
Discourse.Markdown.whiteListTag("li", "data-*");
|
||||||
|
Discourse.Markdown.whiteListTag("li", "style");
|
||||||
|
|
||||||
|
})();
|
|
@ -1,4 +1,16 @@
|
||||||
export default Ember.View.extend({
|
export default Em.View.extend({
|
||||||
templateName: "poll",
|
templateName: "poll",
|
||||||
classNames: ['poll-ui'],
|
classNames: ["poll"],
|
||||||
|
attributeBindings: ["data-poll-type", "data-poll-name", "data-poll-status"],
|
||||||
|
|
||||||
|
poll: Em.computed.alias("controller.poll"),
|
||||||
|
|
||||||
|
"data-poll-type": Em.computed.alias("poll.type"),
|
||||||
|
"data-poll-name": Em.computed.alias("poll.name"),
|
||||||
|
"data-poll-status": Em.computed.alias("poll.status"),
|
||||||
|
|
||||||
|
_fixPollContainerHeight: function() {
|
||||||
|
const pollContainer = this.$(".poll-container");
|
||||||
|
pollContainer.height(pollContainer.height());
|
||||||
|
}.on("didInsertElement")
|
||||||
});
|
});
|
||||||
|
|
105
plugins/poll/assets/stylesheets/poll.scss
Normal file
105
plugins/poll/assets/stylesheets/poll.scss
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
div.poll {
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li, .option {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[data-poll-option-id] {
|
||||||
|
color: $secondary;
|
||||||
|
background: $primary;
|
||||||
|
padding: 0 .8em;
|
||||||
|
margin-bottom: .7em;
|
||||||
|
border-radius: .25rem;
|
||||||
|
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.2),
|
||||||
|
inset 0 0 0 100px rgba(0,0,0,0),
|
||||||
|
0 .2em 0 0 rgba(0,0,0,.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.25),
|
||||||
|
inset 0 0 0 100px rgba(0,0,0,.1),
|
||||||
|
0 .2em 0 0 rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
-webkit-transform: translate(0,2px);
|
||||||
|
transform: translate(0,2px);
|
||||||
|
box-shadow: inset 0 -.1em 0 0 rgba(0,0,0,.25),
|
||||||
|
inset 0 0 0 100px rgba(0,0,0,.1),
|
||||||
|
0 .1em 0 0 rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-poll-selected="selected"] {
|
||||||
|
background: green !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $primary;
|
||||||
|
background: dark-light-diff($primary, $secondary, 90%, -65%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: dark-light-diff($primary, $secondary, 65%, -75%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-container {
|
||||||
|
margin: 0;
|
||||||
|
span {
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
|
||||||
|
.option {
|
||||||
|
max-width: 90%;
|
||||||
|
padding-right: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage {
|
||||||
|
width: 10%;
|
||||||
|
font-size: 1.7em;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: #9E9E9E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-back {
|
||||||
|
background: rgb(219,219,219);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
height: 10px;
|
||||||
|
background: $primary;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-poll-type="number"] {
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: .7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ar:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
zero: "صوت 1"
|
|
||||||
one: "صوت 1"
|
|
||||||
two: "صوت 1"
|
|
||||||
few: "صوت 1"
|
|
||||||
many: "%{احسب} الأصوات"
|
|
||||||
other: "%{احسب} الأصوات"
|
|
||||||
results:
|
|
||||||
show: إظهار النتائج
|
|
||||||
hide: إخفاء النتائج
|
|
||||||
close_poll: "إغلاق التصويت"
|
|
||||||
open_poll: "فتح التصويت"
|
|
|
@ -1,11 +0,0 @@
|
||||||
ca:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 vot"
|
|
||||||
other: "%{count} vots"
|
|
||||||
results:
|
|
||||||
show: Mostra resultats
|
|
||||||
hide: Amaga resultats
|
|
||||||
close_poll: "Tanca enquesta"
|
|
||||||
open_poll: "Obre enquesta"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
de:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 Stimme"
|
|
||||||
other: "%{count} Stimmen"
|
|
||||||
results:
|
|
||||||
show: Ergebnisse anzeigen
|
|
||||||
hide: Ergebnisse ausblenden
|
|
||||||
close_poll: "Umfrage beenden"
|
|
||||||
open_poll: "Umfrage starten"
|
|
|
@ -1,30 +1,41 @@
|
||||||
# encoding: utf-8
|
|
||||||
# This file contains content for the client portion of Discourse, sent out
|
|
||||||
# to the Javascript app.
|
|
||||||
#
|
|
||||||
# To work with us on translations, see:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
#
|
|
||||||
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
|
||||||
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
|
||||||
#
|
|
||||||
# tx push -s
|
|
||||||
#
|
|
||||||
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
|
||||||
#
|
|
||||||
# To validate this YAML file after you change it, please paste it into
|
|
||||||
# http://yamllint.com/
|
|
||||||
|
|
||||||
en:
|
en:
|
||||||
js:
|
js:
|
||||||
poll:
|
poll:
|
||||||
voteCount:
|
total_votes:
|
||||||
one: "1 vote"
|
zero: "No votes yet. Want to be the first?"
|
||||||
other: "%{count} votes"
|
one: "There's only 1 vote."
|
||||||
|
other: "There are %{count} total votes."
|
||||||
|
|
||||||
results:
|
average_rating: "Average rating: <strong>%{average}</strong>."
|
||||||
show: Show Results
|
|
||||||
hide: Hide Results
|
|
||||||
|
|
||||||
close_poll: "Close Poll"
|
multiple:
|
||||||
open_poll: "Open Poll"
|
help:
|
||||||
|
at_least_min_options: "You may choose at least %{count} options."
|
||||||
|
up_to_max_options: "You may choose up to %{count} options."
|
||||||
|
x_options: "You may choose %{count} options."
|
||||||
|
between_min_and_max_options: "You may choose between %{min} and %{max} options."
|
||||||
|
|
||||||
|
cast-votes:
|
||||||
|
title: "Cast your votes"
|
||||||
|
label: "Vote now!"
|
||||||
|
|
||||||
|
show-results:
|
||||||
|
title: "Display the poll results"
|
||||||
|
label: "Show results"
|
||||||
|
|
||||||
|
hide-results:
|
||||||
|
title: "Back to your votes"
|
||||||
|
label: "Hide results"
|
||||||
|
|
||||||
|
open:
|
||||||
|
title: "Open the poll"
|
||||||
|
label: "Open"
|
||||||
|
confirm: "Are you sure you want to open this poll?"
|
||||||
|
|
||||||
|
close:
|
||||||
|
title: "Close the poll"
|
||||||
|
label: "Close"
|
||||||
|
confirm: "Are you sure you want to close this poll?"
|
||||||
|
|
||||||
|
error_while_toggling_status: "There was an error while toggling the status of this poll."
|
||||||
|
error_while_casting_votes: "There was an error while casting your votes."
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
es:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 voto"
|
|
||||||
other: "%{count} votos"
|
|
||||||
results:
|
|
||||||
show: Mostrar resultados
|
|
||||||
hide: Ocultar resultados
|
|
||||||
close_poll: "Cerrar encuesta"
|
|
||||||
open_poll: "Abrir encuesta"
|
|
|
@ -1,17 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fa_IR:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
other: "%{count} آرا"
|
|
||||||
results:
|
|
||||||
show: نمایش نتایج
|
|
||||||
hide: پنهان کرد نتایج
|
|
||||||
close_poll: "بستن نظرسنجی"
|
|
||||||
open_poll: "باز کردن نظرسنجی"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fi:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 ääni"
|
|
||||||
other: "%{count} ääntä"
|
|
||||||
results:
|
|
||||||
show: Näytä tulokset
|
|
||||||
hide: Piilota tulokset
|
|
||||||
close_poll: "Sulje kysely"
|
|
||||||
open_poll: "Avaa kysely"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fr:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 vote"
|
|
||||||
other: "%{count} votes"
|
|
||||||
results:
|
|
||||||
show: Voir les résultats
|
|
||||||
hide: Cacher les résultats
|
|
||||||
close_poll: "Fermer le sondage"
|
|
||||||
open_poll: "Réouvrir le sondage"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
he:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "הצבעה אחת"
|
|
||||||
other: "%{count} הצבעות"
|
|
||||||
results:
|
|
||||||
show: הצגת תוצאות
|
|
||||||
hide: הסתרת תוצאות
|
|
||||||
close_poll: "סגירת הצבעה"
|
|
||||||
open_poll: "פתיחת הצבעה"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
it:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 voto"
|
|
||||||
other: "%{count} voti"
|
|
||||||
results:
|
|
||||||
show: Mostra Risultati
|
|
||||||
hide: Nascondi Risultati
|
|
||||||
close_poll: "Chiudi Sondaggio"
|
|
||||||
open_poll: "Apri Sondaggio"
|
|
|
@ -1,17 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ko:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
other: "%{count} 표"
|
|
||||||
results:
|
|
||||||
show: 결과 보기
|
|
||||||
hide: 결과 숨기기
|
|
||||||
close_poll: "투표 끝내기"
|
|
||||||
open_poll: "투표 시작하기"
|
|
|
@ -1,19 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pl_PL:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 głos"
|
|
||||||
few: "%{count} głosy"
|
|
||||||
other: "%{count} głosów"
|
|
||||||
results:
|
|
||||||
show: Pokaż wyniki
|
|
||||||
hide: Ukryj wyniki
|
|
||||||
close_poll: "Zamknij ankietę"
|
|
||||||
open_poll: "Otwórz ankietę"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pt:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 voto"
|
|
||||||
other: "%{count} votos"
|
|
||||||
results:
|
|
||||||
show: Mostrar resultados
|
|
||||||
hide: Esconder resultados
|
|
||||||
close_poll: "Encerrar votação"
|
|
||||||
open_poll: "Abrir votação"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pt_BR:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 voto"
|
|
||||||
other: "%{count} votos"
|
|
||||||
results:
|
|
||||||
show: Mostrar Resultados
|
|
||||||
hide: Esconder Resultados
|
|
||||||
close_poll: "Fechar Enquete "
|
|
||||||
open_poll: "Enquete aberta"
|
|
|
@ -1,19 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ru:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "проголосовал 1"
|
|
||||||
few: "проголосовало %{count}"
|
|
||||||
other: "проголосовало %{count}"
|
|
||||||
results:
|
|
||||||
show: Показать результаты
|
|
||||||
hide: Скрыть результаты
|
|
||||||
close_poll: "Завершить опрос"
|
|
||||||
open_poll: "Запустить опрос снова"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
sq:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "1 votë"
|
|
||||||
other: "%{count} vota"
|
|
||||||
results:
|
|
||||||
show: Shfaq Rezultatet
|
|
||||||
hide: Fsheh Rezultate
|
|
||||||
close_poll: "Mbyll Sondazhin"
|
|
||||||
open_poll: "Hap Sondazhin"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
te:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
one: "ఒక ఓటు"
|
|
||||||
other: "%{count} ఓట్లు"
|
|
||||||
results:
|
|
||||||
show: ఫలితాలు చూపించు
|
|
||||||
hide: ఫలితాలు దాయు
|
|
||||||
close_poll: "ఓటు ముగించు"
|
|
||||||
open_poll: "ఓటు తెరువు"
|
|
|
@ -1,17 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
tr_TR:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
other: "%{count} oy"
|
|
||||||
results:
|
|
||||||
show: Sonuçları göster
|
|
||||||
hide: Sonuçları gizle
|
|
||||||
close_poll: "Anketi Bitir"
|
|
||||||
open_poll: "Anket Başlat"
|
|
|
@ -1,17 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
zh_CN:
|
|
||||||
js:
|
|
||||||
poll:
|
|
||||||
voteCount:
|
|
||||||
other: "%{count} 次投票"
|
|
||||||
results:
|
|
||||||
show: 显示结果
|
|
||||||
hide: 隐藏结果
|
|
||||||
close_poll: "关闭投票"
|
|
||||||
open_poll: "开始投票"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ar:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "خيارات التصويت"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "يجب أن يحتوي على قائمة خيارات التصويت"
|
|
||||||
cannot_have_modified_options: "التعديل غير ممكن بعد مضي 5 دقائق. اتصل بالمسؤول إذا كنت بحاجة لتغييرها."
|
|
||||||
cannot_add_or_remove_options: "تستطيع تعديله فقط ولا يمكنك إضافته أو حذفه. إذا كنت بحاجة لإضافة أو حذف خيارات يجب أن تُقفل هذا العنوان وتنشئ عنوان جديد."
|
|
||||||
prefix: "تصويت"
|
|
||||||
closed_prefix: "هذا التصويت مغلق"
|
|
|
@ -1,11 +0,0 @@
|
||||||
ca:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opcions d'enquesta"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "cal que contingui una llista d'opcions"
|
|
||||||
cannot_have_modified_options: "no es pot modificar quan hagin passat els primers cinc minuts. Contacta un moderador si necessites fer-hi canvis."
|
|
||||||
cannot_add_or_remove_options: "només es pot editar, no afegir o treure. Si necessites afegir o treure opcions, hauries de tancar aquest tema i crear-ne un de nou."
|
|
||||||
prefix: "Enquesta"
|
|
||||||
closed_prefix: "Enquesta tancada"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
de:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Umfrageoptionen"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "muss eine Liste mit Umfrageoptionen enthalten"
|
|
||||||
cannot_have_modified_options: "können nach den ersten 5 Minuten nicht mehr geändert werden. Kontaktiere einen Moderator, wenn du sie ändern möchtest."
|
|
||||||
cannot_add_or_remove_options: "können nur bearbeitet, jedoch nicht hinzugefügt oder entfernt werden. Wenn du Optionen hinzufügen oder entfernen möchtest, solltest du dieses Thema sperren und ein neues erstellen."
|
|
||||||
prefix: "Umfrage"
|
|
||||||
closed_prefix: "Beendete Umfrage"
|
|
|
@ -1,28 +1,25 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# This file contains content for the server portion of Discourse used by Ruby
|
|
||||||
#
|
|
||||||
# To work with us on translations, see:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
#
|
|
||||||
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
|
||||||
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
|
||||||
#
|
|
||||||
# tx push -s
|
|
||||||
#
|
|
||||||
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
|
||||||
#
|
|
||||||
# To validate this YAML file after you change it, please paste it into
|
|
||||||
# http://yamllint.com/
|
|
||||||
|
|
||||||
en:
|
en:
|
||||||
activerecord:
|
site_settings:
|
||||||
attributes:
|
poll_enabled: "Allow users to create polls?"
|
||||||
post:
|
|
||||||
poll_options: "Poll options"
|
|
||||||
poll:
|
poll:
|
||||||
must_contain_poll_options: "must contain a list of poll options"
|
multiple_polls_without_name: "There are multiple polls without a name. Use the '<code>name</code>' attribute to uniquely identify your polls."
|
||||||
cannot_have_modified_options: "cannot be modified after the first five minutes. Contact a moderator if you need to change them."
|
multiple_polls_with_same_name: "There are multiple polls with the same name: <strong>%{name}</strong>. Use the '<code>name</code>' attribute to uniquely identify your polls."
|
||||||
cannot_add_or_remove_options: "can only be edited, not added or removed. If you need to add or remove options you should lock this topic and create a new one."
|
|
||||||
prefix: "Poll"
|
default_poll_must_have_at_least_2_options: "Poll must have at least 2 options."
|
||||||
closed_prefix: "Closed Poll"
|
named_poll_must_have_at_least_2_options: "Poll named <strong>%{name}</strong> must have at least 2 options."
|
||||||
|
|
||||||
|
default_poll_must_have_different_options: "Poll must have different options."
|
||||||
|
named_poll_must_have_different_options: "Poll name <strong>%{name}</strong> must have different options."
|
||||||
|
|
||||||
|
cannot_change_polls_after_5_minutes: "Polls cannot be changed after the first 5 minutes. Contact a moderator if you need to change them."
|
||||||
|
staff_cannot_add_or_remove_options_after_5_minutes: "Poll options can only be edited after the first 5 minutes. If you need to add or remove options, you should close this topic and create a new one."
|
||||||
|
|
||||||
|
no_polls_associated_with_this_post: "No polls are associated with this post."
|
||||||
|
no_poll_with_this_name: "No poll named <strong>%{name}</strong> associated with this post."
|
||||||
|
|
||||||
|
topic_must_be_open_to_vote: "The topic must be open to vote."
|
||||||
|
poll_must_be_open_to_vote: "Poll must be open to vote."
|
||||||
|
|
||||||
|
topic_must_be_open_to_toggle_status: "The topic must be open to toggle status."
|
||||||
|
only_staff_or_op_can_toggle_status: "Only a staff member or the original poster can toggle a poll status."
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
es:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opciones de la encuesta"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "debe contener una lista con las opciones de la encuesta"
|
|
||||||
cannot_have_modified_options: "pasados 5 minutos, no se pueden modificar las opciones de la encuesta. Contacta un moderador si necesitas cambiarlas"
|
|
||||||
cannot_add_or_remove_options: "solo se pueden modificar, no añadir ni eliminar. Si necesitas añadir o eliminar opciones deberías cerrar este tema y crear una encuesta nueva."
|
|
||||||
prefix: "Encuesta"
|
|
||||||
closed_prefix: "Encuesta cerrada"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fa_IR:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "گزینههای نظرسنجی"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "باید فهرستی شامل گزینههای نظرسنجی باشد"
|
|
||||||
cannot_have_modified_options: "پس از گذشت ۵ دقیقه دیگر نمیتوان ویرایش کرد. اگر تغییری در آنها نیاز است با یکی از ناظمان تماس بگیرید."
|
|
||||||
cannot_add_or_remove_options: "تنها میتواند ویرایش شود، نه افزودنی و نه پاک کردنی. اگر به گزینههای اضافه و پاک کردن نیاز دارید، باید این جستار را قفل کنید و یکی دیگر بسازید."
|
|
||||||
prefix: "نظرسنجی"
|
|
||||||
closed_prefix: "اتمام نظرسنجی"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fi:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Kyselyn vaihtoehtoja"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "täytyy sisältää lista vastausvaihtoehdoista"
|
|
||||||
cannot_have_modified_options: "ei voi muokata kun viisi minuuttia on kulunut kyselyn luomisesta. Ota yhteyttä valvojaan jos sinun tarvitsee muokata vaihtoehtoja."
|
|
||||||
cannot_add_or_remove_options: "voi vain muokata, ei lisätä tai poistaa. Jos sinun tarvitsee lisätä tai poistaa vaihtoehtoja, sinun tulee lukita tämä ketju ja luoda uusi."
|
|
||||||
prefix: "Kysely"
|
|
||||||
closed_prefix: "Suljettu kysely"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
fr:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Les options du sondage"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "doit contenir une liste d'options pour le sondage"
|
|
||||||
cannot_have_modified_options: "ne peuvent pas être modifiés après 5 minutes. Merci de contacter un moderateur, si vous souhaitez les modifier"
|
|
||||||
cannot_add_or_remove_options: "peuvent seulement être modifiés. Si vous souhaitez en supprimer ou en ajouter, veuillez créer un nouveau sujet."
|
|
||||||
prefix: "Sondage "
|
|
||||||
closed_prefix: "Sondage fermé "
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
he:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "אפשרויות הצבעה"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "חובה להכיל רשימה של אפשרויות הצבעה"
|
|
||||||
cannot_have_modified_options: "לא ניתן לשנות את האפשרויות לאחר 5 הדקות הראשונות. יש לפנות למנהל כדי לבצע שינויים אלו."
|
|
||||||
cannot_add_or_remove_options: "ניתן רק לערוך, לא להוסיף או להסיר אפשרויות. כדי להוסיף או להסיר אפשרויות יש לנעול את נושא זה ולפתוח אחד חדש."
|
|
||||||
prefix: "הצבעה"
|
|
||||||
closed_prefix: "הצבעה סגורה"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
it:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opzioni sondaggio"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "deve contenere una lista di opzioni per il sondaggio"
|
|
||||||
cannot_have_modified_options: "non possono essere modificate dopo i primi cinque minuti. Contatta un moderatore se devi cambiarle."
|
|
||||||
cannot_add_or_remove_options: "possono essere solo modificate, ma non aggiunte o rimosse. Se devi aggiungere o rimuovere opzioni, devi prima bloccare questo argomento e crearne uno nuovo."
|
|
||||||
prefix: "Sondaggio"
|
|
||||||
closed_prefix: "Sondaggio Chiuso"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ko:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "투표 옵션"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "투표 옵션 목록 포함 필수"
|
|
||||||
cannot_have_modified_options: "5분 뒤에는 수정할 수 없습니다. 바꾸고 싶다면 관리자에게 문의하세요."
|
|
||||||
cannot_add_or_remove_options: "수정만 가능하고 추가나 삭제가 불가능 합니다. 선택사항을 추가하거나 삭제하고 싶다면 이 토픽을 잠그고 다른 토픽을 생성해야합니다."
|
|
||||||
prefix: "투표"
|
|
||||||
closed_prefix: "투표 닫기"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pl_PL:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opcje ankiety"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "musi zawierać listę możliwych wyborów ankiety"
|
|
||||||
cannot_have_modified_options: "nie mogą być zmienione po pierwszych pięciu minutach. Skontaktuj się z moderatorem, jeżeli musisz je zmienić."
|
|
||||||
cannot_add_or_remove_options: "mogą tylko być edytowane, nie dodawane ani usuwane. Jeśli musisz dodać lub usunąć opcje, powinieneś zamknąć ten temat i utworzyć nowy."
|
|
||||||
prefix: "Ankieta"
|
|
||||||
closed_prefix: "Zamknięta ankieta"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pt:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opções da votação"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "tem que conter uma lista de opções de votação"
|
|
||||||
cannot_have_modified_options: "não podem ser modificadas depois dos primeiros cinco minutos. Contacte um moderador se precisar de alterá-las."
|
|
||||||
cannot_add_or_remove_options: "podem apenas ser editadas, não podendo ser adicionadas ou removidas. Se precisar de adicionar ou remover opções, deverá bloquear este tópico e criar um novo."
|
|
||||||
prefix: "Votação"
|
|
||||||
closed_prefix: "Votação encerrada"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
pt_BR:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opções de votação "
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "deve conter uma lista de opções de votação"
|
|
||||||
cannot_have_modified_options: "não pode ser modificado após os primeiros cinco minutos. Contate o moderador se necessitar fazer alguma mudança."
|
|
||||||
cannot_add_or_remove_options: "Só pode ser editado, mas não adicionar nem remover. Se precisar das opções para adicionar ou remover, você deve bloquear este tópico e criar um novo."
|
|
||||||
prefix: "Votação"
|
|
||||||
closed_prefix: "Votação encerrada"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
ru:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Варианты ответов"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "должен содержать варианты ответов (список)"
|
|
||||||
cannot_have_modified_options: "нельзя изменять после первых пяти минут. Если все же нужно их отредактировать, свяжитесь с модератором."
|
|
||||||
cannot_add_or_remove_options: "можно редактировать, но не добавлять или удалять. Если нужно добавить или удалить, закройте эту тему и создайте новую."
|
|
||||||
prefix: "Опрос"
|
|
||||||
closed_prefix: "Завершившийся опрос"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
sq:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Opsionet e sondazhit"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "duhet të përmbajë një listë me pyetje"
|
|
||||||
cannot_have_modified_options: "nuk mund të ndryshohet pasi kanë kaluar pesë minuta. Kontakto një moderator nëse nevojiten ndryshime."
|
|
||||||
cannot_add_or_remove_options: "nuk mund të redaktohet, shtosh apo fshini pyetje. Nëse dëshironi të shtoni apo fshini pyetje ju duhet ta mbyllni këtë temë dhe të krijoni një të re."
|
|
||||||
prefix: "Sondazh"
|
|
||||||
closed_prefix: "Sondazh i Mbyllur"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
te:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "ఓటు ఐచ్చికాలు"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "తప్పనిసరి ఓటు ఐచ్చికాల జాబితా కలిగి ఉండాలి"
|
|
||||||
cannot_have_modified_options: "మొదటి ఐదు నిమిషాల తర్వాత మార్చైత కాదు. వీటిని మార్చాలంటే ఒక నిర్వాహకుడిని సంప్రదించండి. "
|
|
||||||
cannot_add_or_remove_options: "కేవలం సవరించవచ్చు, కలపైత కాదు, తొలగించైత కాదు. మీరు కలపడం లేదా తొలగించడం చేయాలంటే ఈ విషయానికి తాళం వేసి మరో కొత్త విషయం సృష్టించాలి"
|
|
||||||
prefix: "ఓటు"
|
|
||||||
closed_prefix: "మూసేసిన ఓటు"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
tr_TR:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "Anket seçenekleri"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "anket seçenekleri listesini içermeli"
|
|
||||||
cannot_have_modified_options: "ilk beş dakikadan sonra değişiklik yapılamaz. Değişiklik yapmanız gerekiyorsa, bir moderatör ile iletişime geçin."
|
|
||||||
cannot_add_or_remove_options: "sadece düzenlenebilir, ekleme veya çıkarma yapılamaz. Seçenek ekleme veya çıkarmanız gerekiyorsa, bu konuyu kitlemeli ve yeni bir konu oluşturmalısınız."
|
|
||||||
prefix: "Anket"
|
|
||||||
closed_prefix: "Bitmiş Anket"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#
|
|
||||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
|
||||||
#
|
|
||||||
# To work with us on translations, join this project:
|
|
||||||
# https://www.transifex.com/projects/p/discourse-org/
|
|
||||||
|
|
||||||
zh_CN:
|
|
||||||
activerecord:
|
|
||||||
attributes:
|
|
||||||
post:
|
|
||||||
poll_options: "投票选项"
|
|
||||||
poll:
|
|
||||||
must_contain_poll_options: "必须包含投票选项"
|
|
||||||
cannot_have_modified_options: "在开始的五分钟后不能修改。如果需要修改他们,请联系一位版主。"
|
|
||||||
cannot_add_or_remove_options: "只能被编辑,不能添加或者删除。如果您需要添加或者删除选项,你需要锁定这个投票并创建新的投票。"
|
|
||||||
prefix: "投票"
|
|
||||||
closed_prefix: "已关闭的投票:"
|
|
3
plugins/poll/config/settings.yml
Normal file
3
plugins/poll/config/settings.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
plugins:
|
||||||
|
poll_enabled:
|
||||||
|
default: true
|
|
@ -1,205 +1,267 @@
|
||||||
# name: poll
|
# name: poll
|
||||||
# about: adds poll support to Discourse
|
# about: Official poll plugin for Discourse
|
||||||
# version: 0.2
|
# version: 0.9
|
||||||
# authors: Vikhyat Korrapati
|
# authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip)
|
||||||
# url: https://github.com/discourse/discourse/tree/master/plugins/poll
|
# url: https://github.com/discourse/discourse/tree/master/plugins/poll
|
||||||
|
|
||||||
load File.expand_path("../poll.rb", __FILE__)
|
register_asset "stylesheets/poll.scss"
|
||||||
|
register_asset "javascripts/poll_dialect.js", :server_side
|
||||||
|
|
||||||
# Without this line we can't lookup the constant inside the after_initialize blocks,
|
PLUGIN_NAME ||= "discourse_poll".freeze
|
||||||
# because all of this is instance_eval'd inside an instance of Plugin::Instance.
|
|
||||||
PollPlugin = PollPlugin
|
POLLS_CUSTOM_FIELD ||= "polls".freeze
|
||||||
|
VOTES_CUSTOM_FIELD ||= "polls-votes".freeze
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
# Rails Engine for accepting votes.
|
|
||||||
module PollPlugin
|
module ::DiscoursePoll
|
||||||
class Engine < ::Rails::Engine
|
class Engine < ::Rails::Engine
|
||||||
engine_name "poll_plugin"
|
engine_name PLUGIN_NAME
|
||||||
isolate_namespace PollPlugin
|
isolate_namespace DiscoursePoll
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class PollController < ActionController::Base
|
require_dependency "application_controller"
|
||||||
include CurrentUser
|
class DiscoursePoll::PollsController < ::ApplicationController
|
||||||
|
requires_plugin PLUGIN_NAME
|
||||||
|
|
||||||
|
before_filter :ensure_logged_in
|
||||||
|
|
||||||
def vote
|
def vote
|
||||||
if current_user.nil?
|
post_id = params.require(:post_id)
|
||||||
render status: :forbidden, json: false
|
poll_name = params.require(:poll_name)
|
||||||
return
|
options = params.require(:options)
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post_id}") do
|
||||||
|
post = Post.find(post_id)
|
||||||
|
|
||||||
|
# topic must be open
|
||||||
|
if post.topic.try(:closed) || post.topic.try(:archived)
|
||||||
|
return render_json_error I18n.t("poll.topic_must_be_open_to_vote")
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:post_id].nil? or params[:option].nil?
|
polls = post.custom_fields[POLLS_CUSTOM_FIELD]
|
||||||
render status: 400, json: false
|
|
||||||
return
|
return render_json_error I18n.t("poll.no_polls_associated_with_this_post") if polls.blank?
|
||||||
|
|
||||||
|
poll = polls[poll_name]
|
||||||
|
|
||||||
|
return render_json_error I18n.t("poll.no_poll_with_this_name", name: poll_name) if poll.blank?
|
||||||
|
return render_json_error I18n.t("poll.poll_must_be_open_to_vote") if poll["status"] != "open"
|
||||||
|
|
||||||
|
votes = post.custom_fields["#{VOTES_CUSTOM_FIELD}-#{user_id}"] || {}
|
||||||
|
vote = votes[poll_name] || []
|
||||||
|
|
||||||
|
poll["total_votes"] += 1 if vote.size == 0
|
||||||
|
|
||||||
|
poll["options"].each do |option|
|
||||||
|
option["votes"] -= 1 if vote.include?(option["id"])
|
||||||
|
option["votes"] += 1 if options.include?(option["id"])
|
||||||
end
|
end
|
||||||
|
|
||||||
post = Post.find(params[:post_id])
|
votes[poll_name] = options
|
||||||
poll = PollPlugin::Poll.new(post)
|
|
||||||
unless poll.has_poll_details?
|
|
||||||
render status: 400, json: false
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
options = poll.details
|
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||||
|
post.custom_fields["#{VOTES_CUSTOM_FIELD}-#{user_id}"] = votes
|
||||||
|
post.save_custom_fields
|
||||||
|
|
||||||
unless options.keys.include? params[:option]
|
MessageBus.publish("/polls/#{post_id}", { poll: poll })
|
||||||
render status: 400, json: false
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
poll.set_vote!(current_user, params[:option])
|
render json: { poll: poll, vote: options }
|
||||||
|
|
||||||
MessageBus.publish("/topic/#{post.topic_id}", {
|
|
||||||
id: post.id,
|
|
||||||
post_number: post.post_number,
|
|
||||||
updated_at: Time.now,
|
|
||||||
type: "revised"
|
|
||||||
},
|
|
||||||
group_ids: post.topic.secure_group_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
render json: poll.serialize(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def toggle_close
|
|
||||||
post = Post.find(params[:post_id])
|
|
||||||
topic = post.topic
|
|
||||||
poll = PollPlugin::Poll.new(post)
|
|
||||||
|
|
||||||
# Make sure the user is allowed to close the poll.
|
|
||||||
Guardian.new(current_user).ensure_can_edit!(topic)
|
|
||||||
|
|
||||||
# Make sure this is actually a poll.
|
|
||||||
unless poll.has_poll_details?
|
|
||||||
render status: 400, json: false
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Make sure the topic is not closed.
|
|
||||||
if topic.closed?
|
|
||||||
render status: 400, json: false
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Modify topic title.
|
|
||||||
I18n.with_locale(topic.user.effective_locale) do
|
|
||||||
if topic.title =~ /^(#{I18n.t('poll.prefix').strip})\s?:/i
|
|
||||||
topic.title = topic.title.gsub(/^(#{I18n.t('poll.prefix').strip})\s?:/i, I18n.t('poll.closed_prefix') + ':')
|
|
||||||
elsif topic.title =~ /^(#{I18n.t('poll.closed_prefix').strip})\s?:/i
|
|
||||||
topic.title = topic.title.gsub(/^(#{I18n.t('poll.closed_prefix').strip})\s?:/i, I18n.t('poll.prefix') + ':')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
topic.acting_user = current_user
|
def toggle_status
|
||||||
topic.save!
|
post_id = params.require(:post_id)
|
||||||
|
poll_name = params.require(:poll_name)
|
||||||
|
status = params.require(:status)
|
||||||
|
|
||||||
render json: topic, serializer: BasicTopicSerializer
|
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post_id}") do
|
||||||
|
post = Post.find(post_id)
|
||||||
|
|
||||||
|
# either staff member or OP
|
||||||
|
unless current_user.try(:staff?) || current_user.try(:id) == post.user_id
|
||||||
|
return render_json_error I18n.t("poll.only_staff_or_op_can_toggle_status")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# topic must be open
|
||||||
|
if post.topic.try(:closed) || post.topic.try(:archived)
|
||||||
|
return render_json_error I18n.t("poll.topic_must_be_open_to_toggle_status")
|
||||||
|
end
|
||||||
|
|
||||||
|
polls = post.custom_fields[POLLS_CUSTOM_FIELD]
|
||||||
|
|
||||||
|
return render_json_error I18n.t("poll.no_polls_associated_with_this_post") if polls.blank?
|
||||||
|
return render_json_error I18n.t("poll.no_poll_with_this_name", name: poll_name) if polls[poll_name].blank?
|
||||||
|
|
||||||
|
polls[poll_name]["status"] = status
|
||||||
|
|
||||||
|
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||||
|
post.save_custom_fields
|
||||||
|
|
||||||
|
MessageBus.publish("/polls/#{post_id}", { poll: polls[poll_name] })
|
||||||
|
|
||||||
|
render json: { poll: polls[poll_name] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PollPlugin::Engine.routes.draw do
|
end
|
||||||
put '/' => 'poll#vote'
|
|
||||||
put '/toggle_close' => 'poll#toggle_close'
|
DiscoursePoll::Engine.routes.draw do
|
||||||
|
put "/vote" => "polls#vote"
|
||||||
|
put "/toggle_status" => "polls#toggle_status"
|
||||||
end
|
end
|
||||||
|
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
mount ::PollPlugin::Engine, at: '/poll'
|
mount ::DiscoursePoll::Engine, at: "/polls"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Starting a topic title with "Poll:" will create a poll topic. If the title
|
|
||||||
# starts with "poll:" but the first post doesn't contain a list of options in
|
|
||||||
# it we need to raise an error.
|
|
||||||
Post.class_eval do
|
Post.class_eval do
|
||||||
validate :poll_options
|
attr_accessor :polls
|
||||||
def poll_options
|
|
||||||
poll = PollPlugin::Poll.new(self)
|
|
||||||
|
|
||||||
return unless poll.is_poll?
|
# save the polls when the post is created
|
||||||
|
after_save do
|
||||||
|
next if self.polls.blank? || !self.polls.is_a?(Hash)
|
||||||
|
|
||||||
if poll.options.length == 0
|
post = self
|
||||||
self.errors.add(:raw, I18n.t('poll.must_contain_poll_options'))
|
polls = self.polls
|
||||||
|
|
||||||
|
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post.id}") do
|
||||||
|
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||||
|
post.save_custom_fields
|
||||||
end
|
end
|
||||||
|
|
||||||
poll.ensure_can_be_edited!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Save the list of options to PluginStore after the post is saved.
|
DATA_PREFIX ||= "data-poll-".freeze
|
||||||
Post.class_eval do
|
DEFAULT_POLL_NAME ||= "poll".freeze
|
||||||
after_save :save_poll_options_to_plugin_store
|
|
||||||
def save_poll_options_to_plugin_store
|
validate(:post, :polls) do
|
||||||
PollPlugin::Poll.new(self).update_options!
|
# only care when raw has changed!
|
||||||
|
return unless self.raw_changed?
|
||||||
|
|
||||||
|
# TODO: we should fix the callback mess so that the cooked version is available
|
||||||
|
# in the validators instead of cooking twice
|
||||||
|
cooked = PrettyText.cook(self.raw, topic_id: self.topic_id)
|
||||||
|
parsed = Nokogiri::HTML(cooked)
|
||||||
|
|
||||||
|
polls = {}
|
||||||
|
extracted_polls = []
|
||||||
|
|
||||||
|
# extract polls
|
||||||
|
parsed.css("div.poll").each do |p|
|
||||||
|
poll = { "options" => [], "total_votes" => 0 }
|
||||||
|
|
||||||
|
# extract attributes
|
||||||
|
p.attributes.values.each do |attribute|
|
||||||
|
if attribute.name.start_with?(DATA_PREFIX)
|
||||||
|
poll[attribute.name[DATA_PREFIX.length..-1]] = attribute.value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add poll details into the post serializer.
|
# extract options
|
||||||
PostSerializer.class_eval do
|
p.css("li[#{DATA_PREFIX}option-id]").each do |o|
|
||||||
attributes :poll_details
|
option_id = o.attributes[DATA_PREFIX + "option-id"].value
|
||||||
def poll_details
|
poll["options"] << { "id" => option_id, "html" => o.inner_html, "votes" => 0 }
|
||||||
PollPlugin::Poll.new(object).serialize(scope.user)
|
|
||||||
end
|
end
|
||||||
def include_poll_details?
|
|
||||||
PollPlugin::Poll.new(object).has_poll_details?
|
# add the poll
|
||||||
|
extracted_polls << poll
|
||||||
|
end
|
||||||
|
|
||||||
|
# validate polls
|
||||||
|
extracted_polls.each do |poll|
|
||||||
|
# polls should have a unique name
|
||||||
|
if polls.has_key?(poll["name"])
|
||||||
|
poll["name"] == DEFAULT_POLL_NAME ?
|
||||||
|
self.errors.add(:base, I18n.t("poll.multiple_polls_without_name")) :
|
||||||
|
self.errors.add(:base, I18n.t("poll.multiple_polls_with_same_name", name: poll["name"]))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# options must be unique
|
||||||
|
if poll["options"].map { |o| o["id"] }.uniq.size != poll["options"].size
|
||||||
|
poll["name"] == DEFAULT_POLL_NAME ?
|
||||||
|
self.errors.add(:base, I18n.t("poll.default_poll_must_have_different_options")) :
|
||||||
|
self.errors.add(:base, I18n.t("poll.named_poll_must_have_different_options", name: poll["name"]))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# at least 2 options
|
||||||
|
if poll["options"].size < 2
|
||||||
|
poll["name"] == DEFAULT_POLL_NAME ?
|
||||||
|
self.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_2_options")) :
|
||||||
|
self.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_2_options", name: poll["name"]))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# store the valid poll
|
||||||
|
polls[poll["name"]] = poll
|
||||||
|
end
|
||||||
|
|
||||||
|
# are we updating a post outside the 5-minute edit window?
|
||||||
|
if self.id.present? && self.created_at < 5.minutes.ago
|
||||||
|
post = self
|
||||||
|
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post.id}") do
|
||||||
|
# load previous polls
|
||||||
|
previous_polls = post.custom_fields[POLLS_CUSTOM_FIELD] || {}
|
||||||
|
|
||||||
|
# are the polls different?
|
||||||
|
if polls.keys != previous_polls.keys ||
|
||||||
|
polls.values.map { |p| p["options"] } != previous_polls.values.map { |p| p["options"] }
|
||||||
|
|
||||||
|
# cannot add/remove/change/re-order polls
|
||||||
|
if polls.keys != previous_polls.keys
|
||||||
|
post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# deal with option changes
|
||||||
|
if User.staff.pluck(:id).include?(post.last_editor_id)
|
||||||
|
# staff can only edit options
|
||||||
|
polls.each_key do |poll_name|
|
||||||
|
if polls[poll_name]["options"].size != previous_polls[poll_name]["options"].size
|
||||||
|
post.errors.add(:base, I18n.t("poll.staff_cannot_add_or_remove_options_after_5_minutes"))
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# merge votes
|
||||||
|
polls.each_key do |poll_name|
|
||||||
|
polls[poll_name]["total_votes"] = previous_polls[poll_name]["total_votes"]
|
||||||
|
for o in 0...polls[poll_name]["options"].size
|
||||||
|
polls[poll_name]["options"][o]["votes"] = previous_polls[poll_name]["options"][o]["votes"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# OP cannot change polls after 5 minutes
|
||||||
|
post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# immediately store the polls
|
||||||
|
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||||
|
post.save_custom_fields
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# polls will be saved once we have a post id
|
||||||
|
self.polls = polls
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Post.register_custom_field_type(POLLS_CUSTOM_FIELD, :json)
|
||||||
|
Post.register_custom_field_type("#{VOTES_CUSTOM_FIELD}-*", :json)
|
||||||
|
|
||||||
|
TopicView.add_post_custom_fields_whitelister do |user|
|
||||||
|
whitelisted = [POLLS_CUSTOM_FIELD]
|
||||||
|
whitelisted << "#{VOTES_CUSTOM_FIELD}-#{user.id}" if user
|
||||||
|
whitelisted
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_serializer(:post, :polls, false) { post_custom_fields[POLLS_CUSTOM_FIELD] }
|
||||||
|
add_to_serializer(:post, :include_polls?) { post_custom_fields.present? && post_custom_fields[POLLS_CUSTOM_FIELD].present? }
|
||||||
|
|
||||||
|
add_to_serializer(:post, :polls_votes, false) { post_custom_fields["#{VOTES_CUSTOM_FIELD}-#{scope.user.id}"] }
|
||||||
|
add_to_serializer(:post, :include_polls_votes?) { scope.user && post_custom_fields.present? && post_custom_fields["#{VOTES_CUSTOM_FIELD}-#{scope.user.id}"].present? }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Poll UI.
|
|
||||||
register_asset "javascripts/models/poll.js.es6"
|
|
||||||
register_asset "javascripts/controllers/poll.js.es6"
|
|
||||||
register_asset "javascripts/views/poll.js.es6"
|
|
||||||
register_asset "javascripts/discourse/templates/poll.hbs"
|
|
||||||
register_asset "javascripts/initializers/poll.js.es6"
|
|
||||||
register_asset "javascripts/poll_bbcode.js", :server_side
|
|
||||||
|
|
||||||
register_css <<CSS
|
|
||||||
|
|
||||||
.poll-ui table {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui .row {
|
|
||||||
padding-left: 15px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.poll-ui td.radio input {
|
|
||||||
margin-left: -10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui td {
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui td.option .option {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui td.option .result {
|
|
||||||
float: right;
|
|
||||||
margin-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui tr.active {
|
|
||||||
background-color: #FFFFB3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui button i.fa {
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui .radio {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-ui .toggle-poll {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
CSS
|
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
module ::PollPlugin
|
|
||||||
|
|
||||||
class Poll
|
|
||||||
def initialize(post)
|
|
||||||
@post = post
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_poll?
|
|
||||||
# Not a new post, and also not the first post.
|
|
||||||
return false if @post.post_number.present? && @post.post_number > 1
|
|
||||||
|
|
||||||
topic = @post.topic
|
|
||||||
|
|
||||||
# Topic is not set in a couple of cases in the Discourse test suite.
|
|
||||||
return false if topic.nil? || topic.user.nil?
|
|
||||||
|
|
||||||
# New post, but not the first post in the topic.
|
|
||||||
return false if @post.post_number.nil? && topic.highest_post_number > 0
|
|
||||||
|
|
||||||
I18n.with_locale(topic.user.effective_locale) do
|
|
||||||
topic.title =~ /^(#{I18n.t('poll.prefix').strip}|#{I18n.t('poll.closed_prefix').strip})\s?:/i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_poll_details?
|
|
||||||
self.is_poll?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Called during validation of poll posts. Discourse already restricts edits to
|
|
||||||
# the OP and staff, we want to make sure that:
|
|
||||||
#
|
|
||||||
# * OP cannot edit options after 5 minutes.
|
|
||||||
# * Staff can only edit options after 5 minutes, not add/remove.
|
|
||||||
def ensure_can_be_edited!
|
|
||||||
# Return if this is a new post or the options were not modified.
|
|
||||||
return if @post.id.nil? || (options.sort == details.keys.sort)
|
|
||||||
|
|
||||||
# First 5 minutes -- allow any modification.
|
|
||||||
return unless @post.created_at < 5.minutes.ago
|
|
||||||
|
|
||||||
if User.find(@post.last_editor_id).staff?
|
|
||||||
# Allow editing options, but not adding or removing.
|
|
||||||
if options.length != details.keys.length
|
|
||||||
@post.errors.add(:poll_options, I18n.t('poll.cannot_add_or_remove_options'))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# not staff, tell them to contact one.
|
|
||||||
@post.errors.add(:poll_options, I18n.t('poll.cannot_have_modified_options'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_closed?
|
|
||||||
topic = @post.topic
|
|
||||||
topic.closed? || topic.archived? || (topic.title =~ /^#{I18n.t('poll.closed_prefix', locale: topic.user.effective_locale)}/i) === 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def options
|
|
||||||
cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id)
|
|
||||||
parsed = Nokogiri::HTML(cooked)
|
|
||||||
poll_list = parsed.css(".poll-ui ul").first || parsed.css("ul").first
|
|
||||||
if poll_list
|
|
||||||
poll_list.css("li").map {|x| x.children.to_s.strip }.uniq
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_options!
|
|
||||||
return unless self.is_poll?
|
|
||||||
return if details && details.keys.sort == options.sort
|
|
||||||
|
|
||||||
if details.try(:length) == options.length
|
|
||||||
|
|
||||||
# Assume only renaming, no reordering. Preserve votes.
|
|
||||||
old_details = self.details
|
|
||||||
old_options = old_details.keys
|
|
||||||
new_details = {}
|
|
||||||
new_options = self.options
|
|
||||||
rename = {}
|
|
||||||
|
|
||||||
0.upto(options.length-1) do |i|
|
|
||||||
new_details[ new_options[i] ] = old_details[ old_options[i] ]
|
|
||||||
|
|
||||||
if new_options[i] != old_options[i]
|
|
||||||
rename[ old_options[i] ] = new_options[i]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.set_details! new_details
|
|
||||||
|
|
||||||
# Update existing user votes.
|
|
||||||
# Accessing PluginStoreRow directly isn't a very nice approach but there's
|
|
||||||
# no way around it unfortunately.
|
|
||||||
# TODO: Probably want to move this to a background job.
|
|
||||||
PluginStoreRow.where(plugin_name: "poll", value: rename.keys).where('key LIKE ?', vote_key_prefix+"%").find_each do |row|
|
|
||||||
# This could've been done more efficiently using `update_all` instead of
|
|
||||||
# iterating over each individual vote, however this will be needed in the
|
|
||||||
# future once we support multiple choice polls.
|
|
||||||
row.value = rename[ row.value ]
|
|
||||||
row.save
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
# Options were added or removed.
|
|
||||||
new_options = self.options
|
|
||||||
new_details = self.details || {}
|
|
||||||
new_details.each do |key, value|
|
|
||||||
unless new_options.include? key
|
|
||||||
new_details.delete(key)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
new_options.each do |key|
|
|
||||||
new_details[key] ||= 0
|
|
||||||
end
|
|
||||||
self.set_details! new_details
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def details
|
|
||||||
@details ||= ::PluginStore.get("poll", details_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_details!(new_details)
|
|
||||||
::PluginStore.set("poll", details_key, new_details)
|
|
||||||
@details = new_details
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_vote(user)
|
|
||||||
user.nil? ? nil : ::PluginStore.get("poll", vote_key(user))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_vote!(user, option)
|
|
||||||
return if is_closed?
|
|
||||||
|
|
||||||
# Get the user's current vote.
|
|
||||||
DistributedMutex.new(details_key).synchronize do
|
|
||||||
vote = get_vote(user)
|
|
||||||
vote = nil unless details.keys.include? vote
|
|
||||||
|
|
||||||
new_details = details.dup
|
|
||||||
new_details[vote] -= 1 if vote
|
|
||||||
new_details[option] += 1
|
|
||||||
|
|
||||||
::PluginStore.set("poll", vote_key(user), option)
|
|
||||||
set_details! new_details
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialize(user)
|
|
||||||
return nil if details.nil?
|
|
||||||
{options: details, selected: get_vote(user), closed: is_closed?}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def details_key
|
|
||||||
"poll_options_#{@post.id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def vote_key_prefix
|
|
||||||
"poll_vote_#{@post.id}_"
|
|
||||||
end
|
|
||||||
|
|
||||||
def vote_key(user)
|
|
||||||
"#{vote_key_prefix}#{user.id}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
99
plugins/poll/spec/controllers/polls_controller_spec.rb
Normal file
99
plugins/poll/spec/controllers/polls_controller_spec.rb
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
describe ::DiscoursePoll::PollsController do
|
||||||
|
routes { ::DiscoursePoll::Engine.routes }
|
||||||
|
|
||||||
|
let!(:user) { log_in }
|
||||||
|
let(:topic) { Fabricate(:topic) }
|
||||||
|
let(:poll) { Fabricate(:post, topic_id: topic.id, user_id: user.id, raw: "[poll]\n- A\n- B\n[/poll]") }
|
||||||
|
|
||||||
|
describe "#vote" do
|
||||||
|
|
||||||
|
it "works" do
|
||||||
|
MessageBus.expects(:publish)
|
||||||
|
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||||
|
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["poll"]["name"]).to eq("poll")
|
||||||
|
expect(json["poll"]["total_votes"]).to eq(1)
|
||||||
|
expect(json["vote"]).to eq(["A"])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "supports vote changes" do
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] }
|
||||||
|
expect(response).to be_success
|
||||||
|
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["poll"]["total_votes"]).to eq(1)
|
||||||
|
expect(json["poll"]["options"][0]["votes"]).to eq(0)
|
||||||
|
expect(json["poll"]["options"][1]["votes"]).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ensures topic is not closed" do
|
||||||
|
topic.update_attribute(:closed, true)
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.topic_must_be_open_to_vote"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ensures topic is not archived" do
|
||||||
|
topic.update_attribute(:archived, true)
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.topic_must_be_open_to_vote"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ensures polls are associated with the post" do
|
||||||
|
xhr :put, :vote, { post_id: Fabricate(:post).id, poll_name: "foobar", options: ["A"] }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.no_polls_associated_with_this_post"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "checks the name of the poll" do
|
||||||
|
xhr :put, :vote, { post_id: poll.id, poll_name: "foobar", options: ["A"] }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.no_poll_with_this_name", name: "foobar"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ensures poll is open" do
|
||||||
|
closed_poll = Fabricate(:post, raw: "[poll status=closed]\n- A\n- B\n[/poll]")
|
||||||
|
xhr :put, :vote, { post_id: closed_poll.id, poll_name: "poll", options: ["A"] }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.poll_must_be_open_to_vote"))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#toggle_status" do
|
||||||
|
|
||||||
|
it "works for OP" do
|
||||||
|
MessageBus.expects(:publish)
|
||||||
|
|
||||||
|
xhr :put, :toggle_status, { post_id: poll.id, poll_name: "poll", status: "closed" }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["poll"]["status"]).to eq("closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works for staff" do
|
||||||
|
log_in(:moderator)
|
||||||
|
MessageBus.expects(:publish)
|
||||||
|
|
||||||
|
xhr :put, :toggle_status, { post_id: poll.id, poll_name: "poll", status: "closed" }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["poll"]["status"]).to eq("closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
137
plugins/poll/spec/controllers/posts_controller_spec.rb
Normal file
137
plugins/poll/spec/controllers/posts_controller_spec.rb
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
describe PostsController do
|
||||||
|
let!(:user) { log_in }
|
||||||
|
let!(:title) { "Testing Poll Plugin" }
|
||||||
|
|
||||||
|
describe "polls" do
|
||||||
|
|
||||||
|
it "works" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["cooked"]).to match("data-poll-")
|
||||||
|
expect(json["polls"]["poll"]).to be
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works on any post" do
|
||||||
|
post = Fabricate(:post)
|
||||||
|
xhr :post, :create, { topic_id: post.topic.id, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["cooked"]).to match("data-poll-")
|
||||||
|
expect(json["polls"]["poll"]).to be
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have different options" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- A[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_different_options"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have at least 2 options" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_at_least_2_options"))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "edit window" do
|
||||||
|
|
||||||
|
describe "within the first 5 minutes" do
|
||||||
|
|
||||||
|
let(:post_id) do
|
||||||
|
Timecop.freeze(3.minutes.ago) do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||||
|
::JSON.parse(response.body)["id"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be changed" do
|
||||||
|
xhr :put, :update, { id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["post"]["polls"]["poll"]["options"][2]["html"]).to eq("C")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "after the first 5 minutes" do
|
||||||
|
|
||||||
|
let(:post_id) do
|
||||||
|
Timecop.freeze(6.minutes.ago) do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||||
|
::JSON.parse(response.body)["id"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:new_raw) { "[poll]\n- A\n- C[/poll]" }
|
||||||
|
|
||||||
|
it "cannot be changed by OP" do
|
||||||
|
xhr :put, :update, { id: post_id, post: { raw: new_raw } }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be edited by staff" do
|
||||||
|
log_in_user(Fabricate(:moderator))
|
||||||
|
xhr :put, :update, { id: post_id, post: { raw: new_raw } }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "named polls" do
|
||||||
|
|
||||||
|
it "should have different options" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A\n- A[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_different_options", name: "foo"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have at least 2 options" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_at_least_2_options", name: "foo"))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "multiple polls" do
|
||||||
|
|
||||||
|
it "works" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" }
|
||||||
|
expect(response).to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["cooked"]).to match("data-poll-")
|
||||||
|
expect(json["polls"]["poll"]).to be
|
||||||
|
expect(json["polls"]["foo"]).to be
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have a name" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_without_name"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have unique name" do
|
||||||
|
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" }
|
||||||
|
expect(response).not_to be_success
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_with_same_name", name: "foo"))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,92 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe PollPlugin::PollController, type: :controller do
|
|
||||||
routes { PollPlugin::Engine.routes }
|
|
||||||
|
|
||||||
let(:topic) { create_topic(title: "Poll: Chitoge vs Onodera") }
|
|
||||||
let!(:post) { create_post(topic: topic, raw: "Pick one.\n\n[poll]\n* Chitoge\n* Onodera\n[/poll]") }
|
|
||||||
let(:user1) { Fabricate(:user) }
|
|
||||||
let(:user2) { Fabricate(:user) }
|
|
||||||
let(:admin) { Fabricate(:admin) }
|
|
||||||
|
|
||||||
describe 'vote' do
|
|
||||||
it "returns 403 if no user is logged in" do
|
|
||||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
|
||||||
response.should be_forbidden
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns 400 if post_id or invalid option is not specified" do
|
|
||||||
log_in_user user1
|
|
||||||
xhr :put, :vote
|
|
||||||
response.status.should eq(400)
|
|
||||||
xhr :put, :vote, post_id: post.id
|
|
||||||
response.status.should eq(400)
|
|
||||||
xhr :put, :vote, option: "Chitoge"
|
|
||||||
response.status.should eq(400)
|
|
||||||
xhr :put, :vote, post_id: post.id, option: "Tsugumi"
|
|
||||||
response.status.should eq(400)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns 400 if post_id doesn't correspond to a poll post" do
|
|
||||||
log_in_user user1
|
|
||||||
post2 = create_post(topic: topic, raw: "Generic reply")
|
|
||||||
xhr :put, :vote, post_id: post2.id, option: "Chitoge"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "saves votes correctly" do
|
|
||||||
MessageBus.expects(:publish).times(3)
|
|
||||||
|
|
||||||
log_in_user user1
|
|
||||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
|
||||||
PollPlugin::Poll.new(post).get_vote(user1).should eq("Chitoge")
|
|
||||||
|
|
||||||
log_in_user user2
|
|
||||||
xhr :put, :vote, post_id: post.id, option: "Onodera"
|
|
||||||
PollPlugin::Poll.new(post).get_vote(user2).should eq("Onodera")
|
|
||||||
|
|
||||||
PollPlugin::Poll.new(post).details["Chitoge"].should eq(1)
|
|
||||||
PollPlugin::Poll.new(post).details["Onodera"].should eq(1)
|
|
||||||
|
|
||||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
|
||||||
PollPlugin::Poll.new(post).get_vote(user2).should eq("Chitoge")
|
|
||||||
|
|
||||||
PollPlugin::Poll.new(post).details["Chitoge"].should eq(2)
|
|
||||||
PollPlugin::Poll.new(post).details["Onodera"].should eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'toggle_close' do
|
|
||||||
it "returns 400 if post_id doesn't correspond to a poll post" do
|
|
||||||
log_in_user admin
|
|
||||||
post2 = create_post(topic: topic, raw: "Generic reply")
|
|
||||||
xhr :put, :toggle_close, post_id: post2.id
|
|
||||||
response.status.should eq(400)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns 400 if the topic is locked" do
|
|
||||||
log_in_user admin
|
|
||||||
topic.update_attributes closed: true
|
|
||||||
xhr :put, :toggle_close, post_id: post.id
|
|
||||||
response.status.should eq(400)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "raises Discourse::InvalidAccess is the user is not authorized" do
|
|
||||||
log_in_user user1
|
|
||||||
expect do
|
|
||||||
xhr :put, :toggle_close, post_id: post.id
|
|
||||||
end.to raise_error(Discourse::InvalidAccess)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "renames the topic" do
|
|
||||||
I18n.stubs(:t).with('poll.prefix').returns("Poll ")
|
|
||||||
I18n.stubs(:t).with('poll.closed_prefix').returns("Closed Poll ")
|
|
||||||
log_in_user admin
|
|
||||||
xhr :put, :toggle_close, post_id: post.id
|
|
||||||
response.status.should eq(200)
|
|
||||||
topic.reload.title.should == "Closed Poll : Chitoge vs Onodera"
|
|
||||||
xhr :put, :toggle_close, post_id: post.id
|
|
||||||
response.status.should eq(200)
|
|
||||||
topic.reload.title.should == "Poll : Chitoge vs Onodera"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,97 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe PollPlugin::Poll do
|
|
||||||
let(:topic) { create_topic(title: "Poll: Chitoge vs Onodera") }
|
|
||||||
let(:post) { create_post(topic: topic, raw: "Pick one.\n\n[poll]\n* Chitoge\n* Onodera\n[/poll]") }
|
|
||||||
let(:poll) { PollPlugin::Poll.new(post) }
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
it "should detect poll post correctly" do
|
|
||||||
expect(poll.is_poll?).to be_truthy
|
|
||||||
post2 = create_post(topic: topic, raw: "This is a generic reply.")
|
|
||||||
expect(PollPlugin::Poll.new(post2).is_poll?).to be_falsey
|
|
||||||
post.topic.title = "Not a poll"
|
|
||||||
expect(poll.is_poll?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it "strips whitespace from the prefix translation" do
|
|
||||||
topic.title = "Polll: This might be a poll"
|
|
||||||
topic.save
|
|
||||||
expect(PollPlugin::Poll.new(post).is_poll?).to be_falsey
|
|
||||||
I18n.expects(:t).with('poll.prefix').returns("Polll ")
|
|
||||||
I18n.expects(:t).with('poll.closed_prefix').returns("Closed Poll ")
|
|
||||||
expect(PollPlugin::Poll.new(post).is_poll?).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should get options correctly" do
|
|
||||||
expect(poll.options).to eq(["Chitoge", "Onodera"])
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should fall back to using the first list if [poll] markup is not present" do
|
|
||||||
topic = create_topic(title: "This is not a poll topic")
|
|
||||||
post = create_post(topic: topic, raw: "Pick one.\n\n* Chitoge\n* Onodera")
|
|
||||||
poll = PollPlugin::Poll.new(post)
|
|
||||||
expect(poll.options).to eq(["Chitoge", "Onodera"])
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should get details correctly" do
|
|
||||||
expect(poll.details).to eq({"Chitoge" => 0, "Onodera" => 0})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should set details correctly" do
|
|
||||||
poll.set_details!({})
|
|
||||||
poll.details.should eq({})
|
|
||||||
PollPlugin::Poll.new(post).details.should eq({})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should get and set votes correctly" do
|
|
||||||
poll.get_vote(user).should eq(nil)
|
|
||||||
poll.set_vote!(user, "Onodera")
|
|
||||||
poll.get_vote(user).should eq("Onodera")
|
|
||||||
poll.details["Onodera"].should eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not set votes on closed polls" do
|
|
||||||
poll.set_vote!(user, "Onodera")
|
|
||||||
post.topic.closed = true
|
|
||||||
post.topic.save!
|
|
||||||
poll.set_vote!(user, "Chitoge")
|
|
||||||
poll.get_vote(user).should eq("Onodera")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should serialize correctly" do
|
|
||||||
poll.serialize(user).should eq({options: poll.details, selected: nil, closed: false})
|
|
||||||
poll.set_vote!(user, "Onodera")
|
|
||||||
poll.serialize(user).should eq({options: poll.details, selected: "Onodera", closed: false})
|
|
||||||
poll.serialize(nil).should eq({options: poll.details, selected: nil, closed: false})
|
|
||||||
|
|
||||||
topic.title = "Closed Poll: my poll"
|
|
||||||
topic.save
|
|
||||||
|
|
||||||
post.topic.reload
|
|
||||||
poll = PollPlugin::Poll.new(post)
|
|
||||||
poll.serialize(nil).should eq({options: poll.details, selected: nil, closed: true})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should serialize to nil if there are no poll options" do
|
|
||||||
topic = create_topic(title: "This is not a poll topic")
|
|
||||||
post = create_post(topic: topic, raw: "no options in the content")
|
|
||||||
poll = PollPlugin::Poll.new(post)
|
|
||||||
poll.serialize(user).should eq(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "stores poll options to plugin store" do
|
|
||||||
poll.set_vote!(user, "Onodera")
|
|
||||||
poll.stubs(:options).returns(["Chitoge", "Onodera", "Inferno Cop"])
|
|
||||||
poll.update_options!
|
|
||||||
poll.details.keys.sort.should eq(["Chitoge", "Inferno Cop", "Onodera"])
|
|
||||||
poll.details["Inferno Cop"].should eq(0)
|
|
||||||
poll.details["Onodera"].should eq(1)
|
|
||||||
|
|
||||||
poll.stubs(:options).returns(["Chitoge", "Onodera v2", "Inferno Cop"])
|
|
||||||
poll.update_options!
|
|
||||||
poll.details.keys.sort.should eq(["Chitoge", "Inferno Cop", "Onodera v2"])
|
|
||||||
poll.details["Onodera v2"].should eq(1)
|
|
||||||
poll.get_vote(user).should eq("Onodera v2")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,37 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
require 'post_creator'
|
|
||||||
|
|
||||||
describe PostCreator do
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:admin) { Fabricate(:admin) }
|
|
||||||
|
|
||||||
context "poll topic" do
|
|
||||||
let(:poll_post) { PostCreator.create(user, {title: "Poll: This is a poll", raw: "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"}) }
|
|
||||||
|
|
||||||
it "cannot be created without a list of options" do
|
|
||||||
post = PostCreator.create(user, {title: "Poll: This is a poll", raw: "body does not contain a list"})
|
|
||||||
post.errors[:raw].should be_present
|
|
||||||
end
|
|
||||||
|
|
||||||
it "cannot have options changed after 5 minutes" do
|
|
||||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
|
|
||||||
poll_post.valid?.should == true
|
|
||||||
poll_post.save
|
|
||||||
Timecop.freeze(Time.now + 6.minutes) do
|
|
||||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"
|
|
||||||
poll_post.valid?.should == false
|
|
||||||
poll_post.errors[:poll_options].should be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows staff to edit options after 5 minutes" do
|
|
||||||
poll_post.last_editor_id = admin.id
|
|
||||||
Timecop.freeze(Time.now + 6.minutes) do
|
|
||||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4.1\n[/poll]"
|
|
||||||
poll_post.valid?.should == true
|
|
||||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
|
|
||||||
poll_post.valid?.should == false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,7 +5,6 @@ describe HasCustomFields do
|
||||||
|
|
||||||
context "custom_fields" do
|
context "custom_fields" do
|
||||||
before do
|
before do
|
||||||
|
|
||||||
Topic.exec_sql("create temporary table custom_fields_test_items(id SERIAL primary key)")
|
Topic.exec_sql("create temporary table custom_fields_test_items(id SERIAL primary key)")
|
||||||
Topic.exec_sql("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text)")
|
Topic.exec_sql("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text)")
|
||||||
|
|
||||||
|
@ -85,10 +84,9 @@ describe HasCustomFields do
|
||||||
# refresh loads from database
|
# refresh loads from database
|
||||||
expect(test_item.reload.custom_fields["a"]).to eq("1")
|
expect(test_item.reload.custom_fields["a"]).to eq("1")
|
||||||
expect(test_item.custom_fields["a"]).to eq("1")
|
expect(test_item.custom_fields["a"]).to eq("1")
|
||||||
|
|
||||||
end
|
end
|
||||||
it "double save actually saves" do
|
|
||||||
|
|
||||||
|
it "double save actually saves" do
|
||||||
test_item = CustomFieldsTestItem.new
|
test_item = CustomFieldsTestItem.new
|
||||||
test_item.custom_fields = {"a" => "b"}
|
test_item.custom_fields = {"a" => "b"}
|
||||||
test_item.save
|
test_item.save
|
||||||
|
@ -98,12 +96,9 @@ describe HasCustomFields do
|
||||||
|
|
||||||
db_item = CustomFieldsTestItem.find(test_item.id)
|
db_item = CustomFieldsTestItem.find(test_item.id)
|
||||||
expect(db_item.custom_fields).to eq({"a" => "b", "c" => "d"})
|
expect(db_item.custom_fields).to eq({"a" => "b", "c" => "d"})
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
it "handles arrays properly" do
|
it "handles arrays properly" do
|
||||||
|
|
||||||
test_item = CustomFieldsTestItem.new
|
test_item = CustomFieldsTestItem.new
|
||||||
test_item.custom_fields = {"a" => ["b", "c", "d"]}
|
test_item.custom_fields = {"a" => ["b", "c", "d"]}
|
||||||
test_item.save
|
test_item.save
|
||||||
|
@ -125,11 +120,9 @@ describe HasCustomFields do
|
||||||
|
|
||||||
db_item.custom_fields.delete('a')
|
db_item.custom_fields.delete('a')
|
||||||
expect(db_item.custom_fields).to eq({})
|
expect(db_item.custom_fields).to eq({})
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "casts integers in arrays properly without error" do
|
it "casts integers in arrays properly without error" do
|
||||||
|
|
||||||
test_item = CustomFieldsTestItem.new
|
test_item = CustomFieldsTestItem.new
|
||||||
test_item.custom_fields = {"a" => ["b", 10, "d"]}
|
test_item.custom_fields = {"a" => ["b", 10, "d"]}
|
||||||
test_item.save
|
test_item.save
|
||||||
|
@ -137,19 +130,19 @@ describe HasCustomFields do
|
||||||
|
|
||||||
db_item = CustomFieldsTestItem.find(test_item.id)
|
db_item = CustomFieldsTestItem.find(test_item.id)
|
||||||
expect(db_item.custom_fields).to eq({"a" => ["b", "10", "d"]})
|
expect(db_item.custom_fields).to eq({"a" => ["b", "10", "d"]})
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "supportes type coersion" do
|
it "supportes type coersion" do
|
||||||
test_item = CustomFieldsTestItem.new
|
test_item = CustomFieldsTestItem.new
|
||||||
CustomFieldsTestItem.register_custom_field_type("bool", :boolean)
|
CustomFieldsTestItem.register_custom_field_type("bool", :boolean)
|
||||||
CustomFieldsTestItem.register_custom_field_type("int", :integer)
|
CustomFieldsTestItem.register_custom_field_type("int", :integer)
|
||||||
|
CustomFieldsTestItem.register_custom_field_type("json", :json)
|
||||||
|
|
||||||
test_item.custom_fields = {"bool" => true, "int" => 1}
|
test_item.custom_fields = {"bool" => true, "int" => 1, "json" => { "foo" => "bar" }}
|
||||||
test_item.save
|
test_item.save
|
||||||
test_item.reload
|
test_item.reload
|
||||||
|
|
||||||
expect(test_item.custom_fields).to eq({"bool" => true, "int" => 1})
|
expect(test_item.custom_fields).to eq({"bool" => true, "int" => 1, "json" => { "foo" => "bar" }})
|
||||||
end
|
end
|
||||||
|
|
||||||
it "simple modifications don't interfere" do
|
it "simple modifications don't interfere" do
|
||||||
|
|
|
@ -51,7 +51,7 @@ describe CookedPostProcessor do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with image_sizes" do
|
context "with image_sizes" do
|
||||||
let(:post) { build(:post_with_image_urls) }
|
let(:post) { Fabricate(:post_with_image_urls) }
|
||||||
let(:cpp) { CookedPostProcessor.new(post, image_sizes: image_sizes) }
|
let(:cpp) { CookedPostProcessor.new(post, image_sizes: image_sizes) }
|
||||||
|
|
||||||
before { cpp.post_process_images }
|
before { cpp.post_process_images }
|
||||||
|
@ -87,7 +87,7 @@ describe CookedPostProcessor do
|
||||||
|
|
||||||
context "with unsized images" do
|
context "with unsized images" do
|
||||||
|
|
||||||
let(:post) { build(:post_with_unsized_images) }
|
let(:post) { Fabricate(:post_with_unsized_images) }
|
||||||
let(:cpp) { CookedPostProcessor.new(post) }
|
let(:cpp) { CookedPostProcessor.new(post) }
|
||||||
|
|
||||||
it "adds the width and height to images that don't have them" do
|
it "adds the width and height to images that don't have them" do
|
||||||
|
@ -102,7 +102,7 @@ describe CookedPostProcessor do
|
||||||
context "with large images" do
|
context "with large images" do
|
||||||
|
|
||||||
let(:upload) { Fabricate(:upload) }
|
let(:upload) { Fabricate(:upload) }
|
||||||
let(:post) { build(:post_with_large_image) }
|
let(:post) { Fabricate(:post_with_large_image) }
|
||||||
let(:cpp) { CookedPostProcessor.new(post) }
|
let(:cpp) { CookedPostProcessor.new(post) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -129,7 +129,7 @@ describe CookedPostProcessor do
|
||||||
context "with title" do
|
context "with title" do
|
||||||
|
|
||||||
let(:upload) { Fabricate(:upload) }
|
let(:upload) { Fabricate(:upload) }
|
||||||
let(:post) { build(:post_with_large_image_and_title) }
|
let(:post) { Fabricate(:post_with_large_image_and_title) }
|
||||||
let(:cpp) { CookedPostProcessor.new(post) }
|
let(:cpp) { CookedPostProcessor.new(post) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -62,6 +62,14 @@ describe PostCreator do
|
||||||
expect(creator.spam?).to eq(false)
|
expect(creator.spam?).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "triggers extensibility events" do
|
||||||
|
DiscourseEvent.expects(:trigger).with(:before_create_post, anything).once
|
||||||
|
DiscourseEvent.expects(:trigger).with(:validate_post, anything).once
|
||||||
|
DiscourseEvent.expects(:trigger).with(:topic_created, anything, anything, user).once
|
||||||
|
DiscourseEvent.expects(:trigger).with(:post_created, anything, anything, user).once
|
||||||
|
creator.create
|
||||||
|
end
|
||||||
|
|
||||||
it "does not notify on system messages" do
|
it "does not notify on system messages" do
|
||||||
admin = Fabricate(:admin)
|
admin = Fabricate(:admin)
|
||||||
messages = MessageBus.track_publish do
|
messages = MessageBus.track_publish do
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe User do
|
||||||
@user.delete_all_posts!(@guardian)
|
@user.delete_all_posts!(@guardian)
|
||||||
expect(Post.where(id: @posts.map(&:id))).to be_empty
|
expect(Post.where(id: @posts.map(&:id))).to be_empty
|
||||||
@posts.each do |p|
|
@posts.each do |p|
|
||||||
if p.post_number == 1
|
if p.is_first_post?
|
||||||
expect(Topic.find_by(id: p.topic_id)).to be_nil
|
expect(Topic.find_by(id: p.topic_id)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue