DEV: Apply syntax_tree formatting to `plugins/*`

This commit is contained in:
David Taylor 2023-01-06 20:42:16 +00:00
parent 93e2dad656
commit 055310cea4
No known key found for this signature in database
GPG Key ID: 46904C18B1D3F434
110 changed files with 3712 additions and 3158 deletions

View File

@ -5,6 +5,5 @@
--ignore-files=config/* --ignore-files=config/*
--ignore-files=db/* --ignore-files=db/*
--ignore-files=lib/* --ignore-files=lib/*
--ignore-files=plugins/*
--ignore-files=script/* --ignore-files=script/*
--ignore-files=spec/* --ignore-files=spec/*

View File

@ -9,7 +9,10 @@ class Chat::Api::CategoryChatablesController < ApplicationController
Group Group
.joins(:category_groups) .joins(:category_groups)
.where(category_groups: { category_id: category.id }) .where(category_groups: { category_id: category.id })
.where("category_groups.permission_type IN (?)", [CategoryGroup.permission_types[:full], CategoryGroup.permission_types[:create_post]]) .where(
"category_groups.permission_type IN (?)",
[CategoryGroup.permission_types[:full], CategoryGroup.permission_types[:create_post]],
)
.joins("LEFT OUTER JOIN group_users ON groups.id = group_users.group_id") .joins("LEFT OUTER JOIN group_users ON groups.id = group_users.group_id")
.group("groups.id", "groups.name") .group("groups.id", "groups.name")
.pluck("groups.name", "COUNT(group_users.user_id)") .pluck("groups.name", "COUNT(group_users.user_id)")

View File

@ -9,16 +9,17 @@ class Chat::Api::HintsController < ApplicationController
raise Discourse::InvalidParameters.new(:mentions) if group_names.blank? raise Discourse::InvalidParameters.new(:mentions) if group_names.blank?
visible_groups = Group visible_groups =
.where("LOWER(name) IN (?)", group_names) Group.where("LOWER(name) IN (?)", group_names).visible_groups(current_user).pluck(:name)
.visible_groups(current_user)
.pluck(:name)
mentionable_groups = filter_mentionable_groups(visible_groups) mentionable_groups = filter_mentionable_groups(visible_groups)
result = { result = {
unreachable: visible_groups - mentionable_groups.map(&:name), unreachable: visible_groups - mentionable_groups.map(&:name),
over_members_limit: mentionable_groups.select { |g| g.user_count > SiteSetting.max_users_notified_per_group_mention }.map(&:name), over_members_limit:
mentionable_groups
.select { |g| g.user_count > SiteSetting.max_users_notified_per_group_mention }
.map(&:name),
} }
result[:invalid] = (group_names - result[:unreachable]) - result[:over_members_limit] result[:invalid] = (group_names - result[:unreachable]) - result[:over_members_limit]

View File

@ -5,8 +5,9 @@ module Jobs
def execute(args) def execute(args)
return if args[:user_id].nil? return if args[:user_id].nil?
ChatMessageDestroyer.new ChatMessageDestroyer.new.destroy_in_batches(
.destroy_in_batches(ChatMessage.with_deleted.where(user_id: args[:user_id])) ChatMessage.with_deleted.where(user_id: args[:user_id]),
)
end end
end end
end end

View File

@ -15,10 +15,9 @@ module Jobs
return unless valid_day_value?(:chat_channel_retention_days) return unless valid_day_value?(:chat_channel_retention_days)
ChatMessageDestroyer.new.destroy_in_batches( ChatMessageDestroyer.new.destroy_in_batches(
ChatMessage ChatMessage.in_public_channel.with_deleted.created_before(
.in_public_channel SiteSetting.chat_channel_retention_days.days.ago,
.with_deleted ),
.created_before(SiteSetting.chat_channel_retention_days.days.ago)
) )
end end
@ -26,10 +25,9 @@ module Jobs
return unless valid_day_value?(:chat_dm_retention_days) return unless valid_day_value?(:chat_dm_retention_days)
ChatMessageDestroyer.new.destroy_in_batches( ChatMessageDestroyer.new.destroy_in_batches(
ChatMessage ChatMessage.in_dm_channel.with_deleted.created_before(
.in_dm_channel SiteSetting.chat_dm_retention_days.days.ago,
.with_deleted ),
.created_before(SiteSetting.chat_dm_retention_days.days.ago)
) )
end end

View File

@ -89,7 +89,6 @@ class ChatChannel < ActiveRecord::Base
# TODO (martin) Move UpdateUserCountsForChatChannels into here # TODO (martin) Move UpdateUserCountsForChatChannels into here
def self.update_counts def self.update_counts
# NOTE: ChatChannel#messages_count is not updated every time # NOTE: ChatChannel#messages_count is not updated every time
# a message is created or deleted in a channel, so it should not # a message is created or deleted in a channel, so it should not
# be displayed in the UI. It is updated eventually via Jobs::ChatPeriodicalUpdates # be displayed in the UI. It is updated eventually via Jobs::ChatPeriodicalUpdates

View File

@ -2,11 +2,13 @@
class ChatMessageDestroyer class ChatMessageDestroyer
def destroy_in_batches(chat_messages_query, batch_size: 200) def destroy_in_batches(chat_messages_query, batch_size: 200)
chat_messages_query.in_batches(of: batch_size).each do |relation| chat_messages_query
destroyed_ids = relation.destroy_all.pluck(:id) .in_batches(of: batch_size)
reset_last_read(destroyed_ids) .each do |relation|
delete_flags(destroyed_ids) destroyed_ids = relation.destroy_all.pluck(:id)
end reset_last_read(destroyed_ids)
delete_flags(destroyed_ids)
end
end end
private private

View File

@ -2,10 +2,12 @@
class SaveChatAllowedGroupsSiteSetting < ActiveRecord::Migration[7.0] class SaveChatAllowedGroupsSiteSetting < ActiveRecord::Migration[7.0]
def up def up
chat_enabled = DB.query_single("SELECT value FROM site_settings WHERE name = 'chat_enabled' AND value = 't'") chat_enabled =
DB.query_single("SELECT value FROM site_settings WHERE name = 'chat_enabled' AND value = 't'")
return if chat_enabled.blank? return if chat_enabled.blank?
chat_allowed_groups = DB.query_single("SELECT value FROM site_settings WHERE name = 'chat_allowed_groups'") chat_allowed_groups =
DB.query_single("SELECT value FROM site_settings WHERE name = 'chat_allowed_groups'")
return if chat_allowed_groups.present? return if chat_allowed_groups.present?
# The original default was auto group ID 3 (staff) so we are # The original default was auto group ID 3 (staff) so we are

View File

@ -30,10 +30,14 @@ module Chat::ChatChannelFetcher
end end
def self.generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: false) def self.generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: false)
category_channel_sql = Category.post_create_allowed(guardian) category_channel_sql =
.joins("INNER JOIN chat_channels ON chat_channels.chatable_id = categories.id AND chat_channels.chatable_type = 'Category'") Category
.select("chat_channels.id") .post_create_allowed(guardian)
.to_sql .joins(
"INNER JOIN chat_channels ON chat_channels.chatable_id = categories.id AND chat_channels.chatable_type = 'Category'",
)
.select("chat_channels.id")
.to_sql
dm_channel_sql = "" dm_channel_sql = ""
if !exclude_dm_channels if !exclude_dm_channels
dm_channel_sql = <<~SQL dm_channel_sql = <<~SQL
@ -75,8 +79,7 @@ module Chat::ChatChannelFetcher
end end
def self.secured_public_channel_search(guardian, options = {}) def self.secured_public_channel_search(guardian, options = {})
allowed_channel_ids = allowed_channel_ids = generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true)
generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true)
channels = ChatChannel.includes(chatable: [:topic_only_relative_url]) channels = ChatChannel.includes(chatable: [:topic_only_relative_url])
channels = channels.includes(:chat_channel_archive) if options[:include_archives] channels = channels.includes(:chat_channel_archive) if options[:include_archives]

View File

@ -32,10 +32,11 @@ class Chat::ChatMailer
when_away_frequency = UserOption.chat_email_frequencies[:when_away] when_away_frequency = UserOption.chat_email_frequencies[:when_away]
allowed_group_ids = Chat.allowed_group_ids allowed_group_ids = Chat.allowed_group_ids
users = User users =
.joins(:user_option) User
.where(user_options: { chat_enabled: true, chat_email_frequency: when_away_frequency }) .joins(:user_option)
.where("users.last_seen_at < ?", 15.minutes.ago) .where(user_options: { chat_enabled: true, chat_email_frequency: when_away_frequency })
.where("users.last_seen_at < ?", 15.minutes.ago)
if !allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone]) if !allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
users = users.joins(:groups).where(groups: { id: allowed_group_ids }) users = users.joins(:groups).where(groups: { id: allowed_group_ids })

View File

@ -41,7 +41,7 @@ class Chat::ChatNotifier
:send_message_notifications, :send_message_notifications,
chat_message_id: chat_message.id, chat_message_id: chat_message.id,
timestamp: timestamp.iso8601(6), timestamp: timestamp.iso8601(6),
reason: :edit reason: :edit,
) )
end end
@ -50,7 +50,7 @@ class Chat::ChatNotifier
:send_message_notifications, :send_message_notifications,
chat_message_id: chat_message.id, chat_message_id: chat_message.id,
timestamp: timestamp.iso8601(6), timestamp: timestamp.iso8601(6),
reason: :new reason: :new,
) )
end end
end end
@ -112,8 +112,7 @@ class Chat::ChatNotifier
group_mentions_count = group_name_mentions.length group_mentions_count = group_name_mentions.length
skip_notifications = skip_notifications =
(direct_mentions_count + group_mentions_count) > (direct_mentions_count + group_mentions_count) > SiteSetting.max_mentions_per_chat_message
SiteSetting.max_mentions_per_chat_message
{}.tap do |to_notify| {}.tap do |to_notify|
# The order of these methods is the precedence # The order of these methods is the precedence
@ -248,24 +247,21 @@ class Chat::ChatNotifier
end end
def visible_groups def visible_groups
@visible_groups ||= @visible_groups ||= Group.where("LOWER(name) IN (?)", group_name_mentions).visible_groups(@user)
Group
.where("LOWER(name) IN (?)", group_name_mentions)
.visible_groups(@user)
end end
def expand_group_mentions(to_notify, already_covered_ids, skip) def expand_group_mentions(to_notify, already_covered_ids, skip)
return [] if skip || visible_groups.empty? return [] if skip || visible_groups.empty?
mentionable_groups = Group mentionable_groups =
.mentionable(@user, include_public: false) Group.mentionable(@user, include_public: false).where(id: visible_groups.map(&:id))
.where(id: visible_groups.map(&:id))
mentions_disabled = visible_groups - mentionable_groups mentions_disabled = visible_groups - mentionable_groups
too_many_members, mentionable = mentionable_groups.partition do |group| too_many_members, mentionable =
group.user_count > SiteSetting.max_users_notified_per_group_mention mentionable_groups.partition do |group|
end group.user_count > SiteSetting.max_users_notified_per_group_mention
end
to_notify[:group_mentions_disabled] = mentions_disabled to_notify[:group_mentions_disabled] = mentions_disabled
to_notify[:too_many_members] = too_many_members to_notify[:too_many_members] = too_many_members
@ -275,7 +271,9 @@ class Chat::ChatNotifier
reached_by_group = reached_by_group =
chat_users chat_users
.includes(:groups) .includes(:groups)
.joins(:groups).where(groups: mentionable).where.not(id: already_covered_ids) .joins(:groups)
.where(groups: mentionable)
.where.not(id: already_covered_ids)
grouped = group_users_to_notify(reached_by_group) grouped = group_users_to_notify(reached_by_group)
@ -295,7 +293,13 @@ class Chat::ChatNotifier
end end
def notify_creator_of_inaccessible_mentions(to_notify) def notify_creator_of_inaccessible_mentions(to_notify)
inaccessible = to_notify.extract!(:unreachable, :welcome_to_join, :too_many_members, :group_mentions_disabled) inaccessible =
to_notify.extract!(
:unreachable,
:welcome_to_join,
:too_many_members,
:group_mentions_disabled,
)
return if inaccessible.values.all?(&:blank?) return if inaccessible.values.all?(&:blank?)
ChatPublisher.publish_inaccessible_mentions( ChatPublisher.publish_inaccessible_mentions(
@ -304,7 +308,7 @@ class Chat::ChatNotifier
inaccessible[:unreachable].to_a, inaccessible[:unreachable].to_a,
inaccessible[:welcome_to_join].to_a, inaccessible[:welcome_to_join].to_a,
inaccessible[:too_many_members].to_a, inaccessible[:too_many_members].to_a,
inaccessible[:group_mentions_disabled].to_a inaccessible[:group_mentions_disabled].to_a,
) )
end end
@ -321,9 +325,7 @@ class Chat::ChatNotifier
to_notify to_notify
.except(:unreachable, :welcome_to_join) .except(:unreachable, :welcome_to_join)
.each do |key, user_ids| .each do |key, user_ids|
to_notify[key] = user_ids.reject do |user_id| to_notify[key] = user_ids.reject { |user_id| screener.ignoring_or_muting_actor?(user_id) }
screener.ignoring_or_muting_actor?(user_id)
end
end end
# :welcome_to_join contains users because it's serialized by MB. # :welcome_to_join contains users because it's serialized by MB.
@ -351,11 +353,7 @@ class Chat::ChatNotifier
def notify_watching_users(except: []) def notify_watching_users(except: [])
Jobs.enqueue( Jobs.enqueue(
:chat_notify_watching, :chat_notify_watching,
{ { chat_message_id: @chat_message.id, except_user_ids: except, timestamp: @timestamp },
chat_message_id: @chat_message.id,
except_user_ids: except,
timestamp: @timestamp,
},
) )
end end
end end

View File

@ -22,11 +22,11 @@ class Chat::DuplicateMessageValidator
# Check if the same duplicate message has been posted in the last N seconds by any user # Check if the same duplicate message has been posted in the last N seconds by any user
if !chat_message if !chat_message
.chat_channel .chat_channel
.chat_messages .chat_messages
.where("created_at > ?", matrix[:min_past_seconds].seconds.ago) .where("created_at > ?", matrix[:min_past_seconds].seconds.ago)
.where(message: chat_message.message) .where(message: chat_message.message)
.exists? .exists?
return return
end end

View File

@ -85,27 +85,27 @@ module Chat::UserNotificationsExtension
"user_notifications.chat_summary.subject.chat_channel_more", "user_notifications.chat_summary.subject.chat_channel_more",
email_prefix: @email_prefix, email_prefix: @email_prefix,
channel: channels.first.title, channel: channels.first.title,
count: total_count - 1 count: total_count - 1,
) )
elsif channels.size == 1 && dm_users.size == 0 elsif channels.size == 1 && dm_users.size == 0
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.chat_channel_1", "user_notifications.chat_summary.subject.chat_channel_1",
email_prefix: @email_prefix, email_prefix: @email_prefix,
channel: channels.first.title channel: channels.first.title,
) )
elsif channels.size == 1 && dm_users.size == 1 elsif channels.size == 1 && dm_users.size == 1
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.chat_channel_and_direct_message", "user_notifications.chat_summary.subject.chat_channel_and_direct_message",
email_prefix: @email_prefix, email_prefix: @email_prefix,
channel: channels.first.title, channel: channels.first.title,
username: dm_users.first.username username: dm_users.first.username,
) )
elsif channels.size == 2 elsif channels.size == 2
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.chat_channel_2", "user_notifications.chat_summary.subject.chat_channel_2",
email_prefix: @email_prefix, email_prefix: @email_prefix,
channel1: channels.first.title, channel1: channels.first.title,
channel2: channels.second.title channel2: channels.second.title,
) )
end end
end end
@ -116,21 +116,21 @@ module Chat::UserNotificationsExtension
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.direct_message_from_1", "user_notifications.chat_summary.subject.direct_message_from_1",
email_prefix: @email_prefix, email_prefix: @email_prefix,
username: dm_users.first.username username: dm_users.first.username,
) )
when 2 when 2
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.direct_message_from_2", "user_notifications.chat_summary.subject.direct_message_from_2",
email_prefix: @email_prefix, email_prefix: @email_prefix,
username1: dm_users.first.username, username1: dm_users.first.username,
username2: dm_users.second.username username2: dm_users.second.username,
) )
else else
I18n.t( I18n.t(
"user_notifications.chat_summary.subject.direct_message_from_more", "user_notifications.chat_summary.subject.direct_message_from_more",
email_prefix: @email_prefix, email_prefix: @email_prefix,
username: dm_users.first.username, username: dm_users.first.username,
count: dm_users.size - 1 count: dm_users.size - 1,
) )
end end
end end

View File

@ -32,10 +32,7 @@ describe Chat::ChatMessageCreator do
) )
end end
let(:direct_message_channel) do let(:direct_message_channel) do
Chat::DirectMessageChannelCreator.create!( Chat::DirectMessageChannelCreator.create!(acting_user: user1, target_users: [user1, user2])
acting_user: user1,
target_users: [user1, user2],
)
end end
before do before do
@ -135,13 +132,14 @@ describe Chat::ChatMessageCreator do
end end
it "publishes a DiscourseEvent for new messages" do it "publishes a DiscourseEvent for new messages" do
events = DiscourseEvent.track_events { events =
Chat::ChatMessageCreator.create( DiscourseEvent.track_events do
chat_channel: public_chat_channel, Chat::ChatMessageCreator.create(
user: user1, chat_channel: public_chat_channel,
content: "this is a message", user: user1,
) content: "this is a message",
} )
end
expect(events.map { _1[:event_name] }).to include(:chat_message_created) expect(events.map { _1[:event_name] }).to include(:chat_message_created)
end end
@ -368,8 +366,8 @@ describe Chat::ChatMessageCreator do
content: "hello @#{admin_group.name}", content: "hello @#{admin_group.name}",
) )
}.to change { admin1.chat_mentions.count }.by(1).and change { }.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count admin2.chat_mentions.count
}.by(1) }.by(1)
end end
it "doesn't mention users twice if they are direct mentioned and group mentioned" do it "doesn't mention users twice if they are direct mentioned and group mentioned" do
@ -380,8 +378,8 @@ describe Chat::ChatMessageCreator do
content: "hello @#{admin_group.name} @#{admin1.username} and @#{admin2.username}", content: "hello @#{admin_group.name} @#{admin1.username} and @#{admin2.username}",
) )
}.to change { admin1.chat_mentions.count }.by(1).and change { }.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count admin2.chat_mentions.count
}.by(1) }.by(1)
end end
it "creates chat mentions for group mentions and direct mentions" do it "creates chat mentions for group mentions and direct mentions" do
@ -392,8 +390,8 @@ describe Chat::ChatMessageCreator do
content: "hello @#{admin_group.name} @#{user2.username}", content: "hello @#{admin_group.name} @#{user2.username}",
) )
}.to change { admin1.chat_mentions.count }.by(1).and change { }.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count admin2.chat_mentions.count
}.by(1).and change { user2.chat_mentions.count }.by(1) }.by(1).and change { user2.chat_mentions.count }.by(1)
end end
it "creates chat mentions for group mentions and direct mentions" do it "creates chat mentions for group mentions and direct mentions" do
@ -404,10 +402,10 @@ describe Chat::ChatMessageCreator do
content: "hello @#{admin_group.name} @#{user_group.name}", content: "hello @#{admin_group.name} @#{user_group.name}",
) )
}.to change { admin1.chat_mentions.count }.by(1).and change { }.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count admin2.chat_mentions.count
}.by(1).and change { user2.chat_mentions.count }.by(1).and change { }.by(1).and change { user2.chat_mentions.count }.by(1).and change {
user3.chat_mentions.count user3.chat_mentions.count
}.by(1) }.by(1)
end end
it "doesn't create chat mentions for group mentions where the group is un-mentionable" do it "doesn't create chat mentions for group mentions where the group is un-mentionable" do
@ -475,8 +473,8 @@ describe Chat::ChatMessageCreator do
upload_ids: [upload1.id, upload2.id], upload_ids: [upload1.id, upload2.id],
) )
}.to change { ChatUpload.where(upload_id: upload1.id).count }.by(1).and change { }.to change { ChatUpload.where(upload_id: upload1.id).count }.by(1).and change {
ChatUpload.where(upload_id: upload2.id).count ChatUpload.where(upload_id: upload2.id).count
}.by(1) }.by(1)
end end
it "filters out uploads that weren't uploaded by the user" do it "filters out uploads that weren't uploaded by the user" do

View File

@ -64,11 +64,11 @@ describe Chat::ChatMessageRateLimiter do
limiter.run! limiter.run!
expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded).and change { expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded).and change {
UserHistory.where( UserHistory.where(
target_user: user, target_user: user,
acting_user: Discourse.system_user, acting_user: Discourse.system_user,
action: UserHistory.actions[:silence_user], action: UserHistory.actions[:silence_user],
).count ).count
}.by(1) }.by(1)
end end
end end

View File

@ -76,9 +76,7 @@ end
Fabricator(:chat_upload) do Fabricator(:chat_upload) do
transient :user transient :user
user do user { Fabricate(:user) }
Fabricate(:user)
end
chat_message { |attrs| Fabricate(:chat_message, user: attrs[:user]) } chat_message { |attrs| Fabricate(:chat_message, user: attrs[:user]) }
upload { |attrs| Fabricate(:upload, user: attrs[:user]) } upload { |attrs| Fabricate(:upload, user: attrs[:user]) }

View File

@ -219,9 +219,19 @@ martin</div>
channel = Fabricate(:chat_channel) channel = Fabricate(:chat_channel)
message1 = Fabricate(:chat_message, chat_channel: channel, user: post.user) message1 = Fabricate(:chat_message, chat_channel: channel, user: post.user)
message2 = Fabricate(:chat_message, chat_channel: channel, user: post.user) message2 = Fabricate(:chat_message, chat_channel: channel, user: post.user)
md = ChatTranscriptService.new(channel, message2.user, messages_or_ids: [message2.id]).generate_markdown md =
ChatTranscriptService.new(
channel,
message2.user,
messages_or_ids: [message2.id],
).generate_markdown
message1.update!(message: md) message1.update!(message: md)
md_for_post = ChatTranscriptService.new(channel, message1.user, messages_or_ids: [message1.id]).generate_markdown md_for_post =
ChatTranscriptService.new(
channel,
message1.user,
messages_or_ids: [message1.id],
).generate_markdown
post.update!(raw: md_for_post) post.update!(raw: md_for_post)
expect(post.cooked.chomp).to eq(<<~COOKED.chomp) expect(post.cooked.chomp).to eq(<<~COOKED.chomp)
<div class="chat-transcript" data-message-id="#{message1.id}" data-username="#{message1.user.username}" data-datetime="#{message1.created_at.iso8601}" data-channel-name="#{channel.name}" data-channel-id="#{channel.id}"> <div class="chat-transcript" data-message-id="#{message1.id}" data-username="#{message1.user.username}" data-datetime="#{message1.created_at.iso8601}" data-channel-name="#{channel.name}" data-channel-id="#{channel.id}">

View File

@ -56,23 +56,23 @@ describe Jobs::ChatChannelDelete do
expect { described_class.new.execute(chat_channel_id: chat_channel.id) }.to change { expect { described_class.new.execute(chat_channel_id: chat_channel.id) }.to change {
IncomingChatWebhook.where(chat_channel_id: chat_channel.id).count IncomingChatWebhook.where(chat_channel_id: chat_channel.id).count
}.by(-1).and change { }.by(-1).and change {
ChatWebhookEvent.where(incoming_chat_webhook_id: @incoming_chat_webhook_id).count ChatWebhookEvent.where(incoming_chat_webhook_id: @incoming_chat_webhook_id).count
}.by(-1).and change { ChatDraft.where(chat_channel: chat_channel).count }.by( }.by(-1).and change { ChatDraft.where(chat_channel: chat_channel).count }.by(
-1, -1,
).and change { ).and change {
UserChatChannelMembership.where(chat_channel: chat_channel).count UserChatChannelMembership.where(chat_channel: chat_channel).count
}.by(-3).and change { }.by(-3).and change {
ChatMessageRevision.where(chat_message_id: @message_ids).count ChatMessageRevision.where(chat_message_id: @message_ids).count
}.by(-1).and change { }.by(-1).and change {
ChatMention.where(chat_message_id: @message_ids).count ChatMention.where(chat_message_id: @message_ids).count
}.by(-1).and change { }.by(-1).and change {
ChatUpload.where(chat_message_id: @message_ids).count ChatUpload.where(chat_message_id: @message_ids).count
}.by(-10).and change { }.by(-10).and change {
ChatMessage.where(id: @message_ids).count ChatMessage.where(id: @message_ids).count
}.by(-20).and change { }.by(-20).and change {
ChatMessageReaction.where( ChatMessageReaction.where(
chat_message_id: @message_ids, chat_message_id: @message_ids,
).count ).count
}.by(-10) }.by(-10)
end end
end end

View File

@ -21,7 +21,7 @@ RSpec.describe Jobs::SendMessageNotifications do
subject.execute( subject.execute(
chat_message_id: chat_message.id, chat_message_id: chat_message.id,
reason: "invalid", reason: "invalid",
timestamp: 1.minute.ago timestamp: 1.minute.ago,
) )
end end
@ -29,32 +29,21 @@ RSpec.describe Jobs::SendMessageNotifications do
Chat::ChatNotifier.any_instance.expects(:notify_new).never Chat::ChatNotifier.any_instance.expects(:notify_new).never
Chat::ChatNotifier.any_instance.expects(:notify_edit).never Chat::ChatNotifier.any_instance.expects(:notify_edit).never
subject.execute( subject.execute(chat_message_id: chat_message.id, reason: "new")
chat_message_id: chat_message.id,
reason: "new"
)
end end
it "calls notify_new when the reason is 'new'" do it "calls notify_new when the reason is 'new'" do
Chat::ChatNotifier.any_instance.expects(:notify_new).once Chat::ChatNotifier.any_instance.expects(:notify_new).once
Chat::ChatNotifier.any_instance.expects(:notify_edit).never Chat::ChatNotifier.any_instance.expects(:notify_edit).never
subject.execute( subject.execute(chat_message_id: chat_message.id, reason: "new", timestamp: 1.minute.ago)
chat_message_id: chat_message.id,
reason: "new",
timestamp: 1.minute.ago
)
end end
it "calls notify_edit when the reason is 'edit'" do it "calls notify_edit when the reason is 'edit'" do
Chat::ChatNotifier.any_instance.expects(:notify_new).never Chat::ChatNotifier.any_instance.expects(:notify_new).never
Chat::ChatNotifier.any_instance.expects(:notify_edit).once Chat::ChatNotifier.any_instance.expects(:notify_edit).once
subject.execute( subject.execute(chat_message_id: chat_message.id, reason: "edit", timestamp: 1.minute.ago)
chat_message_id: chat_message.id,
reason: "edit",
timestamp: 1.minute.ago
)
end end
end end
end end

View File

@ -142,17 +142,38 @@ describe Chat::ChatChannelFetcher do
fab!(:group_user) { Fabricate(:group_user, group: group, user: user1) } fab!(:group_user) { Fabricate(:group_user, group: group, user: user1) }
it "does not include the category channel for member of group with readonly access" do it "does not include the category channel for member of group with readonly access" do
category_channel.update!(chatable: Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:readonly])) category_channel.update!(
chatable:
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:readonly],
),
)
expect(subject.all_secured_channel_ids(guardian)).to be_empty expect(subject.all_secured_channel_ids(guardian)).to be_empty
end end
it "includes the category channel for member of group with create_post access" do it "includes the category channel for member of group with create_post access" do
category_channel.update!(chatable: Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:create_post])) category_channel.update!(
chatable:
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:create_post],
),
)
expect(subject.all_secured_channel_ids(guardian)).to match_array([category_channel.id]) expect(subject.all_secured_channel_ids(guardian)).to match_array([category_channel.id])
end end
it "includes the category channel for member of group with full access" do it "includes the category channel for member of group with full access" do
category_channel.update!(chatable: Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:full])) category_channel.update!(
chatable:
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:full],
),
)
expect(subject.all_secured_channel_ids(guardian)).to match_array([category_channel.id]) expect(subject.all_secured_channel_ids(guardian)).to match_array([category_channel.id])
end end
end end

View File

@ -9,7 +9,7 @@ describe Chat::ChatMessageReactor do
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, user: reacting_user) } fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, user: reacting_user) }
let(:subject) { described_class.new(reacting_user, channel) } let(:subject) { described_class.new(reacting_user, channel) }
it 'calls guardian ensure_can_join_chat_channel!' do it "calls guardian ensure_can_join_chat_channel!" do
Guardian.any_instance.expects(:ensure_can_join_chat_channel!).once Guardian.any_instance.expects(:ensure_can_join_chat_channel!).once
subject.react!(message_id: message_1.id, react_action: :add, emoji: ":+1:") subject.react!(message_id: message_1.id, react_action: :add, emoji: ":+1:")
end end

View File

@ -275,7 +275,7 @@ describe Chat::ChatNotifier do
include_examples "ensure only channel members are notified" include_examples "ensure only channel members are notified"
it 'calls guardian can_join_chat_channel?' do it "calls guardian can_join_chat_channel?" do
Guardian.any_instance.expects(:can_join_chat_channel?).at_least_once Guardian.any_instance.expects(:can_join_chat_channel?).at_least_once
msg = build_cooked_msg("Hello @#{group.name} and @#{user_2.username}", user_1) msg = build_cooked_msg("Hello @#{group.name} and @#{user_2.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new to_notify = described_class.new(msg, msg.created_at).notify_new
@ -463,7 +463,8 @@ describe Chat::ChatNotifier do
expect(not_participating_msg).to be_present expect(not_participating_msg).to be_present
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg.data[:cannot_see]).to be_empty
not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } not_participating_users =
not_participating_msg.data[:without_membership].map { |u| u["id"] }
expect(not_participating_users).to contain_exactly(user_3.id) expect(not_participating_users).to contain_exactly(user_3.id)
end end
@ -515,7 +516,8 @@ describe Chat::ChatNotifier do
expect(not_participating_msg).to be_present expect(not_participating_msg).to be_present
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg.data[:cannot_see]).to be_empty
not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } not_participating_users =
not_participating_msg.data[:without_membership].map { |u| u["id"] }
expect(not_participating_users).to contain_exactly(user_3.id) expect(not_participating_users).to contain_exactly(user_3.id)
end end
@ -539,7 +541,8 @@ describe Chat::ChatNotifier do
expect(not_participating_msg).to be_present expect(not_participating_msg).to be_present
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg.data[:cannot_see]).to be_empty
not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } not_participating_users =
not_participating_msg.data[:without_membership].map { |u| u["id"] }
expect(not_participating_users).to contain_exactly(user_3.id) expect(not_participating_users).to contain_exactly(user_3.id)
end end
@ -598,11 +601,12 @@ describe Chat::ChatNotifier do
SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1) SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1)
msg = build_cooked_msg("Hello @#{group.name}", user_1) msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages = MessageBus.track_publish("/chat/#{channel.id}") do messages =
to_notify = described_class.new(msg, msg.created_at).notify_new MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil expect(to_notify[group.name]).to be_nil
end end
too_many_members_msg = messages.first too_many_members_msg = messages.first
expect(too_many_members_msg).to be_present expect(too_many_members_msg).to be_present
@ -614,11 +618,12 @@ describe Chat::ChatNotifier do
group.update!(mentionable_level: Group::ALIAS_LEVELS[:only_admins]) group.update!(mentionable_level: Group::ALIAS_LEVELS[:only_admins])
msg = build_cooked_msg("Hello @#{group.name}", user_1) msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages = MessageBus.track_publish("/chat/#{channel.id}") do messages =
to_notify = described_class.new(msg, msg.created_at).notify_new MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil expect(to_notify[group.name]).to be_nil
end end
mentions_disabled_msg = messages.first mentions_disabled_msg = messages.first
expect(mentions_disabled_msg).to be_present expect(mentions_disabled_msg).to be_present

View File

@ -46,7 +46,7 @@ describe Chat::ChatReviewQueue do
it "returns an error" do it "returns an error" do
expect(second_flag_result).to include success: false, expect(second_flag_result).to include success: false,
errors: [I18n.t("chat.reviewables.message_already_handled")] errors: [I18n.t("chat.reviewables.message_already_handled")]
end end
it "returns an error when trying to use notify_moderators and the previous flag is still pending" do it "returns an error when trying to use notify_moderators and the previous flag is still pending" do
@ -59,7 +59,7 @@ describe Chat::ChatReviewQueue do
) )
expect(notify_moderators_result).to include success: false, expect(notify_moderators_result).to include success: false,
errors: [I18n.t("chat.reviewables.message_already_handled")] errors: [I18n.t("chat.reviewables.message_already_handled")]
end end
end end
@ -87,7 +87,7 @@ describe Chat::ChatReviewQueue do
queue.flag_message(message, admin_guardian, ReviewableScore.types[:spam]) queue.flag_message(message, admin_guardian, ReviewableScore.types[:spam])
expect(second_flag_result).to include success: false, expect(second_flag_result).to include success: false,
errors: [I18n.t("chat.reviewables.message_already_handled")] errors: [I18n.t("chat.reviewables.message_already_handled")]
end end
end end
@ -105,7 +105,7 @@ describe Chat::ChatReviewQueue do
it "raises an error when we are inside the cooldown window" do it "raises an error when we are inside the cooldown window" do
expect(second_flag_result).to include success: false, expect(second_flag_result).to include success: false,
errors: [I18n.t("chat.reviewables.message_already_handled")] errors: [I18n.t("chat.reviewables.message_already_handled")]
end end
it "allows the user to re-flag after the cooldown period" do it "allows the user to re-flag after the cooldown period" do

View File

@ -92,17 +92,32 @@ RSpec.describe Chat::GuardianExtensions do
fab!(:group_user) { Fabricate(:group_user, group: group, user: user) } fab!(:group_user) { Fabricate(:group_user, group: group, user: user) }
it "returns true if the user can join the category" do it "returns true if the user can join the category" do
category = Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:readonly]) category =
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:readonly],
)
channel.update(chatable: category) channel.update(chatable: category)
guardian = Guardian.new(user) guardian = Guardian.new(user)
expect(guardian.can_join_chat_channel?(channel)).to eq(false) expect(guardian.can_join_chat_channel?(channel)).to eq(false)
category = Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:create_post]) category =
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:create_post],
)
channel.update(chatable: category) channel.update(chatable: category)
guardian = Guardian.new(user) guardian = Guardian.new(user)
expect(guardian.can_join_chat_channel?(channel)).to eq(true) expect(guardian.can_join_chat_channel?(channel)).to eq(true)
category = Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:full]) category =
Fabricate(
:private_category,
group: group,
permission_type: CategoryGroup.permission_types[:full],
)
channel.update(chatable: category) channel.update(chatable: category)
guardian = Guardian.new(user) guardian = Guardian.new(user)
expect(guardian.can_join_chat_channel?(channel)).to eq(true) expect(guardian.can_join_chat_channel?(channel)).to eq(true)

View File

@ -25,7 +25,7 @@ describe UserNotifications do
Chat::DirectMessageChannelCreator.create!(acting_user: sender, target_users: [sender, user]) Chat::DirectMessageChannelCreator.create!(acting_user: sender, target_users: [sender, user])
end end
it 'calls guardian can_join_chat_channel?' do it "calls guardian can_join_chat_channel?" do
Fabricate(:chat_message, user: sender, chat_channel: channel) Fabricate(:chat_message, user: sender, chat_channel: channel)
Guardian.any_instance.expects(:can_join_chat_channel?).once Guardian.any_instance.expects(:can_join_chat_channel?).once
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
@ -34,11 +34,12 @@ describe UserNotifications do
describe "email subject" do describe "email subject" do
it "includes the sender username in the subject" do it "includes the sender username in the subject" do
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.direct_message_from_1", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.direct_message_from_1",
username: sender.username email_prefix: SiteSetting.title,
) username: sender.username,
)
Fabricate(:chat_message, user: sender, chat_channel: channel) Fabricate(:chat_message, user: sender, chat_channel: channel)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
@ -54,11 +55,12 @@ describe UserNotifications do
chat_channel: channel, chat_channel: channel,
) )
DirectMessageUser.create!(direct_message: channel.chatable, user: another_participant) DirectMessageUser.create!(direct_message: channel.chatable, user: another_participant)
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.direct_message_from_1", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.direct_message_from_1",
username: sender.username email_prefix: SiteSetting.title,
) username: sender.username,
)
Fabricate(:chat_message, user: sender, chat_channel: channel) Fabricate(:chat_message, user: sender, chat_channel: channel)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
@ -80,12 +82,13 @@ describe UserNotifications do
Fabricate(:chat_message, user: sender, chat_channel: channel) Fabricate(:chat_message, user: sender, chat_channel: channel)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.direct_message_from_2", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.direct_message_from_2",
username1: another_dm_user.username, email_prefix: SiteSetting.title,
username2: sender.username username1: another_dm_user.username,
) username2: sender.username,
)
expect(email.subject).to eq(expected_subject) expect(email.subject).to eq(expected_subject)
expect(email.subject).to include(sender.username) expect(email.subject).to include(sender.username)
@ -116,12 +119,13 @@ describe UserNotifications do
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.direct_message_from_more", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.direct_message_from_more",
username: senders.first.username, email_prefix: SiteSetting.title,
count: 2 username: senders.first.username,
) count: 2,
)
expect(email.subject).to eq(expected_subject) expect(email.subject).to eq(expected_subject)
end end
@ -162,11 +166,12 @@ describe UserNotifications do
before { Fabricate(:chat_mention, user: user, chat_message: chat_message) } before { Fabricate(:chat_mention, user: user, chat_message: chat_message) }
it "includes the sender username in the subject" do it "includes the sender username in the subject" do
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.chat_channel_1", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.chat_channel_1",
channel: channel.title(user) email_prefix: SiteSetting.title,
) channel: channel.title(user),
)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
@ -193,12 +198,13 @@ describe UserNotifications do
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.chat_channel_2", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.chat_channel_2",
channel1: channel.title(user), email_prefix: SiteSetting.title,
channel2: another_chat_channel.title(user) channel1: channel.title(user),
) channel2: another_chat_channel.title(user),
)
expect(email.subject).to eq(expected_subject) expect(email.subject).to eq(expected_subject)
expect(email.subject).to include(channel.title(user)) expect(email.subject).to include(channel.title(user))
@ -224,12 +230,13 @@ describe UserNotifications do
Fabricate(:chat_mention, user: user, chat_message: another_chat_message) Fabricate(:chat_mention, user: user, chat_message: another_chat_message)
end end
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.chat_channel_more", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.chat_channel_more",
channel: channel.title(user), email_prefix: SiteSetting.title,
count: 2 channel: channel.title(user),
) count: 2,
)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})
@ -250,12 +257,13 @@ describe UserNotifications do
end end
it "always includes the DM second" do it "always includes the DM second" do
expected_subject = I18n.t( expected_subject =
"user_notifications.chat_summary.subject.chat_channel_and_direct_message", I18n.t(
email_prefix: SiteSetting.title, "user_notifications.chat_summary.subject.chat_channel_and_direct_message",
channel: channel.title(user), email_prefix: SiteSetting.title,
username: sender.username channel: channel.title(user),
) username: sender.username,
)
email = described_class.chat_summary(user, {}) email = described_class.chat_summary(user, {})

View File

@ -517,7 +517,8 @@ describe ChatMessage do
it "keeps the same hashtags the user has permission to after rebake" do it "keeps the same hashtags the user has permission to after rebake" do
group.add(chat_message.user) group.add(chat_message.user)
chat_message.update!( chat_message.update!(
message: "this is the message ##{category.slug} ##{secure_category.slug} ##{chat_message.chat_channel.slug}", message:
"this is the message ##{category.slug} ##{secure_category.slug} ##{chat_message.chat_channel.slug}",
) )
chat_message.cook chat_message.cook
chat_message.save! chat_message.save!

View File

@ -11,9 +11,7 @@ describe DeletedChatUser do
describe "#avatar_template" do describe "#avatar_template" do
it "returns a default path" do it "returns a default path" do
expect(subject.avatar_template).to eq( expect(subject.avatar_template).to eq("/plugins/chat/images/deleted-chat-user-avatar.png")
"/plugins/chat/images/deleted-chat-user-avatar.png",
)
end end
end end
end end

View File

@ -12,11 +12,7 @@ RSpec.describe Chat::Api::ChatChannelsCurrentUserNotificationsSettingsController
include_examples "channel access example", include_examples "channel access example",
:put, :put,
"/notifications-settings/me", "/notifications-settings/me",
{ { notifications_settings: { muted: true } }
notifications_settings: {
muted: true,
},
}
context "when category channel has invalid params" do context "when category channel has invalid params" do
fab!(:channel_1) { Fabricate(:category_channel) } fab!(:channel_1) { Fabricate(:category_channel) }

View File

@ -61,9 +61,9 @@ RSpec.describe Chat::Api::ChatChannelsStatusController do
context "when changing from open to closed" do context "when changing from open to closed" do
it "changes the status" do it "changes the status" do
expect { put "/chat/api/channels/#{channel_1.id}/status", params: status("closed") }.to change { expect {
channel_1.reload.status put "/chat/api/channels/#{channel_1.id}/status", params: status("closed")
}.to("closed").from("open") }.to change { channel_1.reload.status }.to("closed").from("open")
expect(response.status).to eq(200) expect(response.status).to eq(200)
channel = response.parsed_body["channel"] channel = response.parsed_body["channel"]
@ -75,9 +75,9 @@ RSpec.describe Chat::Api::ChatChannelsStatusController do
before { channel_1.update!(status: "closed") } before { channel_1.update!(status: "closed") }
it "changes the status" do it "changes the status" do
expect { put "/chat/api/channels/#{channel_1.id}/status", params: status("open") }.to change { expect {
channel_1.reload.status put "/chat/api/channels/#{channel_1.id}/status", params: status("open")
}.to("open").from("closed") }.to change { channel_1.reload.status }.to("open").from("closed")
expect(response.status).to eq(200) expect(response.status).to eq(200)
channel = response.parsed_body["channel"] channel = response.parsed_body["channel"]

View File

@ -1114,7 +1114,11 @@ RSpec.describe Chat::ChatController do
it "returns a 403 if the user can't see the channel" do it "returns a 403 if the user can't see the channel" do
category.update!(read_restricted: true) category.update!(read_restricted: true)
group = Fabricate(:group) group = Fabricate(:group)
CategoryGroup.create(group: group, category: category, permission_type: CategoryGroup.permission_types[:create_post]) CategoryGroup.create(
group: group,
category: category,
permission_type: CategoryGroup.permission_types[:create_post],
)
sign_in(user) sign_in(user)
post "/chat/#{channel.id}/quote.json", post "/chat/#{channel.id}/quote.json",
params: { params: {

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe CategoriesController do RSpec.describe CategoriesController do
describe '#destroy' do describe "#destroy" do
subject(:destroy_category) { delete "/categories/#{category.slug}.json" } subject(:destroy_category) { delete "/categories/#{category.slug}.json" }
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }

View File

@ -23,11 +23,7 @@ RSpec.describe ChatMessageDestroyer do
it "deletes flags associated to deleted chat messages" do it "deletes flags associated to deleted chat messages" do
guardian = Guardian.new(Discourse.system_user) guardian = Guardian.new(Discourse.system_user)
Chat::ChatReviewQueue.new.flag_message( Chat::ChatReviewQueue.new.flag_message(message_1, guardian, ReviewableScore.types[:off_topic])
message_1,
guardian,
ReviewableScore.types[:off_topic],
)
reviewable = ReviewableChatMessage.last reviewable = ReviewableChatMessage.last
expect(reviewable).to be_present expect(reviewable).to be_present

View File

@ -6,8 +6,8 @@ RSpec.shared_examples "a chatable model" do
it "returns a new chat channel model" do it "returns a new chat channel model" do
expect(chat_channel).to have_attributes persisted?: false, expect(chat_channel).to have_attributes persisted?: false,
class: channel_class, class: channel_class,
chatable: chatable chatable: chatable
end end
end end

View File

@ -32,7 +32,9 @@ describe "Using #hashtag autocompletion to search for and lookup channels",
count: 3, count: 3,
) )
hashtag_results = page.all(".hashtag-autocomplete__link", count: 3) hashtag_results = page.all(".hashtag-autocomplete__link", count: 3)
expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(["Random", "Raspberry", "razed (x0)"]) expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(
["Random", "Raspberry", "razed (x0)"],
)
end end
it "searches for channels as well with # in a topic composer and deprioritises them" do it "searches for channels as well with # in a topic composer and deprioritises them" do
@ -44,18 +46,26 @@ describe "Using #hashtag autocompletion to search for and lookup channels",
count: 3, count: 3,
) )
hashtag_results = page.all(".hashtag-autocomplete__link", count: 3) hashtag_results = page.all(".hashtag-autocomplete__link", count: 3)
expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(["Raspberry", "razed (x0)", "Random"]) expect(hashtag_results.map(&:text).map { |r| r.gsub("\n", " ") }).to eq(
["Raspberry", "razed (x0)", "Random"],
)
end end
it "cooks the hashtags for channels, categories, and tags serverside when the chat message is saved to the database" do it "cooks the hashtags for channels, categories, and tags serverside when the chat message is saved to the database" do
chat_page.visit_channel(channel1) chat_page.visit_channel(channel1)
expect(chat_channel_page).to have_no_loading_skeleton expect(chat_channel_page).to have_no_loading_skeleton
chat_channel_page.type_in_composer("this is #random and this is #raspberry-beret and this is #razed which is cool") chat_channel_page.type_in_composer(
"this is #random and this is #raspberry-beret and this is #razed which is cool",
)
chat_channel_page.click_send_message chat_channel_page.click_send_message
message = nil message = nil
try_until_success do try_until_success do
message = ChatMessage.find_by(user: user, message: "this is #random and this is #raspberry-beret and this is #razed which is cool") message =
ChatMessage.find_by(
user: user,
message: "this is #random and this is #raspberry-beret and this is #razed which is cool",
)
expect(message).not_to eq(nil) expect(message).not_to eq(nil)
end end
expect(chat_channel_page).to have_message(id: message.id) expect(chat_channel_page).to have_message(id: message.id)

View File

@ -43,8 +43,12 @@ RSpec.describe "List channels | mobile", type: :system, js: true, mobile: true d
it "sorts them alphabetically" do it "sorts them alphabetically" do
visit("/chat") visit("/chat")
expect(page.find("#public-channels a:nth-child(1)")["data-chat-channel-id"]).to eq(channel_2.id.to_s) expect(page.find("#public-channels a:nth-child(1)")["data-chat-channel-id"]).to eq(
expect(page.find("#public-channels a:nth-child(2)")["data-chat-channel-id"]).to eq(channel_1.id.to_s) channel_2.id.to_s,
)
expect(page.find("#public-channels a:nth-child(2)")["data-chat-channel-id"]).to eq(
channel_1.id.to_s,
)
end end
end end

View File

@ -44,8 +44,12 @@ RSpec.describe "List channels | no sidebar", type: :system, js: true do
it "sorts them alphabetically" do it "sorts them alphabetically" do
visit("/chat") visit("/chat")
expect(page.find("#public-channels a:nth-child(1)")["data-chat-channel-id"]).to eq(channel_2.id.to_s) expect(page.find("#public-channels a:nth-child(1)")["data-chat-channel-id"]).to eq(
expect(page.find("#public-channels a:nth-child(2)")["data-chat-channel-id"]).to eq(channel_1.id.to_s) channel_2.id.to_s,
)
expect(page.find("#public-channels a:nth-child(2)")["data-chat-channel-id"]).to eq(
channel_1.id.to_s,
)
end end
end end

View File

@ -53,8 +53,12 @@ RSpec.describe "List channels | sidebar", type: :system, js: true do
it "sorts them alphabetically" do it "sorts them alphabetically" do
visit("/") visit("/")
expect(page.find("#sidebar-section-content-chat-channels li:nth-child(1)")).to have_css(".channel-#{channel_2.id}") expect(page.find("#sidebar-section-content-chat-channels li:nth-child(1)")).to have_css(
expect(page.find("#sidebar-section-content-chat-channels li:nth-child(2)")).to have_css(".channel-#{channel_1.id}") ".channel-#{channel_2.id}",
)
expect(page.find("#sidebar-section-content-chat-channels li:nth-child(2)")).to have_css(
".channel-#{channel_1.id}",
)
end end
end end

View File

@ -43,14 +43,20 @@ RSpec.describe "Navigating to message", type: :system, js: true do
context "when clicking a link to a message from the current channel" do context "when clicking a link to a message from the current channel" do
before do before do
Fabricate(:chat_message, chat_channel: channel_1, message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})") Fabricate(
:chat_message,
chat_channel: channel_1,
message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})",
)
end end
it "highglights the correct message" do it "highglights the correct message" do
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
click_link(link) click_link(link)
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
it "highlights the correct message after using the bottom arrow" do it "highlights the correct message after using the bottom arrow" do
@ -59,7 +65,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
click_link(I18n.t("js.chat.scroll_to_bottom")) click_link(I18n.t("js.chat.scroll_to_bottom"))
click_link(link) click_link(link)
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
end end
@ -67,7 +75,11 @@ RSpec.describe "Navigating to message", type: :system, js: true do
fab!(:channel_2) { Fabricate(:category_channel) } fab!(:channel_2) { Fabricate(:category_channel) }
before do before do
Fabricate(:chat_message, chat_channel: channel_2, message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})") Fabricate(
:chat_message,
chat_channel: channel_2,
message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})",
)
channel_2.add(current_user) channel_2.add(current_user)
end end
@ -75,7 +87,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
chat_page.visit_channel(channel_2) chat_page.visit_channel(channel_2)
click_link(link) click_link(link)
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
end end
@ -83,7 +97,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
it "highglights the correct message" do it "highglights the correct message" do
visit("/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id}") visit("/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id}")
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
end end
end end
@ -113,7 +129,11 @@ RSpec.describe "Navigating to message", type: :system, js: true do
context "when clicking a link to a message from the current channel" do context "when clicking a link to a message from the current channel" do
before do before do
Fabricate(:chat_message, chat_channel: channel_1, message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})") Fabricate(
:chat_message,
chat_channel: channel_1,
message: "[#{link}](/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id})",
)
end end
it "highglights the correct message" do it "highglights the correct message" do
@ -122,7 +142,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
chat_drawer_page.open_channel(channel_1) chat_drawer_page.open_channel(channel_1)
click_link(link) click_link(link)
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
it "highlights the correct message after using the bottom arrow" do it "highlights the correct message after using the bottom arrow" do
@ -133,7 +155,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
click_link(I18n.t("js.chat.scroll_to_bottom")) click_link(I18n.t("js.chat.scroll_to_bottom"))
click_link(link) click_link(link)
expect(page).to have_css(".chat-message-container.highlighted[data-id='#{first_message.id}']") expect(page).to have_css(
".chat-message-container.highlighted[data-id='#{first_message.id}']",
)
end end
end end
end end

View File

@ -4,7 +4,9 @@ module PageObjects
module Pages module Pages
class Chat < PageObjects::Pages::Base class Chat < PageObjects::Pages::Base
def prefers_full_page def prefers_full_page
page.execute_script("window.localStorage.setItem('discourse_chat_preferred_mode', '\"FULL_PAGE_CHAT\"');") page.execute_script(
"window.localStorage.setItem('discourse_chat_preferred_mode', '\"FULL_PAGE_CHAT\"');",
)
end end
def open_from_header def open_from_header

View File

@ -12,32 +12,34 @@ hide_plugin if self.respond_to?(:hide_plugin)
register_asset "stylesheets/details.scss" register_asset "stylesheets/details.scss"
after_initialize do after_initialize do
Email::Styles.register_plugin_style do |fragment| Email::Styles.register_plugin_style do |fragment|
# remove all elided content # remove all elided content
fragment.css("details.elided").each(&:remove) fragment.css("details.elided").each(&:remove)
# replace all details with their summary in emails # replace all details with their summary in emails
fragment.css("details").each do |details| fragment
summary = details.css("summary") .css("details")
if summary && summary[0] .each do |details|
summary = summary[0] summary = details.css("summary")
if summary && summary.respond_to?(:name) if summary && summary[0]
summary.name = "p" summary = summary[0]
details.replace(summary) if summary && summary.respond_to?(:name)
summary.name = "p"
details.replace(summary)
end
end end
end end
end
end end
on(:reduce_cooked) do |fragment, post| on(:reduce_cooked) do |fragment, post|
fragment.css("details").each do |el| fragment
text = el.css("summary").text .css("details")
link = fragment.document.create_element("a") .each do |el|
link["href"] = post.url if post text = el.css("summary").text
link.content = I18n.t("details.excerpt_details") link = fragment.document.create_element("a")
el.replace CGI.escapeHTML(text) + " " + link.to_html link["href"] = post.url if post
end link.content = I18n.t("details.excerpt_details")
el.replace CGI.escapeHTML(text) + " " + link.to_html
end
end end
end end

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require 'pretty_text' require "pretty_text"
RSpec.describe PrettyText do RSpec.describe PrettyText do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
it "supports details tag" do it "supports details tag" do
@ -17,17 +16,19 @@ RSpec.describe PrettyText do
HTML HTML
expect(cooked_html).to match_html(cooked_html) expect(cooked_html).to match_html(cooked_html)
expect(PrettyText.cook("[details=foo]\nbar\n[/details]").gsub("\n", "")).to match_html(cooked_html) expect(PrettyText.cook("[details=foo]\nbar\n[/details]").gsub("\n", "")).to match_html(
cooked_html,
)
end end
it "deletes elided content" do it "deletes elided content" do
cooked_html = PrettyText.cook("Hello World\n\n<details class='elided'>42</details>") cooked_html = PrettyText.cook("Hello World\n\n<details class='elided'>42</details>")
mail_html = "<p>Hello World</p>\n<a href=\"http://test.localhost\">(click for more details)</a>" mail_html = "<p>Hello World</p>\n<a href=\"http://test.localhost\">(click for more details)</a>"
expect(PrettyText.format_for_email(cooked_html)).to match_html(mail_html) expect(PrettyText.format_for_email(cooked_html)).to match_html(mail_html)
end end
it 'can replace spoilers in emails' do it "can replace spoilers in emails" do
md = PrettyText.cook(<<~MD) md = PrettyText.cook(<<~MD)
hello hello
@ -41,7 +42,7 @@ RSpec.describe PrettyText do
expect(md).to eq(html) expect(md).to eq(html)
end end
it 'properly handles multiple spoiler blocks in a post' do it "properly handles multiple spoiler blocks in a post" do
md = PrettyText.cook(<<~MD) md = PrettyText.cook(<<~MD)
[details="First"] [details="First"]
body secret stuff very long body secret stuff very long
@ -58,13 +59,13 @@ RSpec.describe PrettyText do
MD MD
md = PrettyText.format_for_email(md, post) md = PrettyText.format_for_email(md, post)
expect(md).not_to include('secret stuff') expect(md).not_to include("secret stuff")
expect(md.scan(/First/).size).to eq(1) expect(md.scan(/First/).size).to eq(1)
expect(md.scan(/Third/).size).to eq(1) expect(md.scan(/Third/).size).to eq(1)
expect(md.scan(I18n.t('details.excerpt_details')).size).to eq(3) expect(md.scan(I18n.t("details.excerpt_details")).size).to eq(3)
end end
it 'escapes summary text' do it "escapes summary text" do
md = PrettyText.cook(<<~MD) md = PrettyText.cook(<<~MD)
<script>alert('hello')</script> <script>alert('hello')</script>
[details="<script>alert('hello')</script>"] [details="<script>alert('hello')</script>"]
@ -73,7 +74,6 @@ RSpec.describe PrettyText do
MD MD
md = PrettyText.format_for_email(md, post) md = PrettyText.format_for_email(md, post)
expect(md).not_to include('<script>') expect(md).not_to include("<script>")
end end
end end

View File

@ -7,40 +7,40 @@
hide_plugin if self.respond_to?(:hide_plugin) hide_plugin if self.respond_to?(:hide_plugin)
register_asset 'stylesheets/common/discourse-local-dates.scss' register_asset "stylesheets/common/discourse-local-dates.scss"
register_asset 'moment.js', :vendored_core_pretty_text register_asset "moment.js", :vendored_core_pretty_text
register_asset 'moment-timezone.js', :vendored_core_pretty_text register_asset "moment-timezone.js", :vendored_core_pretty_text
enabled_site_setting :discourse_local_dates_enabled enabled_site_setting :discourse_local_dates_enabled
after_initialize do after_initialize do
module ::DiscourseLocalDates module ::DiscourseLocalDates
PLUGIN_NAME ||= 'discourse-local-dates'.freeze PLUGIN_NAME ||= "discourse-local-dates".freeze
POST_CUSTOM_FIELD ||= 'local_dates'.freeze POST_CUSTOM_FIELD ||= "local_dates".freeze
end end
%w[../lib/discourse_local_dates/engine.rb].each do |path| %w[../lib/discourse_local_dates/engine.rb].each { |path| load File.expand_path(path, __FILE__) }
load File.expand_path(path, __FILE__)
end
register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json) register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json)
on(:before_post_process_cooked) do |doc, post| on(:before_post_process_cooked) do |doc, post|
dates = [] dates = []
doc.css('span.discourse-local-date').map do |cooked_date| doc
next if cooked_date.ancestors("aside").length > 0 .css("span.discourse-local-date")
date = {} .map do |cooked_date|
cooked_date.attributes.values.each do |attribute| next if cooked_date.ancestors("aside").length > 0
data_name = attribute.name&.gsub('data-', '') date = {}
if data_name && %w[date time timezone recurring].include?(data_name) cooked_date.attributes.values.each do |attribute|
unless attribute.value == 'undefined' data_name = attribute.name&.gsub("data-", "")
date[data_name] = CGI.escapeHTML(attribute.value || '') if data_name && %w[date time timezone recurring].include?(data_name)
unless attribute.value == "undefined"
date[data_name] = CGI.escapeHTML(attribute.value || "")
end
end end
end end
dates << date
end end
dates << date
end
if dates.present? if dates.present?
post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates
@ -51,22 +51,22 @@ after_initialize do
end end
end end
add_to_class(:post, :local_dates) do add_to_class(:post, :local_dates) { custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] || [] }
custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] || []
end
on(:reduce_excerpt) do |fragment, post| on(:reduce_excerpt) do |fragment, post|
fragment.css('.discourse-local-date').each do |container| fragment
container.content = "#{container.content} (UTC)" .css(".discourse-local-date")
end .each { |container| container.content = "#{container.content} (UTC)" }
end end
on(:reduce_cooked) do |fragment| on(:reduce_cooked) do |fragment|
fragment.css('.discourse-local-date').each do |container| fragment
if container.attributes['data-email-preview'] .css(".discourse-local-date")
preview = container.attributes['data-email-preview'].value .each do |container|
container.content = preview if container.attributes["data-email-preview"]
preview = container.attributes["data-email-preview"].value
container.content = preview
end
end end
end
end end
end end

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe "Local Dates" do RSpec.describe "Local Dates" do
before do before { freeze_time DateTime.parse("2018-11-10 12:00") }
freeze_time DateTime.parse('2018-11-10 12:00')
end
it "should work without timezone" do it "should work without timezone" do
post = Fabricate(:post, raw: <<~MD) post = Fabricate(:post, raw: <<~MD)
@ -15,14 +13,12 @@ RSpec.describe "Local Dates" do
expect(cooked).to include('class="discourse-local-date"') expect(cooked).to include('class="discourse-local-date"')
expect(cooked).to include('data-date="2018-05-08"') expect(cooked).to include('data-date="2018-05-08"')
expect(cooked).to include('data-format="L LTS"') expect(cooked).to include('data-format="L LTS"')
expect(cooked).not_to include('data-timezone=') expect(cooked).not_to include("data-timezone=")
expect(cooked).to include( expect(cooked).to include('data-timezones="Europe/Paris|America/Los_Angeles"')
'data-timezones="Europe/Paris|America/Los_Angeles"'
)
expect(cooked).to include('data-email-preview="2018-05-08T22:00:00Z UTC"') expect(cooked).to include('data-email-preview="2018-05-08T22:00:00Z UTC"')
expect(cooked).to include('05/08/2018 10:00:00 PM') expect(cooked).to include("05/08/2018 10:00:00 PM")
end end
it "should work with timezone" do it "should work with timezone" do
@ -33,10 +29,10 @@ RSpec.describe "Local Dates" do
cooked = post.cooked cooked = post.cooked
expect(cooked).to include('data-timezone="Asia/Calcutta"') expect(cooked).to include('data-timezone="Asia/Calcutta"')
expect(cooked).to include('05/08/2018 4:30:00 PM') expect(cooked).to include("05/08/2018 4:30:00 PM")
end end
it 'requires the right attributes to convert to a local date' do it "requires the right attributes to convert to a local date" do
post = Fabricate(:post, raw: <<~MD) post = Fabricate(:post, raw: <<~MD)
[date] [date]
MD MD
@ -44,10 +40,10 @@ RSpec.describe "Local Dates" do
cooked = post.cooked cooked = post.cooked
expect(post.cooked).to include("<p>[date]</p>") expect(post.cooked).to include("<p>[date]</p>")
expect(cooked).to_not include('data-date=') expect(cooked).to_not include("data-date=")
end end
it 'requires the right attributes to convert to a local date' do it "requires the right attributes to convert to a local date" do
post = Fabricate(:post, raw: <<~MD) post = Fabricate(:post, raw: <<~MD)
[date] [date]
MD MD
@ -55,48 +51,48 @@ RSpec.describe "Local Dates" do
cooked = post.cooked cooked = post.cooked
expect(post.cooked).to include("<p>[date]</p>") expect(post.cooked).to include("<p>[date]</p>")
expect(cooked).to_not include('data-date=') expect(cooked).to_not include("data-date=")
end end
it 'it works with only a date and time' do it "it works with only a date and time" do
raw = "[date=2018-11-01 time=12:00]" raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2018-11-01"') expect(cooked).to include('data-date="2018-11-01"')
expect(cooked).to include('data-time="12:00"') expect(cooked).to include('data-time="12:00"')
end end
it 'doesnt include format by default' do it "doesnt include format by default" do
raw = "[date=2018-11-01 time=12:00]" raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).not_to include('data-format=') expect(cooked).not_to include("data-format=")
end end
it 'doesnt include timezone by default' do it "doesnt include timezone by default" do
raw = "[date=2018-11-01 time=12:00]" raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).not_to include("data-timezone=") expect(cooked).not_to include("data-timezone=")
end end
it 'supports countdowns' do it "supports countdowns" do
raw = "[date=2018-11-01 time=12:00 countdown=true]" raw = "[date=2018-11-01 time=12:00 countdown=true]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include("data-countdown=") expect(cooked).to include("data-countdown=")
end end
describe 'ranges' do describe "ranges" do
it 'generates ranges without time' do it "generates ranges without time" do
raw = "[date-range from=2022-01-06 to=2022-01-08]" raw = "[date-range from=2022-01-06 to=2022-01-08]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2022-01-06') expect(cooked).to include('data-date="2022-01-06')
expect(cooked).to include('data-range="from"') expect(cooked).to include('data-range="from"')
expect(cooked).to include('data-range="to"') expect(cooked).to include('data-range="to"')
expect(cooked).not_to include('data-time=') expect(cooked).not_to include("data-time=")
end end
it 'supports time and timezone' do it "supports time and timezone" do
raw = "[date-range from=2022-01-06T13:00 to=2022-01-08 timezone=Australia/Sydney]" raw = "[date-range from=2022-01-06T13:00 to=2022-01-08 timezone=Australia/Sydney]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked
@ -107,7 +103,7 @@ RSpec.describe "Local Dates" do
expect(cooked).to include('data-timezone="Australia/Sydney"') expect(cooked).to include('data-timezone="Australia/Sydney"')
end end
it 'generates single date when range without end date' do it "generates single date when range without end date" do
raw = "[date-range from=2022-01-06T13:00]" raw = "[date-range from=2022-01-06T13:00]"
cooked = Fabricate(:post, raw: raw).cooked cooked = Fabricate(:post, raw: raw).cooked

View File

@ -15,94 +15,118 @@ def generate_html(text, opts = {})
end end
RSpec.describe PrettyText do RSpec.describe PrettyText do
before do before { freeze_time }
freeze_time
end
describe 'emails simplified rendering' do describe "emails simplified rendering" do
it 'works with default markup' do it "works with default markup" do
cooked = PrettyText.cook("[date=2018-05-08]") cooked = PrettyText.cook("[date=2018-05-08]")
cooked_mail = generate_html("2018-05-08T00:00:00Z UTC", cooked_mail =
date: "2018-05-08", generate_html(
email_preview: "2018-05-08T00:00:00Z UTC" "2018-05-08T00:00:00Z UTC",
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
it 'works with time' do
cooked = PrettyText.cook("[date=2018-05-08 time=20:00:00]")
cooked_mail = generate_html("2018-05-08T20:00:00Z UTC",
date: "2018-05-08",
email_preview: "2018-05-08T20:00:00Z UTC",
time: "20:00:00"
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
it 'works with multiple timezones' do
cooked = PrettyText.cook('[date=2018-05-08 timezone="Europe/Paris" timezones="America/Los_Angeles|Pacific/Auckland"]')
cooked_mail = generate_html("2018-05-07T22:00:00Z UTC",
date: "2018-05-08",
email_preview: "2018-05-07T22:00:00Z UTC",
timezone: "Europe/Paris",
timezones: "America/Los_Angeles|Pacific/Auckland"
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
describe 'discourse_local_dates_email_format' do
before do
SiteSetting.discourse_local_dates_email_format = "DD/MM"
end
it 'uses the site setting' do
cooked = PrettyText.cook("[date=2018-05-08]")
cooked_mail = generate_html("08/05 UTC",
date: "2018-05-08", date: "2018-05-08",
email_preview: "08/05 UTC" email_preview: "2018-05-08T00:00:00Z UTC",
) )
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
it "works with time" do
cooked = PrettyText.cook("[date=2018-05-08 time=20:00:00]")
cooked_mail =
generate_html(
"2018-05-08T20:00:00Z UTC",
date: "2018-05-08",
email_preview: "2018-05-08T20:00:00Z UTC",
time: "20:00:00",
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
it "works with multiple timezones" do
cooked =
PrettyText.cook(
'[date=2018-05-08 timezone="Europe/Paris" timezones="America/Los_Angeles|Pacific/Auckland"]',
)
cooked_mail =
generate_html(
"2018-05-07T22:00:00Z UTC",
date: "2018-05-08",
email_preview: "2018-05-07T22:00:00Z UTC",
timezone: "Europe/Paris",
timezones: "America/Los_Angeles|Pacific/Auckland",
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
describe "discourse_local_dates_email_format" do
before { SiteSetting.discourse_local_dates_email_format = "DD/MM" }
it "uses the site setting" do
cooked = PrettyText.cook("[date=2018-05-08]")
cooked_mail = generate_html("08/05 UTC", date: "2018-05-08", email_preview: "08/05 UTC")
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end end
end end
end end
describe 'excerpt simplified rendering' do describe "excerpt simplified rendering" do
let(:post) { Fabricate(:post, raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone="America/New_York"]') } let(:post) do
Fabricate(
:post,
raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone="America/New_York"]',
)
end
it 'adds UTC' do it "adds UTC" do
excerpt = PrettyText.excerpt(post.cooked, 200) excerpt = PrettyText.excerpt(post.cooked, 200)
expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)") expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)")
end end
end end
describe 'special quotes' do describe "special quotes" do
it 'converts special quotes to regular quotes' do it "converts special quotes to regular quotes" do
# german # german
post = Fabricate(:post, raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=„America/New_York“]') post =
Fabricate(
:post,
raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=„America/New_York“]',
)
excerpt = PrettyText.excerpt(post.cooked, 200) excerpt = PrettyText.excerpt(post.cooked, 200)
expect(excerpt).to eq('Wednesday, October 16, 2019 6:00 PM (UTC)') expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)")
# french # french
post = Fabricate(:post, raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=«America/New_York»]') post =
Fabricate(
:post,
raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=«America/New_York»]',
)
excerpt = PrettyText.excerpt(post.cooked, 200) excerpt = PrettyText.excerpt(post.cooked, 200)
expect(excerpt).to eq('Wednesday, October 16, 2019 6:00 PM (UTC)') expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)")
post = Fabricate(:post, raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=“America/New_York”]') post =
Fabricate(
:post,
raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=“America/New_York”]',
)
excerpt = PrettyText.excerpt(post.cooked, 200) excerpt = PrettyText.excerpt(post.cooked, 200)
expect(excerpt).to eq('Wednesday, October 16, 2019 6:00 PM (UTC)') expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)")
end end
end end
describe 'french quotes' do describe "french quotes" do
let(:post) { Fabricate(:post, raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=«America/New_York»]') } let(:post) do
Fabricate(
:post,
raw: '[date=2019-10-16 time=14:00:00 format="LLLL" timezone=«America/New_York»]',
)
end
it 'converts french quotes to regular quotes' do it "converts french quotes to regular quotes" do
excerpt = PrettyText.excerpt(post.cooked, 200) excerpt = PrettyText.excerpt(post.cooked, 200)
expect(excerpt).to eq('Wednesday, October 16, 2019 6:00 PM (UTC)') expect(excerpt).to eq("Wednesday, October 16, 2019 6:00 PM (UTC)")
end end
end end
end end

View File

@ -1,12 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe Post do RSpec.describe Post do
before { Jobs.run_immediately! }
before do describe "#local_dates" do
Jobs.run_immediately!
end
describe '#local_dates' do
it "should have correct custom fields" do it "should have correct custom fields" do
post = Fabricate(:post, raw: <<~SQL) post = Fabricate(:post, raw: <<~SQL)
[date=2018-09-17 time=01:39:00 format="LLL" timezone="Europe/Paris" timezones="Europe/Paris|America/Los_Angeles"] [date=2018-09-17 time=01:39:00 format="LLL" timezone="Europe/Paris" timezones="Europe/Paris|America/Los_Angeles"]
@ -37,7 +34,7 @@ RSpec.describe Post do
end end
it "should not contain dates from examples" do it "should not contain dates from examples" do
Oneboxer.stubs(:cached_onebox).with('https://example.com').returns(<<-HTML) Oneboxer.stubs(:cached_onebox).with("https://example.com").returns(<<-HTML)
<aside class="onebox githubcommit"> <aside class="onebox githubcommit">
<span class="discourse-local-date" data-format="ll" data-date="2020-01-20" data-time="15:06:58" data-timezone="UTC">03:06PM - 20 Jan 20 UTC</span> <span class="discourse-local-date" data-format="ll" data-date="2020-01-20" data-time="15:06:58" data-timezone="UTC">03:06PM - 20 Jan 20 UTC</span>
</aside> </aside>
@ -48,5 +45,4 @@ RSpec.describe Post do
expect(post.local_dates.count).to eq(0) expect(post.local_dates.count).to eq(0)
end end
end end
end end

View File

@ -4,18 +4,11 @@ describe "Local dates", type: :system, js: true do
fab!(:topic) { Fabricate(:topic) } fab!(:topic) { Fabricate(:topic) }
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
before do before { create_post(user: user, topic: topic, title: "Date range test post", raw: <<~RAW) }
create_post(
user: user,
topic: topic,
title: "Date range test post",
raw: <<~RAW
First option: [date=2022-12-15 time=14:19:00 timezone="Asia/Singapore"] First option: [date=2022-12-15 time=14:19:00 timezone="Asia/Singapore"]
Second option: [date=2022-12-15 time=01:20:00 timezone="Asia/Singapore"], or [date=2022-12-15 time=02:40:00 timezone="Asia/Singapore"] Second option: [date=2022-12-15 time=01:20:00 timezone="Asia/Singapore"], or [date=2022-12-15 time=02:40:00 timezone="Asia/Singapore"]
Third option: [date-range from=2022-12-15T11:25:00 to=2022-12-16T00:26:00 timezone="Asia/Singapore"] or [date-range from=2022-12-22T11:57:00 to=2022-12-23T11:58:00 timezone="Asia/Singapore"] Third option: [date-range from=2022-12-15T11:25:00 to=2022-12-16T00:26:00 timezone="Asia/Singapore"] or [date-range from=2022-12-22T11:57:00 to=2022-12-23T11:58:00 timezone="Asia/Singapore"]
RAW RAW
)
end
let(:topic_page) { PageObjects::Pages::Topic.new } let(:topic_page) { PageObjects::Pages::Topic.new }
@ -53,12 +46,18 @@ describe "Local dates", type: :system, js: true do
post_dates[3].click post_dates[3].click
tippy_date = topic_page.find(".tippy-content .current .date-time") tippy_date = topic_page.find(".tippy-content .current .date-time")
expect(tippy_date).to have_text("Thursday, December 15, 2022\n11:25 AM → 12:26 AM", exact: true) expect(tippy_date).to have_text(
"Thursday, December 15, 2022\n11:25 AM → 12:26 AM",
exact: true,
)
post_dates[5].click post_dates[5].click
tippy_date = topic_page.find(".tippy-content .current .date-time") tippy_date = topic_page.find(".tippy-content .current .date-time")
expect(tippy_date).to have_text("Thursday, December 22, 2022 11:57 AM → Friday, December 23, 2022 11:58 AM", exact: true) expect(tippy_date).to have_text(
"Thursday, December 22, 2022 11:57 AM → Friday, December 23, 2022 11:58 AM",
exact: true,
)
end end
end end
end end

View File

@ -4,33 +4,29 @@ module Jobs
module DiscourseNarrativeBot module DiscourseNarrativeBot
class GrantBadges < ::Jobs::Onceoff class GrantBadges < ::Jobs::Onceoff
def execute_onceoff(args) def execute_onceoff(args)
new_user_track_badge = Badge.find_by( new_user_track_badge =
name: ::DiscourseNarrativeBot::NewUserNarrative.badge_name Badge.find_by(name: ::DiscourseNarrativeBot::NewUserNarrative.badge_name)
)
advanced_user_track_badge = Badge.find_by( advanced_user_track_badge =
name: ::DiscourseNarrativeBot::AdvancedUserNarrative.badge_name Badge.find_by(name: ::DiscourseNarrativeBot::AdvancedUserNarrative.badge_name)
)
PluginStoreRow.where( PluginStoreRow
plugin_name: ::DiscourseNarrativeBot::PLUGIN_NAME, .where(plugin_name: ::DiscourseNarrativeBot::PLUGIN_NAME, type_name: "JSON")
type_name: 'JSON' .find_each do |row|
).find_each do |row| value = JSON.parse(row.value)
completed = value["completed"]
user = User.find_by(id: row.key)
value = JSON.parse(row.value) if user && completed
completed = value["completed"] if completed.include?(::DiscourseNarrativeBot::NewUserNarrative.to_s)
user = User.find_by(id: row.key) BadgeGranter.grant(new_user_track_badge, user)
end
if user && completed if completed.include?(::DiscourseNarrativeBot::AdvancedUserNarrative.to_s)
if completed.include?(::DiscourseNarrativeBot::NewUserNarrative.to_s) BadgeGranter.grant(advanced_user_track_badge, user)
BadgeGranter.grant(new_user_track_badge, user) end
end
if completed.include?(::DiscourseNarrativeBot::AdvancedUserNarrative.to_s)
BadgeGranter.grant(advanced_user_track_badge, user)
end end
end end
end
end end
end end
end end

View File

@ -4,34 +4,40 @@ module Jobs
module DiscourseNarrativeBot module DiscourseNarrativeBot
class RemapOldBotImages < ::Jobs::Onceoff class RemapOldBotImages < ::Jobs::Onceoff
def execute_onceoff(args) def execute_onceoff(args)
paths = [ paths = %w[
"/images/font-awesome-link.png", /images/font-awesome-link.png
"/images/unicorn.png", /images/unicorn.png
"/images/font-awesome-ellipsis.png", /images/font-awesome-ellipsis.png
"/images/font-awesome-bookmark.png", /images/font-awesome-bookmark.png
"/images/font-awesome-smile.png", /images/font-awesome-smile.png
"/images/font-awesome-flag.png", /images/font-awesome-flag.png
"/images/font-awesome-search.png", /images/font-awesome-search.png
"/images/capybara-eating.gif", /images/capybara-eating.gif
"/images/font-awesome-pencil.png", /images/font-awesome-pencil.png
"/images/font-awesome-trash.png", /images/font-awesome-trash.png
"/images/font-awesome-rotate-left.png", /images/font-awesome-rotate-left.png
"/images/font-awesome-gear.png", /images/font-awesome-gear.png
] ]
Post.raw_match("/images/").where(user_id: -2).find_each do |post| Post
if (matches = post.raw.scan(/(?<!\/plugins\/discourse-narrative-bot)(#{paths.join("|")})/)).present? .raw_match("/images/")
new_raw = post.raw .where(user_id: -2)
.find_each do |post|
if (
matches =
post.raw.scan(%r{(?<!/plugins/discourse-narrative-bot)(#{paths.join("|")})})
).present?
new_raw = post.raw
matches.each do |match| matches.each do |match|
path = match.first path = match.first
new_raw = new_raw.gsub(path, "/plugins/discourse-narrative-bot#{path}") new_raw = new_raw.gsub(path, "/plugins/discourse-narrative-bot#{path}")
end
post.update_columns(raw: new_raw)
post.rebake!
end end
post.update_columns(raw: new_raw)
post.rebake!
end end
end
end end
end end
end end

View File

@ -2,16 +2,17 @@
module Jobs module Jobs
class BotInput < ::Jobs::Base class BotInput < ::Jobs::Base
sidekiq_options queue: "critical", retry: false
sidekiq_options queue: 'critical', retry: false
def execute(args) def execute(args)
return unless user = User.find_by(id: args[:user_id]) return unless user = User.find_by(id: args[:user_id])
I18n.with_locale(user.effective_locale) do I18n.with_locale(user.effective_locale) do
::DiscourseNarrativeBot::TrackSelector.new(args[:input].to_sym, user, ::DiscourseNarrativeBot::TrackSelector.new(
args[:input].to_sym,
user,
post_id: args[:post_id], post_id: args[:post_id],
topic_id: args[:topic_id] topic_id: args[:topic_id],
).select ).select
end end
end end

View File

@ -2,13 +2,11 @@
module Jobs module Jobs
class NarrativeInit < ::Jobs::Base class NarrativeInit < ::Jobs::Base
sidekiq_options queue: 'critical' sidekiq_options queue: "critical"
def execute(args) def execute(args)
if user = User.find_by(id: args[:user_id]) if user = User.find_by(id: args[:user_id])
I18n.with_locale(user.effective_locale) do I18n.with_locale(user.effective_locale) { args[:klass].constantize.new.input(:init, user) }
args[:klass].constantize.new.input(:init, user)
end
end end
end end
end end

View File

@ -4,23 +4,24 @@ module Jobs
class SendDefaultWelcomeMessage < ::Jobs::Base class SendDefaultWelcomeMessage < ::Jobs::Base
def execute(args) def execute(args)
if user = User.find_by(id: args[:user_id]) if user = User.find_by(id: args[:user_id])
type = user.invited_by ? 'welcome_invite' : 'welcome_user' type = user.invited_by ? "welcome_invite" : "welcome_user"
params = SystemMessage.new(user).defaults params = SystemMessage.new(user).defaults
title = I18n.t("system_messages.#{type}.subject_template", params) title = I18n.t("system_messages.#{type}.subject_template", params)
raw = I18n.t("system_messages.#{type}.text_body_template", params) raw = I18n.t("system_messages.#{type}.text_body_template", params)
discobot_user = ::DiscourseNarrativeBot::Base.new.discobot_user discobot_user = ::DiscourseNarrativeBot::Base.new.discobot_user
post = PostCreator.create!( post =
discobot_user, PostCreator.create!(
title: title, discobot_user,
raw: raw, title: title,
archetype: Archetype.private_message, raw: raw,
target_usernames: user.username, archetype: Archetype.private_message,
skip_validations: true target_usernames: user.username,
) skip_validations: true,
)
post.topic.update_status('closed', true, discobot_user) post.topic.update_status("closed", true, discobot_user)
end end
end end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
discobot_username = 'discobot' discobot_username = "discobot"
def seed_primary_email def seed_primary_email
UserEmail.seed do |ue| UserEmail.seed do |ue|
@ -42,15 +42,13 @@ bot.create_user_option! if !bot.user_option
bot.user_option.update!( bot.user_option.update!(
email_messages_level: UserOption.email_level_types[:never], email_messages_level: UserOption.email_level_types[:never],
email_level: UserOption.email_level_types[:never] email_level: UserOption.email_level_types[:never],
) )
bot.create_user_profile! if !bot.user_profile bot.create_user_profile! if !bot.user_profile
if !bot.user_profile.bio_raw if !bot.user_profile.bio_raw
bot.user_profile.update!( bot.user_profile.update!(bio_raw: I18n.t("discourse_narrative_bot.bio"))
bio_raw: I18n.t('discourse_narrative_bot.bio')
)
end end
Group.user_trust_level_change!(DiscourseNarrativeBot::BOT_USER_ID, TrustLevel[4]) Group.user_trust_level_change!(DiscourseNarrativeBot::BOT_USER_ID, TrustLevel[4])

View File

@ -1,41 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
Badge Badge.where(name: "Complete New User Track").update_all(
.where(name: 'Complete New User Track') name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME,
.update_all(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME) )
Badge Badge.where(name: "Complete Discobot Advanced User Track").update_all(
.where(name: 'Complete Discobot Advanced User Track') name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME,
.update_all(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME) )
new_user_narrative_badge = Badge.find_by(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME) new_user_narrative_badge = Badge.find_by(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME)
unless new_user_narrative_badge unless new_user_narrative_badge
new_user_narrative_badge = Badge.create!( new_user_narrative_badge =
name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME, Badge.create!(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME, badge_type_id: 3)
badge_type_id: 3
)
end end
advanced_user_narrative_badge = Badge.find_by(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME) advanced_user_narrative_badge =
Badge.find_by(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME)
unless advanced_user_narrative_badge unless advanced_user_narrative_badge
advanced_user_narrative_badge = Badge.create!( advanced_user_narrative_badge =
name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME, Badge.create!(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME, badge_type_id: 2)
badge_type_id: 2
)
end end
badge_grouping = BadgeGrouping.find(1) badge_grouping = BadgeGrouping.find(1)
[ [
[new_user_narrative_badge, I18n.t('badges.certified.description')], [new_user_narrative_badge, I18n.t("badges.certified.description")],
[advanced_user_narrative_badge, I18n.t('badges.licensed.description')] [advanced_user_narrative_badge, I18n.t("badges.licensed.description")],
].each do |badge, description| ].each do |badge, description|
badge.update!(badge_grouping: badge_grouping, description: description, system: true)
badge.update!(
badge_grouping: badge_grouping,
description: description,
system: true
)
end end

View File

@ -23,18 +23,19 @@ module DiscourseNarrativeBot
topic_id: post.topic_id, topic_id: post.topic_id,
reply_to_post_number: post.post_number, reply_to_post_number: post.post_number,
post_alert_options: defaut_post_alert_opts, post_alert_options: defaut_post_alert_opts,
skip_validations: true skip_validations: true,
} }
new_post = PostCreator.create!(self.discobot_user, default_opts.merge(opts)) new_post = PostCreator.create!(self.discobot_user, default_opts.merge(opts))
reset_rate_limits(post) if new_post reset_rate_limits(post) if new_post
new_post new_post
else else
PostCreator.create!(self.discobot_user, { PostCreator.create!(
post_alert_options: defaut_post_alert_opts, self.discobot_user,
raw: raw, { post_alert_options: defaut_post_alert_opts, raw: raw, skip_validations: true }.merge(
skip_validations: true opts,
}.merge(opts)) ),
)
end end
end end
@ -53,7 +54,8 @@ module DiscourseNarrativeBot
data = DiscourseNarrativeBot::Store.get(user.id.to_s) data = DiscourseNarrativeBot::Store.get(user.id.to_s)
return unless data return unless data
key = "#{DiscourseNarrativeBot::PLUGIN_NAME}:reset-rate-limit:#{post.topic_id}:#{data['state']}" key =
"#{DiscourseNarrativeBot::PLUGIN_NAME}:reset-rate-limit:#{post.topic_id}:#{data["state"]}"
if !(count = Discourse.redis.get(key)) if !(count = Discourse.redis.get(key))
count = 0 count = 0
@ -76,12 +78,14 @@ module DiscourseNarrativeBot
valid = false valid = false
doc.css(".mention").each do |mention| doc
if User.normalize_username(mention.text) == "@#{self.discobot_username}" .css(".mention")
valid = true .each do |mention|
break if User.normalize_username(mention.text) == "@#{self.discobot_username}"
valid = true
break
end
end end
end
valid valid
end end
@ -94,8 +98,7 @@ module DiscourseNarrativeBot
topic = post.topic topic = post.topic
return false if !topic return false if !topic
topic.pm_with_non_human_user? && topic.pm_with_non_human_user? && topic.topic_allowed_users.where(user_id: -2).exists?
topic.topic_allowed_users.where(user_id: -2).exists?
end end
def cancel_timeout_job(user) def cancel_timeout_job(user)
@ -107,9 +110,11 @@ module DiscourseNarrativeBot
cancel_timeout_job(user) cancel_timeout_job(user)
Jobs.enqueue_in(TIMEOUT_DURATION, :narrative_timeout, Jobs.enqueue_in(
TIMEOUT_DURATION,
:narrative_timeout,
user_id: user.id, user_id: user.id,
klass: self.class.to_s klass: self.class.to_s,
) )
end end
end end

View File

@ -3,99 +3,101 @@
module DiscourseNarrativeBot module DiscourseNarrativeBot
class AdvancedUserNarrative < Base class AdvancedUserNarrative < Base
I18N_KEY = "discourse_narrative_bot.advanced_user_narrative".freeze I18N_KEY = "discourse_narrative_bot.advanced_user_narrative".freeze
BADGE_NAME = 'Licensed'.freeze BADGE_NAME = "Licensed".freeze
TRANSITION_TABLE = { TRANSITION_TABLE = {
begin: { begin: {
next_state: :tutorial_edit, next_state: :tutorial_edit,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.edit.instructions", i18n_post_args) }, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.edit.instructions", i18n_post_args) },
init: { init: {
action: :start_advanced_track action: :start_advanced_track,
} },
}, },
tutorial_edit: { tutorial_edit: {
next_state: :tutorial_delete, next_state: :tutorial_delete,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.delete.instructions", i18n_post_args) }, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.delete.instructions", i18n_post_args) },
edit: { edit: {
action: :reply_to_edit action: :reply_to_edit,
}, },
reply: { reply: {
next_state: :tutorial_edit, next_state: :tutorial_edit,
action: :missing_edit action: :missing_edit,
} },
}, },
tutorial_delete: { tutorial_delete: {
next_state: :tutorial_recover, next_state: :tutorial_recover,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.recover.instructions", i18n_post_args) }, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.recover.instructions", i18n_post_args) },
delete: { delete: {
action: :reply_to_delete action: :reply_to_delete,
}, },
reply: { reply: {
next_state: :tutorial_delete, next_state: :tutorial_delete,
action: :missing_delete action: :missing_delete,
} },
}, },
tutorial_recover: { tutorial_recover: {
next_state: :tutorial_category_hashtag, next_state: :tutorial_category_hashtag,
next_instructions: Proc.new do next_instructions:
category = Category.secured(Guardian.new(@user)).last Proc.new do
slug = category.slug category = Category.secured(Guardian.new(@user)).last
slug = category.slug
if parent_category = category.parent_category if parent_category = category.parent_category
slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}" slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}"
end end
I18n.t("#{I18N_KEY}.category_hashtag.instructions", I18n.t(
i18n_post_args(category: "##{slug}") "#{I18N_KEY}.category_hashtag.instructions",
) i18n_post_args(category: "##{slug}"),
end, )
end,
recover: { recover: {
action: :reply_to_recover action: :reply_to_recover,
}, },
reply: { reply: {
next_state: :tutorial_recover, next_state: :tutorial_recover,
action: :missing_recover action: :missing_recover,
} },
}, },
tutorial_category_hashtag: { tutorial_category_hashtag: {
next_state: :tutorial_change_topic_notification_level, next_state: :tutorial_change_topic_notification_level,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.change_topic_notification_level.instructions", i18n_post_args) }, next_instructions:
Proc.new do
I18n.t("#{I18N_KEY}.change_topic_notification_level.instructions", i18n_post_args)
end,
reply: { reply: {
action: :reply_to_category_hashtag action: :reply_to_category_hashtag,
} },
}, },
tutorial_change_topic_notification_level: { tutorial_change_topic_notification_level: {
next_state: :tutorial_poll, next_state: :tutorial_poll,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.poll.instructions", i18n_post_args) }, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.poll.instructions", i18n_post_args) },
topic_notification_level_changed: { topic_notification_level_changed: {
action: :reply_to_topic_notification_level_changed action: :reply_to_topic_notification_level_changed,
}, },
reply: { reply: {
next_state: :tutorial_change_topic_notification_level, next_state: :tutorial_change_topic_notification_level,
action: :missing_topic_notification_level_change action: :missing_topic_notification_level_change,
} },
}, },
tutorial_poll: { tutorial_poll: {
prerequisite: Proc.new { SiteSetting.poll_enabled && @user.has_trust_level?(SiteSetting.poll_minimum_trust_level_to_create) }, prerequisite:
Proc.new do
SiteSetting.poll_enabled &&
@user.has_trust_level?(SiteSetting.poll_minimum_trust_level_to_create)
end,
next_state: :tutorial_details, next_state: :tutorial_details,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions", i18n_post_args) }, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions", i18n_post_args) },
reply: { reply: {
action: :reply_to_poll action: :reply_to_poll,
} },
}, },
tutorial_details: { tutorial_details: {
next_state: :end, next_state: :end,
reply: { reply: {
action: :reply_to_details action: :reply_to_details,
} },
} },
} }
def self.badge_name def self.badge_name
@ -103,7 +105,7 @@ module DiscourseNarrativeBot
end end
def self.reset_trigger def self.reset_trigger
I18n.t('discourse_narrative_bot.advanced_user_narrative.reset_trigger') I18n.t("discourse_narrative_bot.advanced_user_narrative.reset_trigger")
end end
def reset_bot(user, post) def reset_bot(user, post)
@ -123,13 +125,18 @@ module DiscourseNarrativeBot
fake_delay fake_delay
post = PostCreator.create!( post =
@user, PostCreator.create!(
raw: I18n.t("#{I18N_KEY}.edit.bot_created_post_raw", i18n_post_args(discobot_username: self.discobot_username)), @user,
topic_id: data[:topic_id], raw:
skip_bot: true, I18n.t(
skip_validations: true "#{I18N_KEY}.edit.bot_created_post_raw",
) i18n_post_args(discobot_username: self.discobot_username),
),
topic_id: data[:topic_id],
skip_bot: true,
skip_validations: true,
)
set_state_data(:post_id, post.id) set_state_data(:post_id, post.id)
post post
@ -138,13 +145,18 @@ module DiscourseNarrativeBot
def init_tutorial_recover def init_tutorial_recover
data = get_data(@user) data = get_data(@user)
post = PostCreator.create!( post =
@user, PostCreator.create!(
raw: I18n.t("#{I18N_KEY}.recover.deleted_post_raw", i18n_post_args(discobot_username: self.discobot_username)), @user,
topic_id: data[:topic_id], raw:
skip_bot: true, I18n.t(
skip_validations: true "#{I18N_KEY}.recover.deleted_post_raw",
) i18n_post_args(discobot_username: self.discobot_username),
),
topic_id: data[:topic_id],
skip_bot: true,
skip_validations: true,
)
set_state_data(:post_id, post.id) set_state_data(:post_id, post.id)
@ -172,18 +184,15 @@ module DiscourseNarrativeBot
opts = { opts = {
title: I18n.t("#{I18N_KEY}.title"), title: I18n.t("#{I18N_KEY}.title"),
target_usernames: @user.username, target_usernames: @user.username,
archetype: Archetype.private_message archetype: Archetype.private_message,
} }
if @post && if @post && @post.topic.private_message? &&
@post.topic.private_message? && @post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
end end
if @data[:topic_id] if @data[:topic_id]
opts = opts opts = opts.merge(topic_id: @data[:topic_id]).except(:title, :target_usernames, :archetype)
.merge(topic_id: @data[:topic_id])
.except(:title, :target_usernames, :archetype)
end end
post = reply_to(@post, raw, opts) post = reply_to(@post, raw, opts)
@ -213,9 +222,10 @@ module DiscourseNarrativeBot
fake_delay fake_delay
unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.edit.not_found", reply_to(
i18n_post_args(url: Post.find_by(id: post_id).url) @post,
)) I18n.t("#{I18N_KEY}.edit.not_found", i18n_post_args(url: Post.find_by(id: post_id).url)),
)
end end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
@ -233,16 +243,15 @@ module DiscourseNarrativeBot
#{instance_eval(&@next_instructions)} #{instance_eval(&@next_instructions)}
MD MD
PostCreator.create!(self.discobot_user, PostCreator.create!(self.discobot_user, raw: raw, topic_id: @topic_id)
raw: raw,
topic_id: @topic_id
)
end end
def missing_delete def missing_delete
return unless valid_topic?(@post.topic_id) return unless valid_topic?(@post.topic_id)
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.delete.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.delete.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -258,18 +267,19 @@ module DiscourseNarrativeBot
#{instance_eval(&@next_instructions)} #{instance_eval(&@next_instructions)}
MD MD
PostCreator.create!(self.discobot_user, PostCreator.create!(self.discobot_user, raw: raw, topic_id: @post.topic_id)
raw: raw,
topic_id: @post.topic_id
)
end end
def missing_recover def missing_recover
return unless valid_topic?(@post.topic_id) && unless valid_topic?(@post.topic_id) &&
post_id = get_state_data(:post_id) && @post.id != post_id post_id = get_state_data(:post_id) && @post.id != post_id
return
end
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.recover.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.recover.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -278,7 +288,7 @@ module DiscourseNarrativeBot
topic_id = @post.topic_id topic_id = @post.topic_id
return unless valid_topic?(topic_id) return unless valid_topic?(topic_id)
if Nokogiri::HTML5.fragment(@post.cooked).css('.hashtag').size > 0 if Nokogiri::HTML5.fragment(@post.cooked).css(".hashtag").size > 0
raw = <<~MD raw = <<~MD
#{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)} #{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)}
@ -289,7 +299,9 @@ module DiscourseNarrativeBot
reply_to(@post, raw) reply_to(@post, raw)
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -299,7 +311,12 @@ module DiscourseNarrativeBot
return unless valid_topic?(@post.topic_id) return unless valid_topic?(@post.topic_id)
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.change_topic_notification_level.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(
@post,
I18n.t("#{I18N_KEY}.change_topic_notification_level.not_found", i18n_post_args),
)
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -316,10 +333,7 @@ module DiscourseNarrativeBot
fake_delay fake_delay
post = PostCreator.create!(self.discobot_user, post = PostCreator.create!(self.discobot_user, raw: raw, topic_id: @topic_id)
raw: raw,
topic_id: @topic_id
)
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
post post
@ -340,7 +354,9 @@ module DiscourseNarrativeBot
reply_to(@post, raw) reply_to(@post, raw)
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.poll.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.poll.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -355,7 +371,9 @@ module DiscourseNarrativeBot
if Nokogiri::HTML5.fragment(@post.cooked).css("details").size > 0 if Nokogiri::HTML5.fragment(@post.cooked).css("details").size > 0
reply_to(@post, I18n.t("#{I18N_KEY}.details.reply", i18n_post_args)) reply_to(@post, I18n.t("#{I18N_KEY}.details.reply", i18n_post_args))
else else
reply_to(@post, I18n.t("#{I18N_KEY}.details.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.details.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -370,7 +388,9 @@ module DiscourseNarrativeBot
if @post.wiki if @post.wiki
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.reply", i18n_post_args)) reply_to(@post, I18n.t("#{I18N_KEY}.wiki.reply", i18n_post_args))
else else
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -379,9 +399,10 @@ module DiscourseNarrativeBot
def end_reply def end_reply
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.end.message", reply_to(
i18n_post_args(certificate: certificate('advanced')) @post,
)) I18n.t("#{I18N_KEY}.end.message", i18n_post_args(certificate: certificate("advanced"))),
)
end end
def synchronize(user) def synchronize(user)

View File

@ -4,7 +4,8 @@ module DiscourseNarrativeBot
class Base class Base
include Actions include Actions
class InvalidTransitionError < StandardError; end class InvalidTransitionError < StandardError
end
def input(input, user, post: nil, topic_id: nil, skip: false) def input(input, user, post: nil, topic_id: nil, skip: false)
new_post = nil new_post = nil
@ -30,16 +31,18 @@ module DiscourseNarrativeBot
next_opts = self.class::TRANSITION_TABLE.fetch(next_state) next_opts = self.class::TRANSITION_TABLE.fetch(next_state)
prerequisite = next_opts[:prerequisite] prerequisite = next_opts[:prerequisite]
if (!prerequisite || instance_eval(&prerequisite)) && !( if (!prerequisite || instance_eval(&prerequisite)) &&
SiteSetting.discourse_narrative_bot_skip_tutorials.present? && !(
SiteSetting.discourse_narrative_bot_skip_tutorials.split("|").include?(next_state.to_s)) SiteSetting.discourse_narrative_bot_skip_tutorials.present? &&
SiteSetting
.discourse_narrative_bot_skip_tutorials
.split("|")
.include?(next_state.to_s)
)
break break
end end
[:next_state, :next_instructions].each do |key| %i[next_state next_instructions].each { |key| opts[key] = next_opts[key] }
opts[key] = next_opts[key]
end
end end
rescue InvalidTransitionError rescue InvalidTransitionError
# For given input, no transition for current state # For given input, no transition for current state
@ -78,16 +81,9 @@ module DiscourseNarrativeBot
end_reply end_reply
cancel_timeout_job(user) cancel_timeout_job(user)
BadgeGranter.grant( BadgeGranter.grant(Badge.find_by(name: self.class.badge_name), user)
Badge.find_by(name: self.class.badge_name),
user
)
set_data(@user, set_data(@user, topic_id: new_post.topic_id, state: :end, track: self.class.to_s)
topic_id: new_post.topic_id,
state: :end,
track: self.class.to_s
)
end end
end end
rescue => e rescue => e
@ -116,25 +112,29 @@ module DiscourseNarrativeBot
@data = get_data(user) || {} @data = get_data(user) || {}
if post = Post.find_by(id: @data[:last_post_id]) if post = Post.find_by(id: @data[:last_post_id])
reply_to(post, I18n.t("discourse_narrative_bot.timeout.message", reply_to(
i18n_post_args( post,
username: user.username, I18n.t(
skip_trigger: TrackSelector.skip_trigger, "discourse_narrative_bot.timeout.message",
reset_trigger: "#{TrackSelector.reset_trigger} #{self.class.reset_trigger}" i18n_post_args(
) username: user.username,
), {}, skip_send_email: false) skip_trigger: TrackSelector.skip_trigger,
reset_trigger: "#{TrackSelector.reset_trigger} #{self.class.reset_trigger}",
),
),
{},
skip_send_email: false,
)
end end
end end
def certificate(type = nil) def certificate(type = nil)
options = { options = { user_id: @user.id, date: Time.zone.now.strftime("%b %d %Y"), format: :svg }
user_id: @user.id,
date: Time.zone.now.strftime('%b %d %Y'),
format: :svg
}
options.merge!(type: type) if type options.merge!(type: type) if type
src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options) src =
Discourse.base_url +
DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options)
alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt")) alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt"))
"<iframe class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{alt}'></iframe>" "<iframe class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{alt}'></iframe>"
@ -192,7 +192,7 @@ module DiscourseNarrativeBot
end end
def not_implemented def not_implemented
raise 'Not implemented.' raise "Not implemented."
end end
private private

View File

@ -10,7 +10,7 @@ module DiscourseNarrativeBot
begin begin
Date.parse(date) Date.parse(date)
rescue ArgumentError => e rescue ArgumentError => e
if e.message == 'invalid date' if e.message == "invalid date"
Date.parse(Date.today.to_s) Date.parse(Date.today.to_s)
else else
raise e raise e
@ -25,14 +25,20 @@ module DiscourseNarrativeBot
svg_default_width = 538.583 svg_default_width = 538.583
logo_container = logo_group(55, svg_default_width, 280) logo_container = logo_group(55, svg_default_width, 280)
ApplicationController.render(inline: read_template('new_user'), assigns: assign_options(svg_default_width, logo_container)) ApplicationController.render(
inline: read_template("new_user"),
assigns: assign_options(svg_default_width, logo_container),
)
end end
def advanced_user_track def advanced_user_track
svg_default_width = 722.8 svg_default_width = 722.8
logo_container = logo_group(40, svg_default_width, 350) logo_container = logo_group(40, svg_default_width, 350)
ApplicationController.render(inline: read_template('advanced_user'), assigns: assign_options(svg_default_width, logo_container)) ApplicationController.render(
inline: read_template("advanced_user"),
assigns: assign_options(svg_default_width, logo_container),
)
end end
private private
@ -48,7 +54,7 @@ module DiscourseNarrativeBot
date: @date, date: @date,
avatar_url: @avatar_url, avatar_url: @avatar_url,
logo_group: logo_group, logo_group: logo_group,
name: name name: name,
} }
end end

View File

@ -7,25 +7,26 @@ module DiscourseNarrativeBot
def self.roll(num_of_dice, range_of_dice) def self.roll(num_of_dice, range_of_dice)
if num_of_dice == 0 || range_of_dice == 0 if num_of_dice == 0 || range_of_dice == 0
return I18n.t('discourse_narrative_bot.dice.invalid') return I18n.t("discourse_narrative_bot.dice.invalid")
end end
output = +'' output = +""
if num_of_dice > MAXIMUM_NUM_OF_DICE if num_of_dice > MAXIMUM_NUM_OF_DICE
output << I18n.t('discourse_narrative_bot.dice.not_enough_dice', count: MAXIMUM_NUM_OF_DICE) output << I18n.t("discourse_narrative_bot.dice.not_enough_dice", count: MAXIMUM_NUM_OF_DICE)
output << "\n\n" output << "\n\n"
num_of_dice = MAXIMUM_NUM_OF_DICE num_of_dice = MAXIMUM_NUM_OF_DICE
end end
if range_of_dice > MAXIMUM_RANGE_OF_DICE if range_of_dice > MAXIMUM_RANGE_OF_DICE
output << I18n.t('discourse_narrative_bot.dice.out_of_range') output << I18n.t("discourse_narrative_bot.dice.out_of_range")
output << "\n\n" output << "\n\n"
range_of_dice = MAXIMUM_RANGE_OF_DICE range_of_dice = MAXIMUM_RANGE_OF_DICE
end end
output << I18n.t('discourse_narrative_bot.dice.results', output << I18n.t(
results: num_of_dice.times.map { rand(1..range_of_dice) }.join(", ") "discourse_narrative_bot.dice.results",
results: num_of_dice.times.map { rand(1..range_of_dice) }.join(", "),
) )
end end
end end

View File

@ -3,9 +3,10 @@
module DiscourseNarrativeBot module DiscourseNarrativeBot
class Magic8Ball class Magic8Ball
def self.generate_answer def self.generate_answer
I18n.t("discourse_narrative_bot.magic_8_ball.result", result: I18n.t( I18n.t(
"discourse_narrative_bot.magic_8_ball.answers.#{rand(1..20)}" "discourse_narrative_bot.magic_8_ball.result",
)) result: I18n.t("discourse_narrative_bot.magic_8_ball.answers.#{rand(1..20)}"),
)
end end
end end
end end

View File

@ -1,136 +1,136 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'distributed_mutex' require "distributed_mutex"
module DiscourseNarrativeBot module DiscourseNarrativeBot
class NewUserNarrative < Base class NewUserNarrative < Base
I18N_KEY = "discourse_narrative_bot.new_user_narrative".freeze I18N_KEY = "discourse_narrative_bot.new_user_narrative".freeze
BADGE_NAME = 'Certified'.freeze BADGE_NAME = "Certified".freeze
TRANSITION_TABLE = { TRANSITION_TABLE = {
begin: { begin: {
init: { init: {
next_state: :tutorial_bookmark, next_state: :tutorial_bookmark,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.bookmark.instructions", base_uri: Discourse.base_path) }, next_instructions:
action: :say_hello Proc.new { I18n.t("#{I18N_KEY}.bookmark.instructions", base_uri: Discourse.base_path) },
} action: :say_hello,
},
}, },
tutorial_bookmark: { tutorial_bookmark: {
next_state: :tutorial_onebox, next_state: :tutorial_onebox,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.onebox.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.onebox.instructions", base_uri: Discourse.base_path) },
bookmark: { bookmark: {
action: :reply_to_bookmark action: :reply_to_bookmark,
}, },
reply: { reply: {
next_state: :tutorial_bookmark, next_state: :tutorial_bookmark,
action: :missing_bookmark action: :missing_bookmark,
} },
}, },
tutorial_onebox: { tutorial_onebox: {
next_state: :tutorial_emoji, next_state: :tutorial_emoji,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.emoji.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.emoji.instructions", base_uri: Discourse.base_path) },
reply: { reply: {
action: :reply_to_onebox action: :reply_to_onebox,
} },
}, },
tutorial_emoji: { tutorial_emoji: {
prerequisite: Proc.new { SiteSetting.enable_emoji }, prerequisite: Proc.new { SiteSetting.enable_emoji },
next_state: :tutorial_mention, next_state: :tutorial_mention,
next_instructions: Proc.new { next_instructions:
I18n.t("#{I18N_KEY}.mention.instructions", Proc.new do
discobot_username: self.discobot_username, I18n.t(
base_uri: Discourse.base_path) "#{I18N_KEY}.mention.instructions",
}, discobot_username: self.discobot_username,
base_uri: Discourse.base_path,
)
end,
reply: { reply: {
action: :reply_to_emoji action: :reply_to_emoji,
} },
}, },
tutorial_mention: { tutorial_mention: {
prerequisite: Proc.new { SiteSetting.enable_mentions }, prerequisite: Proc.new { SiteSetting.enable_mentions },
next_state: :tutorial_formatting, next_state: :tutorial_formatting,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.formatting.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.formatting.instructions", base_uri: Discourse.base_path) },
reply: { reply: {
action: :reply_to_mention action: :reply_to_mention,
} },
}, },
tutorial_formatting: { tutorial_formatting: {
next_state: :tutorial_quote, next_state: :tutorial_quote,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.quoting.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.quoting.instructions", base_uri: Discourse.base_path) },
reply: { reply: {
action: :reply_to_formatting action: :reply_to_formatting,
} },
}, },
tutorial_quote: { tutorial_quote: {
next_state: :tutorial_images, next_state: :tutorial_images,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.images.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.images.instructions", base_uri: Discourse.base_path) },
reply: { reply: {
action: :reply_to_quote action: :reply_to_quote,
} },
}, },
# Note: tutorial_images and tutorial_likes are mutually exclusive. # Note: tutorial_images and tutorial_likes are mutually exclusive.
# The prerequisites should ensure only one of them is called. # The prerequisites should ensure only one of them is called.
tutorial_images: { tutorial_images: {
prerequisite: Proc.new { @user.has_trust_level?(SiteSetting.min_trust_to_post_embedded_media) }, prerequisite:
Proc.new { @user.has_trust_level?(SiteSetting.min_trust_to_post_embedded_media) },
next_state: :tutorial_likes, next_state: :tutorial_likes,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.likes.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.likes.instructions", base_uri: Discourse.base_path) },
reply: { reply: {
action: :reply_to_image action: :reply_to_image,
}, },
like: { like: {
action: :track_images_like action: :track_images_like,
} },
}, },
tutorial_likes: { tutorial_likes: {
prerequisite: Proc.new { !@user.has_trust_level?(SiteSetting.min_trust_to_post_embedded_media) }, prerequisite:
Proc.new { !@user.has_trust_level?(SiteSetting.min_trust_to_post_embedded_media) },
next_state: :tutorial_flag, next_state: :tutorial_flag,
next_instructions: Proc.new { next_instructions:
I18n.t("#{I18N_KEY}.flag.instructions", Proc.new do
guidelines_url: url_helpers(:guidelines_url), I18n.t(
about_url: url_helpers(:about_index_url), "#{I18N_KEY}.flag.instructions",
base_uri: Discourse.base_path) guidelines_url: url_helpers(:guidelines_url),
}, about_url: url_helpers(:about_index_url),
base_uri: Discourse.base_path,
)
end,
like: { like: {
action: :reply_to_likes action: :reply_to_likes,
}, },
reply: { reply: {
next_state: :tutorial_likes, next_state: :tutorial_likes,
action: :missing_likes_like action: :missing_likes_like,
} },
}, },
tutorial_flag: { tutorial_flag: {
prerequisite: Proc.new { SiteSetting.allow_flagging_staff }, prerequisite: Proc.new { SiteSetting.allow_flagging_staff },
next_state: :tutorial_search, next_state: :tutorial_search,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.search.instructions", base_uri: Discourse.base_path) }, next_instructions:
Proc.new { I18n.t("#{I18N_KEY}.search.instructions", base_uri: Discourse.base_path) },
flag: { flag: {
action: :reply_to_flag action: :reply_to_flag,
}, },
reply: { reply: {
next_state: :tutorial_flag, next_state: :tutorial_flag,
action: :missing_flag action: :missing_flag,
} },
}, },
tutorial_search: { tutorial_search: {
next_state: :end, next_state: :end,
reply: { reply: {
action: :reply_to_search action: :reply_to_search,
} },
} },
} }
def self.badge_name def self.badge_name
@ -138,7 +138,7 @@ module DiscourseNarrativeBot
end end
def self.search_answer def self.search_answer
':herb:' ":herb:"
end end
def self.search_answer_emoji def self.search_answer_emoji
@ -146,7 +146,7 @@ module DiscourseNarrativeBot
end end
def self.reset_trigger def self.reset_trigger
I18n.t('discourse_narrative_bot.new_user_narrative.reset_trigger') I18n.t("discourse_narrative_bot.new_user_narrative.reset_trigger")
end end
def reset_bot(user, post) def reset_bot(user, post)
@ -173,7 +173,7 @@ module DiscourseNarrativeBot
topic = @post.topic topic = @post.topic
post = topic.first_post post = topic.first_post
MessageBus.publish('/new_user_narrative/tutorial_search', {}, user_ids: [@user.id]) MessageBus.publish("/new_user_narrative/tutorial_search", {}, user_ids: [@user.id])
raw = <<~MD raw = <<~MD
#{post.raw} #{post.raw}
@ -184,7 +184,8 @@ module DiscourseNarrativeBot
PostRevisor.new(post, topic).revise!( PostRevisor.new(post, topic).revise!(
self.discobot_user, self.discobot_user,
{ raw: raw }, { raw: raw },
skip_validations: true, force_new_version: true skip_validations: true,
force_new_version: true,
) )
set_state_data(:post_version, post.reload.version || 0) set_state_data(:post_version, post.reload.version || 0)
@ -198,13 +199,11 @@ module DiscourseNarrativeBot
end end
def say_hello def say_hello
raw = I18n.t( raw =
"#{I18N_KEY}.hello.message", I18n.t(
i18n_post_args( "#{I18N_KEY}.hello.message",
username: @user.username, i18n_post_args(username: @user.username, title: SiteSetting.title),
title: SiteSetting.title
) )
)
raw = <<~MD raw = <<~MD
#{raw} #{raw}
@ -213,9 +212,7 @@ module DiscourseNarrativeBot
MD MD
title = I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title) title = I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title)
if SiteSetting.max_emojis_in_title == 0 title = title.gsub(/:([\w\-+]+(?::t\d)?):/, "").strip if SiteSetting.max_emojis_in_title == 0
title = title.gsub(/:([\w\-+]+(?::t\d)?):/, '').strip
end
opts = { opts = {
title: title, title: title,
@ -224,17 +221,13 @@ module DiscourseNarrativeBot
subtype: TopicSubtype.system_message, subtype: TopicSubtype.system_message,
} }
if @post && if @post && @post.topic.private_message? &&
@post.topic.private_message? && @post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
opts = opts.merge(topic_id: @post.topic_id) opts = opts.merge(topic_id: @post.topic_id)
end end
if @data[:topic_id] if @data[:topic_id]
opts = opts opts = opts.merge(topic_id: @data[:topic_id]).except(:title, :target_usernames, :archetype)
.merge(topic_id: @data[:topic_id])
.except(:title, :target_usernames, :archetype)
end end
post = reply_to(@post, raw, opts) post = reply_to(@post, raw, opts)
@ -249,7 +242,9 @@ module DiscourseNarrativeBot
fake_delay fake_delay
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
reply_to(@post, I18n.t("#{I18N_KEY}.bookmark.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.bookmark.not_found", i18n_post_args))
end
false false
end end
@ -292,7 +287,9 @@ module DiscourseNarrativeBot
reply reply
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.onebox.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.onebox.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -302,11 +299,12 @@ module DiscourseNarrativeBot
post_topic_id = @post.topic_id post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id) return unless valid_topic?(post_topic_id)
post_liked = PostAction.exists?( post_liked =
post_action_type_id: PostActionType.types[:like], PostAction.exists?(
post_id: @data[:last_post_id], post_action_type_id: PostActionType.types[:like],
user_id: @user.id post_id: @data[:last_post_id],
) user_id: @user.id,
)
if post_liked if post_liked
set_state_data(:liked, true) set_state_data(:liked, true)
@ -358,18 +356,23 @@ module DiscourseNarrativeBot
like_post(@post) like_post(@post)
else else
raw = I18n.t( raw =
"#{I18N_KEY}.images.like_not_found", I18n.t(
i18n_post_args(url: Post.find_by(id: @data[:last_post_id]).url) "#{I18N_KEY}.images.like_not_found",
) i18n_post_args(url: Post.find_by(id: @data[:last_post_id]).url),
)
transition = false transition = false
end end
else else
raw = I18n.t( raw =
"#{I18N_KEY}.images.not_found", I18n.t(
i18n_post_args(image_url: "#{Discourse.base_url}/plugins/discourse-narrative-bot/images/dog-walk.gif") "#{I18N_KEY}.images.not_found",
) i18n_post_args(
image_url:
"#{Discourse.base_url}/plugins/discourse-narrative-bot/images/dog-walk.gif",
),
)
transition = false transition = false
end end
@ -398,11 +401,12 @@ module DiscourseNarrativeBot
post_topic_id = @post.topic_id post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id) return unless valid_topic?(post_topic_id)
post_liked = PostAction.exists?( post_liked =
post_action_type_id: PostActionType.types[:like], PostAction.exists?(
post_id: @data[:last_post_id], post_action_type_id: PostActionType.types[:like],
user_id: @user.id post_id: @data[:last_post_id],
) user_id: @user.id,
)
if post_liked if post_liked
raw = <<~MD raw = <<~MD
@ -425,7 +429,10 @@ module DiscourseNarrativeBot
post_topic_id = @post.topic_id post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id) return unless valid_topic?(post_topic_id)
if Nokogiri::HTML5.fragment(@post.cooked).css("b", "strong", "em", "i", ".bbcode-i", ".bbcode-b").size > 0 if Nokogiri::HTML5
.fragment(@post.cooked)
.css("b", "strong", "em", "i", ".bbcode-i", ".bbcode-b")
.size > 0
raw = <<~MD raw = <<~MD
#{I18n.t("#{I18N_KEY}.formatting.reply", i18n_post_args)} #{I18n.t("#{I18N_KEY}.formatting.reply", i18n_post_args)}
@ -439,7 +446,9 @@ module DiscourseNarrativeBot
reply reply
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.formatting.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.formatting.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -465,7 +474,9 @@ module DiscourseNarrativeBot
reply reply
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.quoting.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.quoting.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -491,7 +502,9 @@ module DiscourseNarrativeBot
reply reply
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.emoji.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.emoji.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -517,13 +530,13 @@ module DiscourseNarrativeBot
fake_delay fake_delay
unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t( reply_to(
"#{I18N_KEY}.mention.not_found", @post,
i18n_post_args( I18n.t(
username: @user.username, "#{I18N_KEY}.mention.not_found",
discobot_username: self.discobot_username i18n_post_args(username: @user.username, discobot_username: self.discobot_username),
) ),
)) )
end end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
@ -536,9 +549,13 @@ module DiscourseNarrativeBot
# Remove any incorrect flags so that they can try again # Remove any incorrect flags so that they can try again
if @post.user_id == -2 if @post.user_id == -2
@post.post_actions @post
.post_actions
.where(user_id: @user.id) .where(user_id: @user.id)
.where("post_action_type_id IN (?)", (PostActionType.flag_types.values - [PostActionType.types[:inappropriate]])) .where(
"post_action_type_id IN (?)",
(PostActionType.flag_types.values - [PostActionType.types[:inappropriate]]),
)
.destroy_all .destroy_all
end end
@ -571,12 +588,18 @@ module DiscourseNarrativeBot
post_topic_id = @post.topic_id post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id) return unless valid_topic?(post_topic_id)
if @post.raw.include?(NewUserNarrative.search_answer) || @post.raw.include?(NewUserNarrative.search_answer_emoji) if @post.raw.include?(NewUserNarrative.search_answer) ||
@post.raw.include?(NewUserNarrative.search_answer_emoji)
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.search.reply", i18n_post_args(search_url: url_helpers(:search_url)))) reply_to(
@post,
I18n.t("#{I18N_KEY}.search.reply", i18n_post_args(search_url: url_helpers(:search_url))),
)
else else
fake_delay fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.search.not_found", i18n_post_args)) unless @data[:attempted] unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.search.not_found", i18n_post_args))
end
enqueue_timeout_job(@user) enqueue_timeout_job(@user)
false false
end end
@ -587,16 +610,17 @@ module DiscourseNarrativeBot
reply_to( reply_to(
@post, @post,
I18n.t("#{I18N_KEY}.end.message", I18n.t(
"#{I18N_KEY}.end.message",
i18n_post_args( i18n_post_args(
username: @user.username, username: @user.username,
base_url: Discourse.base_url, base_url: Discourse.base_url,
certificate: certificate, certificate: certificate,
discobot_username: self.discobot_username, discobot_username: self.discobot_username,
advanced_trigger: AdvancedUserNarrative.reset_trigger advanced_trigger: AdvancedUserNarrative.reset_trigger,
) ),
), ),
topic_id: @data[:topic_id] topic_id: @data[:topic_id],
) )
end end
@ -605,15 +629,12 @@ module DiscourseNarrativeBot
end end
def welcome_topic def welcome_topic
Topic.find_by(slug: 'welcome-to-discourse', archetype: Archetype.default) || Topic.find_by(slug: "welcome-to-discourse", archetype: Archetype.default) ||
Topic.recent(1).first Topic.recent(1).first
end end
def url_helpers(url, opts = {}) def url_helpers(url, opts = {})
Rails.application.routes.url_helpers.public_send( Rails.application.routes.url_helpers.public_send(url, opts.merge(host: Discourse.base_url))
url,
opts.merge(host: Discourse.base_url)
)
end end
end end
end end

View File

@ -1,24 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'excon' require "excon"
module DiscourseNarrativeBot module DiscourseNarrativeBot
class QuoteGenerator class QuoteGenerator
API_ENDPOINT = 'http://api.forismatic.com/api/1.0/'.freeze API_ENDPOINT = "http://api.forismatic.com/api/1.0/".freeze
def self.format_quote(quote, author) def self.format_quote(quote, author)
I18n.t('discourse_narrative_bot.quote.results', quote: quote, author: author) I18n.t("discourse_narrative_bot.quote.results", quote: quote, author: author)
end end
def self.generate(user) def self.generate(user)
quote, author = quote, author =
if !user.effective_locale.start_with?('en') if !user.effective_locale.start_with?("en")
translation_key = "discourse_narrative_bot.quote.#{rand(1..10)}" translation_key = "discourse_narrative_bot.quote.#{rand(1..10)}"
[ [I18n.t("#{translation_key}.quote"), I18n.t("#{translation_key}.author")]
I18n.t("#{translation_key}.quote"),
I18n.t("#{translation_key}.author")
]
else else
connection = Excon.new("#{API_ENDPOINT}?lang=en&format=json&method=getQuote") connection = Excon.new("#{API_ENDPOINT}?lang=en&format=json&method=getQuote")
response = connection.request(expects: [200, 201], method: :Get) response = connection.request(expects: [200, 201], method: :Get)

View File

@ -4,18 +4,12 @@ module DiscourseNarrativeBot
class TrackSelector class TrackSelector
include Actions include Actions
GENERIC_REPLIES_COUNT_PREFIX = 'discourse-narrative-bot:track-selector-count:'.freeze GENERIC_REPLIES_COUNT_PREFIX = "discourse-narrative-bot:track-selector-count:".freeze
PUBLIC_DISPLAY_BOT_HELP_KEY = 'discourse-narrative-bot:track-selector:display-bot-help'.freeze PUBLIC_DISPLAY_BOT_HELP_KEY = "discourse-narrative-bot:track-selector:display-bot-help".freeze
TRACKS = [ TRACKS = [AdvancedUserNarrative, NewUserNarrative]
AdvancedUserNarrative,
NewUserNarrative
]
TOPIC_ACTIONS = [ TOPIC_ACTIONS = %i[delete topic_notification_level_changed].each(&:freeze)
:delete,
:topic_notification_level_changed
].each(&:freeze)
RESET_TRIGGER_EXACT_MATCH_LENGTH = 200 RESET_TRIGGER_EXACT_MATCH_LENGTH = 200
@ -118,7 +112,7 @@ module DiscourseNarrativeBot
trigger = "#{self.class.reset_trigger} #{klass.reset_trigger}" trigger = "#{self.class.reset_trigger} #{klass.reset_trigger}"
if @post.raw.length < RESET_TRIGGER_EXACT_MATCH_LENGTH && @is_pm_to_bot if @post.raw.length < RESET_TRIGGER_EXACT_MATCH_LENGTH && @is_pm_to_bot
@post.raw.match(Regexp.new("\\b\\W\?#{trigger}\\W\?\\b", 'i')) @post.raw.match(Regexp.new("\\b\\W\?#{trigger}\\W\?\\b", "i"))
else else
match_trigger?(trigger) match_trigger?(trigger)
end end
@ -127,7 +121,7 @@ module DiscourseNarrativeBot
def bot_commands(hint = true) def bot_commands(hint = true)
raw = raw =
if @user.manually_disabled_discobot? if @user.manually_disabled_discobot?
I18n.t(self.class.i18n_key('random_mention.discobot_disabled')) I18n.t(self.class.i18n_key("random_mention.discobot_disabled"))
elsif match_data = match_trigger?("#{self.class.dice_trigger} (\\d+)d(\\d+)") elsif match_data = match_trigger?("#{self.class.dice_trigger} (\\d+)d(\\d+)")
DiscourseNarrativeBot::Dice.roll(match_data[1].to_i, match_data[2].to_i) DiscourseNarrativeBot::Dice.roll(match_data[1].to_i, match_data[2].to_i)
elsif match_trigger?(self.class.quote_trigger) elsif match_trigger?(self.class.quote_trigger)
@ -137,20 +131,23 @@ module DiscourseNarrativeBot
elsif match_trigger?(self.class.help_trigger) elsif match_trigger?(self.class.help_trigger)
help_message help_message
elsif hint elsif hint
message = I18n.t(self.class.i18n_key('random_mention.reply'), message =
discobot_username: self.discobot_username, I18n.t(
help_trigger: self.class.help_trigger self.class.i18n_key("random_mention.reply"),
) discobot_username: self.discobot_username,
help_trigger: self.class.help_trigger,
)
if public_reply? if public_reply?
key = "#{PUBLIC_DISPLAY_BOT_HELP_KEY}:#{@post.topic_id}" key = "#{PUBLIC_DISPLAY_BOT_HELP_KEY}:#{@post.topic_id}"
last_bot_help_post_number = Discourse.redis.get(key) last_bot_help_post_number = Discourse.redis.get(key)
if !last_bot_help_post_number || if !last_bot_help_post_number ||
(last_bot_help_post_number && (
@post.post_number - 10 > last_bot_help_post_number.to_i && last_bot_help_post_number &&
(1.day.to_i - Discourse.redis.ttl(key)) > 6.hours.to_i) @post.post_number - 10 > last_bot_help_post_number.to_i &&
(1.day.to_i - Discourse.redis.ttl(key)) > 6.hours.to_i
)
Discourse.redis.setex(key, 1.day.to_i, @post.post_number) Discourse.redis.setex(key, 1.day.to_i, @post.post_number)
message message
end end
@ -166,20 +163,24 @@ module DiscourseNarrativeBot
end end
def help_message def help_message
message = I18n.t( message =
self.class.i18n_key('random_mention.tracks'), I18n.t(
discobot_username: self.discobot_username, self.class.i18n_key("random_mention.tracks"),
reset_trigger: self.class.reset_trigger, discobot_username: self.discobot_username,
tracks: [NewUserNarrative.reset_trigger, AdvancedUserNarrative.reset_trigger].join(', ') reset_trigger: self.class.reset_trigger,
) tracks: [NewUserNarrative.reset_trigger, AdvancedUserNarrative.reset_trigger].join(", "),
)
message << "\n\n#{I18n.t(self.class.i18n_key('random_mention.bot_actions'), message << "\n\n#{
discobot_username: self.discobot_username, I18n.t(
dice_trigger: self.class.dice_trigger, self.class.i18n_key("random_mention.bot_actions"),
quote_trigger: self.class.quote_trigger, discobot_username: self.discobot_username,
quote_sample: DiscourseNarrativeBot::QuoteGenerator.generate(@user), dice_trigger: self.class.dice_trigger,
magic_8_ball_trigger: self.class.magic_8_ball_trigger quote_trigger: self.class.quote_trigger,
)}" quote_sample: DiscourseNarrativeBot::QuoteGenerator.generate(@user),
magic_8_ball_trigger: self.class.magic_8_ball_trigger,
)
}"
end end
def generic_replies_key(user) def generic_replies_key(user)
@ -193,18 +194,23 @@ module DiscourseNarrativeBot
case count case count
when 0 when 0
raw = I18n.t(self.class.i18n_key('do_not_understand.first_response')) raw = I18n.t(self.class.i18n_key("do_not_understand.first_response"))
if state && state.to_sym != :end if state && state.to_sym != :end
raw = "#{raw}\n\n#{I18n.t(self.class.i18n_key('do_not_understand.track_response'), reset_trigger: reset_trigger, skip_trigger: self.class.skip_trigger)}" raw =
"#{raw}\n\n#{I18n.t(self.class.i18n_key("do_not_understand.track_response"), reset_trigger: reset_trigger, skip_trigger: self.class.skip_trigger)}"
end end
reply_to(@post, raw) reply_to(@post, raw)
when 1 when 1
reply_to(@post, I18n.t(self.class.i18n_key('do_not_understand.second_response'), reply_to(
base_path: Discourse.base_path, @post,
reset_trigger: self.class.reset_trigger I18n.t(
)) self.class.i18n_key("do_not_understand.second_response"),
base_path: Discourse.base_path,
reset_trigger: self.class.reset_trigger,
),
)
else else
# Stay out of the user's way # Stay out of the user's way
end end
@ -218,7 +224,9 @@ module DiscourseNarrativeBot
def skip_track? def skip_track?
if @is_pm_to_bot if @is_pm_to_bot
@post.raw.match(/((^@#{self.discobot_username} #{self.class.skip_trigger})|(^#{self.class.skip_trigger}$))/i) @post.raw.match(
/((^@#{self.discobot_username} #{self.class.skip_trigger})|(^#{self.class.skip_trigger}$))/i,
)
else else
false false
end end
@ -233,24 +241,23 @@ module DiscourseNarrativeBot
def match_trigger?(trigger) def match_trigger?(trigger)
# we remove the leading <p> to allow for trigger to be at the end of a paragraph # we remove the leading <p> to allow for trigger to be at the end of a paragraph
cooked_trigger = cook(trigger)[3..-1] cooked_trigger = cook(trigger)[3..-1]
regexp = Regexp.new(cooked_trigger, 'i') regexp = Regexp.new(cooked_trigger, "i")
match = @post.cooked.match(regexp) match = @post.cooked.match(regexp)
if @is_pm_to_bot if @is_pm_to_bot
match || @post.raw.strip.match(Regexp.new("^#{trigger}$", 'i')) match || @post.raw.strip.match(Regexp.new("^#{trigger}$", "i"))
else else
match match
end end
end end
def like_user_post def like_user_post
if @post.raw.match(/thank/i) PostActionCreator.like(self.discobot_user, @post) if @post.raw.match(/thank/i)
PostActionCreator.like(self.discobot_user, @post)
end
end end
def bot_mentioned? def bot_mentioned?
@bot_mentioned ||= PostAnalyzer.new(@post.raw, @post.topic_id).raw_mentions.include?(self.discobot_username) @bot_mentioned ||=
PostAnalyzer.new(@post.raw, @post.topic_id).raw_mentions.include?(self.discobot_username)
end end
def public_reply? def public_reply?

View File

@ -8,8 +8,14 @@ module DiscourseNarrativeBot
def self.values def self.values
@values ||= [ @values ||= [
{ name: 'discourse_narrative_bot.welcome_post_type.new_user_track', value: 'new_user_track' }, {
{ name: 'discourse_narrative_bot.welcome_post_type.welcome_message', value: 'welcome_message' } name: "discourse_narrative_bot.welcome_post_type.new_user_track",
value: "new_user_track",
},
{
name: "discourse_narrative_bot.welcome_post_type.welcome_message",
value: "welcome_message",
},
] ]
end end

View File

@ -18,34 +18,37 @@ if Rails.env == "development"
# 3. we have a post_edited hook that queues a job for bot input # 3. we have a post_edited hook that queues a job for bot input
# 4. if you are not running sidekiq in dev every time you save a post it will trigger it # 4. if you are not running sidekiq in dev every time you save a post it will trigger it
# 5. but the constant can not be autoloaded # 5. but the constant can not be autoloaded
Rails.configuration.autoload_paths << File.expand_path('../autoload/jobs', __FILE__) Rails.configuration.autoload_paths << File.expand_path("../autoload/jobs", __FILE__)
end end
require_relative 'lib/discourse_narrative_bot/welcome_post_type_site_setting.rb' require_relative "lib/discourse_narrative_bot/welcome_post_type_site_setting.rb"
register_asset 'stylesheets/discourse-narrative-bot.scss' register_asset "stylesheets/discourse-narrative-bot.scss"
after_initialize do after_initialize do
SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-narrative-bot", "db", "fixtures").to_s SeedFu.fixture_paths << Rails
.root
.join("plugins", "discourse-narrative-bot", "db", "fixtures")
.to_s
Mime::Type.register "image/svg+xml", :svg Mime::Type.register "image/svg+xml", :svg
[ %w[
'../autoload/jobs/regular/bot_input.rb', ../autoload/jobs/regular/bot_input.rb
'../autoload/jobs/regular/narrative_timeout.rb', ../autoload/jobs/regular/narrative_timeout.rb
'../autoload/jobs/regular/narrative_init.rb', ../autoload/jobs/regular/narrative_init.rb
'../autoload/jobs/regular/send_default_welcome_message.rb', ../autoload/jobs/regular/send_default_welcome_message.rb
'../autoload/jobs/onceoff/discourse_narrative_bot/grant_badges.rb', ../autoload/jobs/onceoff/discourse_narrative_bot/grant_badges.rb
'../autoload/jobs/onceoff/discourse_narrative_bot/remap_old_bot_images.rb', ../autoload/jobs/onceoff/discourse_narrative_bot/remap_old_bot_images.rb
'../lib/discourse_narrative_bot/actions.rb', ../lib/discourse_narrative_bot/actions.rb
'../lib/discourse_narrative_bot/base.rb', ../lib/discourse_narrative_bot/base.rb
'../lib/discourse_narrative_bot/new_user_narrative.rb', ../lib/discourse_narrative_bot/new_user_narrative.rb
'../lib/discourse_narrative_bot/advanced_user_narrative.rb', ../lib/discourse_narrative_bot/advanced_user_narrative.rb
'../lib/discourse_narrative_bot/track_selector.rb', ../lib/discourse_narrative_bot/track_selector.rb
'../lib/discourse_narrative_bot/certificate_generator.rb', ../lib/discourse_narrative_bot/certificate_generator.rb
'../lib/discourse_narrative_bot/dice.rb', ../lib/discourse_narrative_bot/dice.rb
'../lib/discourse_narrative_bot/quote_generator.rb', ../lib/discourse_narrative_bot/quote_generator.rb
'../lib/discourse_narrative_bot/magic_8_ball.rb', ../lib/discourse_narrative_bot/magic_8_ball.rb
'../lib/discourse_narrative_bot/welcome_post_type_site_setting.rb' ../lib/discourse_narrative_bot/welcome_post_type_site_setting.rb
].each { |path| load File.expand_path(path, __FILE__) } ].each { |path| load File.expand_path(path, __FILE__) }
RailsMultisite::ConnectionManagement.safe_each_connection do RailsMultisite::ConnectionManagement.safe_each_connection do
@ -55,12 +58,13 @@ after_initialize do
certificate_path = "#{Discourse.base_url}/discobot/certificate.svg" certificate_path = "#{Discourse.base_url}/discobot/certificate.svg"
if !SiteSetting.allowed_iframes.include?(certificate_path) if !SiteSetting.allowed_iframes.include?(certificate_path)
SiteSetting.allowed_iframes = SiteSetting.allowed_iframes.split('|').append(certificate_path).join('|') SiteSetting.allowed_iframes =
SiteSetting.allowed_iframes.split("|").append(certificate_path).join("|")
end end
end end
end end
require_dependency 'plugin_store' require_dependency "plugin_store"
module ::DiscourseNarrativeBot module ::DiscourseNarrativeBot
PLUGIN_NAME = "discourse-narrative-bot".freeze PLUGIN_NAME = "discourse-narrative-bot".freeze
@ -94,13 +98,15 @@ after_initialize do
immutable_for(24.hours) immutable_for(24.hours)
%i[date user_id].each do |key| %i[date user_id].each do |key|
raise Discourse::InvalidParameters.new("#{key} must be present") unless params[key]&.present? unless params[key]&.present?
raise Discourse::InvalidParameters.new("#{key} must be present")
end
end end
if params[:user_id].to_i != current_user.id if params[:user_id].to_i != current_user.id
rate_limiter = RateLimiter.new(current_user, 'svg_certificate', 3, 1.minute) rate_limiter = RateLimiter.new(current_user, "svg_certificate", 3, 1.minute)
else else
rate_limiter = RateLimiter.new(current_user, 'svg_certificate_self', 30, 10.minutes) rate_limiter = RateLimiter.new(current_user, "svg_certificate_self", 30, 10.minutes)
end end
rate_limiter.performed! unless current_user.staff? rate_limiter.performed! unless current_user.staff?
@ -110,33 +116,28 @@ after_initialize do
hijack do hijack do
generator = CertificateGenerator.new(user, params[:date], avatar_url(user)) generator = CertificateGenerator.new(user, params[:date], avatar_url(user))
svg = params[:type] == 'advanced' ? generator.advanced_user_track : generator.new_user_track svg =
params[:type] == "advanced" ? generator.advanced_user_track : generator.new_user_track
respond_to do |format| respond_to { |format| format.svg { render inline: svg } }
format.svg { render inline: svg }
end
end end
end end
private private
def avatar_url(user) def avatar_url(user)
UrlHelper.absolute(Discourse.base_path + user.avatar_template.gsub('{size}', '250')) UrlHelper.absolute(Discourse.base_path + user.avatar_template.gsub("{size}", "250"))
end end
end end
end end
DiscourseNarrativeBot::Engine.routes.draw do DiscourseNarrativeBot::Engine.routes.draw do
get "/certificate" => "certificates#generate", format: :svg get "/certificate" => "certificates#generate", :format => :svg
end end
Discourse::Application.routes.append do Discourse::Application.routes.append { mount ::DiscourseNarrativeBot::Engine, at: "/discobot" }
mount ::DiscourseNarrativeBot::Engine, at: "/discobot"
end
self.add_model_callback(User, :after_destroy) do self.add_model_callback(User, :after_destroy) { DiscourseNarrativeBot::Store.remove(self.id) }
DiscourseNarrativeBot::Store.remove(self.id)
end
self.on(:user_created) do |user| self.on(:user_created) do |user|
if SiteSetting.discourse_narrative_bot_welcome_post_delay == 0 && !user.staged if SiteSetting.discourse_narrative_bot_welcome_post_delay == 0 && !user.staged
@ -145,19 +146,13 @@ after_initialize do
end end
self.on(:user_first_logged_in) do |user| self.on(:user_first_logged_in) do |user|
if SiteSetting.discourse_narrative_bot_welcome_post_delay > 0 user.enqueue_bot_welcome_post if SiteSetting.discourse_narrative_bot_welcome_post_delay > 0
user.enqueue_bot_welcome_post
end
end end
self.on(:user_unstaged) do |user| self.on(:user_unstaged) { |user| user.enqueue_bot_welcome_post }
user.enqueue_bot_welcome_post
end
self.add_model_callback(UserOption, :after_save) do self.add_model_callback(UserOption, :after_save) do
if saved_change_to_skip_new_user_tips? && self.skip_new_user_tips user.delete_bot_welcome_post if saved_change_to_skip_new_user_tips? && self.skip_new_user_tips
user.delete_bot_welcome_post
end
end end
self.add_to_class(:user, :enqueue_bot_welcome_post) do self.add_to_class(:user, :enqueue_bot_welcome_post) do
@ -166,28 +161,29 @@ after_initialize do
delay = SiteSetting.discourse_narrative_bot_welcome_post_delay delay = SiteSetting.discourse_narrative_bot_welcome_post_delay
case SiteSetting.discourse_narrative_bot_welcome_post_type case SiteSetting.discourse_narrative_bot_welcome_post_type
when 'new_user_track' when "new_user_track"
if enqueue_narrative_bot_job? && !manually_disabled_discobot? if enqueue_narrative_bot_job? && !manually_disabled_discobot?
Jobs.enqueue_in(delay, :narrative_init, Jobs.enqueue_in(
delay,
:narrative_init,
user_id: self.id, user_id: self.id,
klass: DiscourseNarrativeBot::NewUserNarrative.to_s klass: DiscourseNarrativeBot::NewUserNarrative.to_s,
) )
end end
when 'welcome_message' when "welcome_message"
Jobs.enqueue_in(delay, :send_default_welcome_message, user_id: self.id) Jobs.enqueue_in(delay, :send_default_welcome_message, user_id: self.id)
end end
end end
self.add_to_class(:user, :manually_disabled_discobot?) do self.add_to_class(:user, :manually_disabled_discobot?) { user_option&.skip_new_user_tips }
user_option&.skip_new_user_tips
end
self.add_to_class(:user, :enqueue_narrative_bot_job?) do self.add_to_class(:user, :enqueue_narrative_bot_job?) do
SiteSetting.discourse_narrative_bot_enabled && SiteSetting.discourse_narrative_bot_enabled && self.human? && !self.anonymous? &&
self.human? &&
!self.anonymous? &&
!self.staged && !self.staged &&
!SiteSetting.discourse_narrative_bot_ignored_usernames.split('|'.freeze).include?(self.username) !SiteSetting
.discourse_narrative_bot_ignored_usernames
.split("|".freeze)
.include?(self.username)
end end
self.add_to_class(:user, :delete_bot_welcome_post) do self.add_to_class(:user, :delete_bot_welcome_post) do
@ -219,42 +215,31 @@ after_initialize do
user = post.user user = post.user
if user&.enqueue_narrative_bot_job? && !options[:skip_bot] if user&.enqueue_narrative_bot_job? && !options[:skip_bot]
Jobs.enqueue(:bot_input, Jobs.enqueue(:bot_input, user_id: user.id, post_id: post.id, input: "reply")
user_id: user.id,
post_id: post.id,
input: "reply"
)
end end
end end
self.on(:post_edited) do |post| self.on(:post_edited) do |post|
if post.user&.enqueue_narrative_bot_job? if post.user&.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input, Jobs.enqueue(:bot_input, user_id: post.user.id, post_id: post.id, input: "edit")
user_id: post.user.id,
post_id: post.id,
input: "edit"
)
end end
end end
self.on(:post_destroyed) do |post, options, user| self.on(:post_destroyed) do |post, options, user|
if user&.enqueue_narrative_bot_job? && !options[:skip_bot] if user&.enqueue_narrative_bot_job? && !options[:skip_bot]
Jobs.enqueue(:bot_input, Jobs.enqueue(
:bot_input,
user_id: user.id, user_id: user.id,
post_id: post.id, post_id: post.id,
topic_id: post.topic_id, topic_id: post.topic_id,
input: "delete" input: "delete",
) )
end end
end end
self.on(:post_recovered) do |post, _, user| self.on(:post_recovered) do |post, _, user|
if user&.enqueue_narrative_bot_job? if user&.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input, Jobs.enqueue(:bot_input, user_id: user.id, post_id: post.id, input: "recover")
user_id: user.id,
post_id: post.id,
input: "recover"
)
end end
end end
@ -268,20 +253,19 @@ after_initialize do
"like" "like"
end end
if input Jobs.enqueue(:bot_input, user_id: self.user.id, post_id: self.post.id, input: input) if input
Jobs.enqueue(:bot_input,
user_id: self.user.id,
post_id: self.post.id,
input: input
)
end
end end
end end
self.add_model_callback(Bookmark, :after_commit, on: :create) do self.add_model_callback(Bookmark, :after_commit, on: :create) do
if self.user.enqueue_narrative_bot_job? if self.user.enqueue_narrative_bot_job?
if self.bookmarkable_type == "Post" if self.bookmarkable_type == "Post"
Jobs.enqueue(:bot_input, user_id: self.user_id, post_id: self.bookmarkable_id, input: "bookmark") Jobs.enqueue(
:bot_input,
user_id: self.user_id,
post_id: self.bookmarkable_id,
input: "bookmark",
)
end end
end end
end end
@ -290,31 +274,36 @@ after_initialize do
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
if user && user.enqueue_narrative_bot_job? if user && user.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input, Jobs.enqueue(
:bot_input,
user_id: user_id, user_id: user_id,
topic_id: topic_id, topic_id: topic_id,
input: "topic_notification_level_changed" input: "topic_notification_level_changed",
) )
end end
end end
UserAvatar.register_custom_user_gravatar_email_hash( UserAvatar.register_custom_user_gravatar_email_hash(
DiscourseNarrativeBot::BOT_USER_ID, DiscourseNarrativeBot::BOT_USER_ID,
"discobot@discourse.org" "discobot@discourse.org",
) )
self.on(:system_message_sent) do |args| self.on(:system_message_sent) do |args|
next if !SiteSetting.discourse_narrative_bot_enabled next if !SiteSetting.discourse_narrative_bot_enabled
next if args[:message_type] != 'tl2_promotion_message' next if args[:message_type] != "tl2_promotion_message"
recipient = args[:post].topic.topic_users.where.not(user_id: args[:post].user_id).last&.user recipient = args[:post].topic.topic_users.where.not(user_id: args[:post].user_id).last&.user
recipient ||= Discourse.site_contact_user if args[:post].user == Discourse.site_contact_user recipient ||= Discourse.site_contact_user if args[:post].user == Discourse.site_contact_user
next if recipient.nil? next if recipient.nil?
I18n.with_locale(recipient.effective_locale) do I18n.with_locale(recipient.effective_locale) do
raw = I18n.t("discourse_narrative_bot.tl2_promotion_message.text_body_template", raw =
discobot_username: ::DiscourseNarrativeBot::Base.new.discobot_username, I18n.t(
reset_trigger: "#{::DiscourseNarrativeBot::TrackSelector.reset_trigger} #{::DiscourseNarrativeBot::AdvancedUserNarrative.reset_trigger}") "discourse_narrative_bot.tl2_promotion_message.text_body_template",
discobot_username: ::DiscourseNarrativeBot::Base.new.discobot_username,
reset_trigger:
"#{::DiscourseNarrativeBot::TrackSelector.reset_trigger} #{::DiscourseNarrativeBot::AdvancedUserNarrative.reset_trigger}",
)
PostCreator.create!( PostCreator.create!(
::DiscourseNarrativeBot::Base.new.discobot_user, ::DiscourseNarrativeBot::Base.new.discobot_user,
@ -322,7 +311,7 @@ after_initialize do
raw: raw, raw: raw,
skip_validations: true, skip_validations: true,
archetype: Archetype.private_message, archetype: Archetype.private_message,
target_usernames: recipient.username target_usernames: recipient.username,
) )
end end
end end
@ -331,12 +320,12 @@ after_initialize do
alias_method :existing_can_create_post?, :can_create_post? alias_method :existing_can_create_post?, :can_create_post?
def can_create_post?(parent) def can_create_post?(parent)
return true if SiteSetting.discourse_narrative_bot_enabled && if SiteSetting.discourse_narrative_bot_enabled && parent.try(:subtype) == "system_message" &&
parent.try(:subtype) == "system_message" && parent.try(:user) == ::DiscourseNarrativeBot::Base.new.discobot_user
parent.try(:user) == ::DiscourseNarrativeBot::Base.new.discobot_user return true
end
existing_can_create_post?(parent) existing_can_create_post?(parent)
end end
end end
end end

View File

@ -1,28 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe DiscourseNarrativeBot::Store do RSpec.describe DiscourseNarrativeBot::Store do
describe '.set' do describe ".set" do
it 'should set the right value in the plugin store' do it "should set the right value in the plugin store" do
key = 'somekey' key = "somekey"
described_class.set(key, 'yay') described_class.set(key, "yay")
plugin_store_row = PluginStoreRow.last plugin_store_row = PluginStoreRow.last
expect(plugin_store_row.value).to eq('yay') expect(plugin_store_row.value).to eq("yay")
expect(plugin_store_row.plugin_name).to eq(DiscourseNarrativeBot::PLUGIN_NAME) expect(plugin_store_row.plugin_name).to eq(DiscourseNarrativeBot::PLUGIN_NAME)
expect(plugin_store_row.key).to eq(key) expect(plugin_store_row.key).to eq(key)
end end
end end
describe '.get' do describe ".get" do
it 'should get the right value from the plugin store' do it "should get the right value from the plugin store" do
PluginStoreRow.create!( PluginStoreRow.create!(
plugin_name: DiscourseNarrativeBot::PLUGIN_NAME, plugin_name: DiscourseNarrativeBot::PLUGIN_NAME,
key: 'somekey', key: "somekey",
value: 'yay', value: "yay",
type_name: 'string' type_name: "string",
) )
expect(described_class.get('somekey')).to eq('yay') expect(described_class.get("somekey")).to eq("yay")
end end
end end
end end

View File

@ -5,13 +5,16 @@ RSpec.describe Jobs::DiscourseNarrativeBot::GrantBadges do
let(:other_user) { Fabricate(:user) } let(:other_user) { Fabricate(:user) }
before do before do
DiscourseNarrativeBot::Store.set(user.id, completed: [ DiscourseNarrativeBot::Store.set(
DiscourseNarrativeBot::NewUserNarrative.to_s, user.id,
DiscourseNarrativeBot::AdvancedUserNarrative.to_s completed: [
]) DiscourseNarrativeBot::NewUserNarrative.to_s,
DiscourseNarrativeBot::AdvancedUserNarrative.to_s,
],
)
end end
it 'should grant the right badges' do it "should grant the right badges" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(user.badges.count).to eq(2) expect(user.badges.count).to eq(2)

View File

@ -3,14 +3,17 @@
RSpec.describe Jobs::DiscourseNarrativeBot::RemapOldBotImages do RSpec.describe Jobs::DiscourseNarrativeBot::RemapOldBotImages do
context "when bot's post contains an old link" do context "when bot's post contains an old link" do
let!(:post) do let!(:post) do
Fabricate(:post, Fabricate(
:post,
user: ::DiscourseNarrativeBot::Base.new.discobot_user, user: ::DiscourseNarrativeBot::Base.new.discobot_user,
raw: 'If youd like to learn more, select <img src="/images/font-awesome-gear.png" width="16" height="16"> <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!' raw:
'If youd like to learn more, select <img src="/images/font-awesome-gear.png" width="16" height="16"> <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!',
) )
end end
it 'should remap the links correctly' do it "should remap the links correctly" do
expected_raw = 'If youd like to learn more, select <img src="/plugins/discourse-narrative-bot/images/font-awesome-gear.png" width="16" height="16"> <img src="/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/plugins/discourse-narrative-bot/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!' expected_raw =
'If youd like to learn more, select <img src="/plugins/discourse-narrative-bot/images/font-awesome-gear.png" width="16" height="16"> <img src="/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/plugins/discourse-narrative-bot/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!'
2.times do 2.times do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
@ -19,19 +22,21 @@ RSpec.describe Jobs::DiscourseNarrativeBot::RemapOldBotImages do
end end
end end
context 'with subfolder' do context "with subfolder" do
let!(:post) do let!(:post) do
Fabricate(:post, Fabricate(
:post,
user: ::DiscourseNarrativeBot::Base.new.discobot_user, user: ::DiscourseNarrativeBot::Base.new.discobot_user,
raw: 'If youd like to learn more, select <img src="/community/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/community/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!' raw:
'If youd like to learn more, select <img src="/community/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/community/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!',
) )
end end
it 'should remap the links correctly' do it "should remap the links correctly" do
described_class.new.execute_onceoff({}) described_class.new.execute_onceoff({})
expect(post.reload.raw).to eq( expect(post.reload.raw).to eq(
'If youd like to learn more, select <img src="/community/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/community/plugins/discourse-narrative-bot/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!' 'If youd like to learn more, select <img src="/community/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/community/plugins/discourse-narrative-bot/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!',
) )
end end
end end

View File

@ -3,42 +3,51 @@
RSpec.describe Jobs::SendDefaultWelcomeMessage do RSpec.describe Jobs::SendDefaultWelcomeMessage do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
it 'should send the right welcome message' do it "should send the right welcome message" do
described_class.new.execute(user_id: user.id) described_class.new.execute(user_id: user.id)
topic = Topic.last topic = Topic.last
expect(topic.title).to eq(I18n.t( expect(topic.title).to eq(
"system_messages.welcome_user.subject_template", I18n.t("system_messages.welcome_user.subject_template", site_name: SiteSetting.title),
site_name: SiteSetting.title )
))
expect(topic.first_post.raw).to eq(I18n.t( expect(topic.first_post.raw).to eq(
"system_messages.welcome_user.text_body_template", I18n.t(
SystemMessage.new(user).defaults "system_messages.welcome_user.text_body_template",
).chomp) SystemMessage.new(user).defaults,
).chomp,
)
expect(topic.closed).to eq(true) expect(topic.closed).to eq(true)
end end
describe 'for an invited user' do describe "for an invited user" do
let(:invite) { Fabricate(:invite, email: 'foo@bar.com') } let(:invite) { Fabricate(:invite, email: "foo@bar.com") }
let(:invited_user) { Fabricate(:invited_user, invite: invite, user: Fabricate(:user, email: 'foo@bar.com'), redeemed_at: Time.zone.now) } let(:invited_user) do
Fabricate(
:invited_user,
invite: invite,
user: Fabricate(:user, email: "foo@bar.com"),
redeemed_at: Time.zone.now,
)
end
it 'should send the right welcome message' do it "should send the right welcome message" do
described_class.new.execute(user_id: invited_user.user_id) described_class.new.execute(user_id: invited_user.user_id)
topic = Topic.last topic = Topic.last
expect(topic.title).to eq(I18n.t( expect(topic.title).to eq(
"system_messages.welcome_invite.subject_template", I18n.t("system_messages.welcome_invite.subject_template", site_name: SiteSetting.title),
site_name: SiteSetting.title )
))
expect(topic.first_post.raw).to eq(I18n.t( expect(topic.first_post.raw).to eq(
"system_messages.welcome_invite.text_body_template", I18n.t(
SystemMessage.new(invited_user.user).defaults "system_messages.welcome_invite.text_body_template",
).chomp) SystemMessage.new(invited_user.user).defaults,
).chomp,
)
expect(topic.closed).to eq(true) expect(topic.closed).to eq(true)
end end

View File

@ -2,24 +2,21 @@
RSpec.describe DiscourseNarrativeBot::CertificateGenerator do RSpec.describe DiscourseNarrativeBot::CertificateGenerator do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:avatar_url) { 'http://test.localhost/cdn/avatar.png' } let(:avatar_url) { "http://test.localhost/cdn/avatar.png" }
let(:date) { "2017-00-10" } let(:date) { "2017-00-10" }
describe 'when an invalid date is given' do describe "when an invalid date is given" do
it 'should default to the current date' do it "should default to the current date" do
expect { described_class.new(user, date, avatar_url) }.to_not raise_error expect { described_class.new(user, date, avatar_url) }.to_not raise_error
end end
end end
describe '#logo_group' do describe "#logo_group" do
describe 'when SiteSetting.site_logo_small_url is blank' do describe "when SiteSetting.site_logo_small_url is blank" do
before do before { SiteSetting.logo_small = "" }
SiteSetting.logo_small = ''
end
it 'should not try to fetch a image' do it "should not try to fetch a image" do
expect(described_class.new(user, date, avatar_url).send(:logo_group, 1, 1, 1)) expect(described_class.new(user, date, avatar_url).send(:logo_group, 1, 1, 1)).to eq(nil)
.to eq(nil)
end end
end end
end end

View File

@ -1,48 +1,43 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe "Discobot Certificate" do RSpec.describe "Discobot Certificate" do
let(:user) { Fabricate(:user, name: 'Jeff Atwood') } let(:user) { Fabricate(:user, name: "Jeff Atwood") }
let(:params) { let(:params) { { date: Time.zone.now.strftime("%b %d %Y"), user_id: user.id } }
{
date: Time.zone.now.strftime("%b %d %Y"),
user_id: user.id
}
}
describe 'when viewing the certificate' do describe "when viewing the certificate" do
describe 'when no logged in' do describe "when no logged in" do
it 'should return the right response' do it "should return the right response" do
get '/discobot/certificate.svg', params: params get "/discobot/certificate.svg", params: params
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
describe 'when logged in' do describe "when logged in" do
before do before { sign_in(user) }
sign_in(user)
end
it 'should return the right text' do it "should return the right text" do
stub_request(:get, /letter_avatar_proxy/).to_return(status: 200, body: 'http://test.localhost/cdn/avatar.png') stub_request(:get, /letter_avatar_proxy/).to_return(
status: 200,
body: "http://test.localhost/cdn/avatar.png",
)
stub_request(:get, /avatar.png/).to_return(status: 200) stub_request(:get, /avatar.png/).to_return(status: 200)
stub_request(:get, SiteSetting.site_logo_small_url) stub_request(:get, SiteSetting.site_logo_small_url).to_return(status: 200)
.to_return(status: 200)
get '/discobot/certificate.svg', params: params get "/discobot/certificate.svg", params: params
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include('<svg') expect(response.body).to include("<svg")
expect(response.body).to include(user.avatar_template.gsub('{size}', '250')) expect(response.body).to include(user.avatar_template.gsub("{size}", "250"))
expect(response.body).to include(SiteSetting.site_logo_small_url) expect(response.body).to include(SiteSetting.site_logo_small_url)
end end
describe 'when params are missing' do describe "when params are missing" do
it "should raise the right errors" do it "should raise the right errors" do
params.each do |key, _| params.each do |key, _|
get '/discobot/certificate.svg', params: params.except(key) get "/discobot/certificate.svg", params: params.except(key)
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end

View File

@ -3,40 +3,39 @@
RSpec.describe "Discobot welcome post" do RSpec.describe "Discobot welcome post" do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
before do before { SiteSetting.discourse_narrative_bot_enabled = true }
SiteSetting.discourse_narrative_bot_enabled = true
end
context 'when discourse_narrative_bot_welcome_post_delay is 0' do context "when discourse_narrative_bot_welcome_post_delay is 0" do
it 'should not delay the welcome post' do it "should not delay the welcome post" do
user user
expect { sign_in(user) }.to_not change { Jobs::NarrativeInit.jobs.count } expect { sign_in(user) }.to_not change { Jobs::NarrativeInit.jobs.count }
end end
end end
context 'when discourse_narrative_bot_welcome_post_delay is greater than 0' do context "when discourse_narrative_bot_welcome_post_delay is greater than 0" do
before do before { SiteSetting.discourse_narrative_bot_welcome_post_delay = 5 }
SiteSetting.discourse_narrative_bot_welcome_post_delay = 5
end
context 'when user logs in normally' do context "when user logs in normally" do
it 'should delay the welcome post until user logs in' do it "should delay the welcome post until user logs in" do
expect { sign_in(user) }.to change { Jobs::NarrativeInit.jobs.count }.by(1) expect { sign_in(user) }.to change { Jobs::NarrativeInit.jobs.count }.by(1)
expect(Jobs::NarrativeInit.jobs.first["args"].first["user_id"]).to eq(user.id) expect(Jobs::NarrativeInit.jobs.first["args"].first["user_id"]).to eq(user.id)
end end
end end
context 'when user redeems an invite' do context "when user redeems an invite" do
let!(:invite) { Fabricate(:invite, invited_by: Fabricate(:admin), email: 'testing@gmail.com') } let!(:invite) do
Fabricate(:invite, invited_by: Fabricate(:admin), email: "testing@gmail.com")
end
it 'should delay the welcome post until the user logs in' do it "should delay the welcome post until the user logs in" do
expect do expect do
put "/invites/show/#{invite.invite_key}.json", params: { put "/invites/show/#{invite.invite_key}.json",
username: 'somename', params: {
name: 'testing', username: "somename",
password: 'verystrongpassword', name: "testing",
email_token: invite.email_token password: "verystrongpassword",
} email_token: invite.email_token,
}
end.to change { User.count }.by(1) end.to change { User.count }.by(1)
expect(Jobs::NarrativeInit.jobs.first["args"].first["user_id"]).to eq(User.last.id) expect(Jobs::NarrativeInit.jobs.first["args"].first["user_id"]).to eq(User.last.id)
@ -44,14 +43,12 @@ RSpec.describe "Discobot welcome post" do
end end
end end
context 'when user is staged' do context "when user is staged" do
let(:staged_user) { Fabricate(:user, staged: true) } let(:staged_user) { Fabricate(:user, staged: true) }
before do before { SiteSetting.discourse_narrative_bot_welcome_post_type = "welcome_message" }
SiteSetting.discourse_narrative_bot_welcome_post_type = 'welcome_message'
end
it 'should not send welcome message' do it "should not send welcome message" do
expect { staged_user }.to_not change { Jobs::SendDefaultWelcomeMessage.jobs.count } expect { staged_user }.to_not change { Jobs::SendDefaultWelcomeMessage.jobs.count }
end end
end end

View File

@ -18,79 +18,76 @@ RSpec.describe User do
SiteSetting.discourse_narrative_bot_enabled = true SiteSetting.discourse_narrative_bot_enabled = true
end end
describe 'when a user is created' do describe "when a user is created" do
it 'should initiate the bot' do it "should initiate the bot" do
NotificationEmailer.expects(:process_notification).never NotificationEmailer.expects(:process_notification).never
user user
expected_raw = i18n_t('discourse_narrative_bot.new_user_narrative.hello.message', expected_raw =
username: user.username, title: SiteSetting.title i18n_t(
) "discourse_narrative_bot.new_user_narrative.hello.message",
username: user.username,
title: SiteSetting.title,
)
expect(Post.last.raw).to include(expected_raw.chomp) expect(Post.last.raw).to include(expected_raw.chomp)
end end
describe 'welcome post' do describe "welcome post" do
context 'when disabled' do context "when disabled" do
before do before { SiteSetting.disable_discourse_narrative_bot_welcome_post = true }
SiteSetting.disable_discourse_narrative_bot_welcome_post = true
end
it 'should not initiate the bot' do it "should not initiate the bot" do
expect { user }.to_not change { Post.count } expect { user }.to_not change { Post.count }
end end
end end
context 'with title emoji disabled' do context "with title emoji disabled" do
before do before do
SiteSetting.disable_discourse_narrative_bot_welcome_post = false SiteSetting.disable_discourse_narrative_bot_welcome_post = false
SiteSetting.max_emojis_in_title = 0 SiteSetting.max_emojis_in_title = 0
end end
it 'initiates the bot' do it "initiates the bot" do
expect { user }.to change { Topic.count }.by(1) expect { user }.to change { Topic.count }.by(1)
expect(Topic.last.title).to eq(i18n_t( expect(Topic.last.title).to eq(
'discourse_narrative_bot.new_user_narrative.hello.title' i18n_t("discourse_narrative_bot.new_user_narrative.hello.title").gsub(
).gsub(/:robot:/, '').strip) /:robot:/,
"",
).strip,
)
end end
end end
context 'when enabled' do context "when enabled" do
before do before { SiteSetting.disable_discourse_narrative_bot_welcome_post = false }
SiteSetting.disable_discourse_narrative_bot_welcome_post = false
end
it 'initiate the bot' do it "initiate the bot" do
expect { user }.to change { Topic.count }.by(1) expect { user }.to change { Topic.count }.by(1)
expect(Topic.last.title).to eq(i18n_t( expect(Topic.last.title).to eq(
'discourse_narrative_bot.new_user_narrative.hello.title' i18n_t("discourse_narrative_bot.new_user_narrative.hello.title"),
)) )
end end
describe "when send welcome message is selected" do describe "when send welcome message is selected" do
before do before { SiteSetting.discourse_narrative_bot_welcome_post_type = "welcome_message" }
SiteSetting.discourse_narrative_bot_welcome_post_type = 'welcome_message'
end
it 'should send the right welcome message' do it "should send the right welcome message" do
expect { user }.to change { Topic.count }.by(1) expect { user }.to change { Topic.count }.by(1)
expect(Topic.last.title).to eq(i18n_t( expect(Topic.last.title).to eq(
"system_messages.welcome_user.subject_template", i18n_t("system_messages.welcome_user.subject_template", site_name: SiteSetting.title),
site_name: SiteSetting.title )
))
end end
end end
describe 'when welcome message is configured to be delayed' do describe "when welcome message is configured to be delayed" do
before do before { SiteSetting.discourse_narrative_bot_welcome_post_delay = 100 }
SiteSetting.discourse_narrative_bot_welcome_post_delay = 100
end
it 'should delay the welcome post until user logs in' do it "should delay the welcome post until user logs in" do
user user
expect(Jobs::NarrativeInit.jobs.count).to eq(0) expect(Jobs::NarrativeInit.jobs.count).to eq(0)
@ -99,41 +96,37 @@ RSpec.describe User do
end end
end end
context 'when user is staged' do context "when user is staged" do
let(:user) { Fabricate(:user, staged: true) } let(:user) { Fabricate(:user, staged: true) }
it 'should not initiate the bot' do it "should not initiate the bot" do
expect { user }.to_not change { Post.count } expect { user }.to_not change { Post.count }
end end
end end
context 'when user skipped the new user tips' do context "when user skipped the new user tips" do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
it 'should not initiate the bot' do it "should not initiate the bot" do
SiteSetting.default_other_skip_new_user_tips = true SiteSetting.default_other_skip_new_user_tips = true
expect { user }.to_not change { Post.count } expect { user }.to_not change { Post.count }
end end
it 'should delete the existing PM' do it "should delete the existing PM" do
user.user_option.skip_new_user_tips = true user.user_option.skip_new_user_tips = true
expect { expect { user.user_option.save! }.to change { Topic.count }.by(-1).and not_change {
user.user_option.save! UserHistory.count
}.to change { Topic.count }.by(-1) }.and change { user.unread_high_priority_notifications }.by(-1).and change {
.and not_change { UserHistory.count } user.notifications.count
.and change { user.unread_high_priority_notifications }.by(-1) }.by(-1)
.and change { user.notifications.count }.by(-1)
end end
end end
context 'when user is anonymous?' do context "when user is anonymous?" do
before do before { SiteSetting.allow_anonymous_posting = true }
SiteSetting.allow_anonymous_posting = true
end
it 'should initiate bot for real user only' do
it "should initiate bot for real user only" do
user = Fabricate(:user, trust_level: 1) user = Fabricate(:user, trust_level: 1)
shadow = AnonymousShadowCreator.get(user) shadow = AnonymousShadowCreator.get(user)
@ -145,21 +138,19 @@ RSpec.describe User do
context "when user's username should be ignored" do context "when user's username should be ignored" do
let(:user) { Fabricate.build(:user) } let(:user) { Fabricate.build(:user) }
before do before { SiteSetting.discourse_narrative_bot_ignored_usernames = "discourse|test" }
SiteSetting.discourse_narrative_bot_ignored_usernames = 'discourse|test'
end
['discourse', 'test'].each do |username| %w[discourse test].each do |username|
it 'should not initiate the bot' do it "should not initiate the bot" do
expect { user.update!(username: username) }.to_not change { Post.count } expect { user.update!(username: username) }.to_not change { Post.count }
end end
end end
end end
end end
describe 'when a user has been destroyed' do describe "when a user has been destroyed" do
it "should clean up plugin's store" do it "should clean up plugin's store" do
DiscourseNarrativeBot::Store.set(user.id, 'test') DiscourseNarrativeBot::Store.set(user.id, "test")
user.destroy! user.destroy!
@ -167,8 +158,8 @@ RSpec.describe User do
end end
end end
describe '#manually_disabled_discobot?' do describe "#manually_disabled_discobot?" do
it 'returns true if the user manually disabled new user tips' do it "returns true if the user manually disabled new user tips" do
user.user_option.skip_new_user_tips = true user.user_option.skip_new_user_tips = true
expect(user.manually_disabled_discobot?).to eq(true) expect(user.manually_disabled_discobot?).to eq(true)

View File

@ -9,40 +9,40 @@
enabled_site_setting :presence_enabled enabled_site_setting :presence_enabled
hide_plugin if self.respond_to?(:hide_plugin) hide_plugin if self.respond_to?(:hide_plugin)
register_asset 'stylesheets/presence.scss' register_asset "stylesheets/presence.scss"
after_initialize do after_initialize do
register_presence_channel_prefix("discourse-presence") do |channel_name| register_presence_channel_prefix("discourse-presence") do |channel_name|
if topic_id = channel_name[/\/discourse-presence\/reply\/(\d+)/, 1] if topic_id = channel_name[%r{/discourse-presence/reply/(\d+)}, 1]
topic = Topic.find(topic_id) topic = Topic.find(topic_id)
config = PresenceChannel::Config.new config = PresenceChannel::Config.new
if topic.private_message? if topic.private_message?
config.allowed_user_ids = topic.allowed_users.pluck(:id) config.allowed_user_ids = topic.allowed_users.pluck(:id)
config.allowed_group_ids = topic.allowed_groups.pluck(:group_id) + [::Group::AUTO_GROUPS[:staff]] config.allowed_group_ids =
topic.allowed_groups.pluck(:group_id) + [::Group::AUTO_GROUPS[:staff]]
elsif secure_group_ids = topic.secure_group_ids elsif secure_group_ids = topic.secure_group_ids
config.allowed_group_ids = secure_group_ids + [::Group::AUTO_GROUPS[:admins]] config.allowed_group_ids = secure_group_ids + [::Group::AUTO_GROUPS[:admins]]
else else
# config.public=true would make data available to anon, so use the tl0 group instead # config.public=true would make data available to anon, so use the tl0 group instead
config.allowed_group_ids = [ ::Group::AUTO_GROUPS[:trust_level_0] ] config.allowed_group_ids = [::Group::AUTO_GROUPS[:trust_level_0]]
end end
config config
elsif topic_id = channel_name[/\/discourse-presence\/whisper\/(\d+)/, 1] elsif topic_id = channel_name[%r{/discourse-presence/whisper/(\d+)}, 1]
Topic.find(topic_id) # Just ensure it exists Topic.find(topic_id) # Just ensure it exists
PresenceChannel::Config.new(allowed_group_ids: [::Group::AUTO_GROUPS[:staff]]) PresenceChannel::Config.new(allowed_group_ids: [::Group::AUTO_GROUPS[:staff]])
elsif post_id = channel_name[/\/discourse-presence\/edit\/(\d+)/, 1] elsif post_id = channel_name[%r{/discourse-presence/edit/(\d+)}, 1]
post = Post.find(post_id) post = Post.find(post_id)
topic = Topic.find(post.topic_id) topic = Topic.find(post.topic_id)
config = PresenceChannel::Config.new config = PresenceChannel::Config.new
config.allowed_group_ids = [ ::Group::AUTO_GROUPS[:staff] ] config.allowed_group_ids = [::Group::AUTO_GROUPS[:staff]]
# Locked and whisper posts are staff only # Locked and whisper posts are staff only
next config if post.locked? || post.whisper? next config if post.locked? || post.whisper?
config.allowed_user_ids = [ post.user_id ] config.allowed_user_ids = [post.user_id]
if topic.private_message? && post.wiki if topic.private_message? && post.wiki
# Ignore trust level and just publish to all allowed groups since # Ignore trust level and just publish to all allowed groups since
@ -52,14 +52,17 @@ after_initialize do
config.allowed_user_ids += topic.allowed_users.pluck(:id) config.allowed_user_ids += topic.allowed_users.pluck(:id)
config.allowed_group_ids += topic.allowed_groups.pluck(:id) config.allowed_group_ids += topic.allowed_groups.pluck(:id)
elsif post.wiki elsif post.wiki
config.allowed_group_ids << Group::AUTO_GROUPS[:"trust_level_#{SiteSetting.min_trust_to_edit_wiki_post}"] config.allowed_group_ids << Group::AUTO_GROUPS[
:"trust_level_#{SiteSetting.min_trust_to_edit_wiki_post}"
]
end end
if !topic.private_message? && SiteSetting.trusted_users_can_edit_others? if !topic.private_message? && SiteSetting.trusted_users_can_edit_others?
config.allowed_group_ids << Group::AUTO_GROUPS[:trust_level_4] config.allowed_group_ids << Group::AUTO_GROUPS[:trust_level_4]
end end
if SiteSetting.enable_category_group_moderation? && group_id = topic.category&.reviewable_by_group_id if SiteSetting.enable_category_group_moderation? &&
group_id = topic.category&.reviewable_by_group_id
config.allowed_group_ids << group_id config.allowed_group_ids << group_id
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe "discourse-presence" do RSpec.describe "discourse-presence" do
describe 'PresenceChannel configuration' do describe "PresenceChannel configuration" do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) }
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }
@ -16,25 +16,21 @@ RSpec.describe "discourse-presence" do
fab!(:private_topic) { Fabricate(:topic, category: category) } fab!(:private_topic) { Fabricate(:topic, category: category) }
fab!(:public_topic) { Fabricate(:topic, first_post: Fabricate(:post)) } fab!(:public_topic) { Fabricate(:topic, first_post: Fabricate(:post)) }
fab!(:private_message) do fab!(:private_message) { Fabricate(:private_message_topic, allowed_groups: [group]) }
Fabricate(:private_message_topic,
allowed_groups: [group]
)
end
before { PresenceChannel.clear_all! } before { PresenceChannel.clear_all! }
it 'handles invalid topic IDs' do it "handles invalid topic IDs" do
expect do expect do PresenceChannel.new("/discourse-presence/reply/-999").config end.to raise_error(
PresenceChannel.new('/discourse-presence/reply/-999').config PresenceChannel::NotFound,
end.to raise_error(PresenceChannel::NotFound) )
expect do expect do PresenceChannel.new("/discourse-presence/reply/blah").config end.to raise_error(
PresenceChannel.new('/discourse-presence/reply/blah').config PresenceChannel::NotFound,
end.to raise_error(PresenceChannel::NotFound) )
end end
it 'handles deleted topics' do it "handles deleted topics" do
public_topic.trash! public_topic.trash!
expect do expect do
@ -50,7 +46,7 @@ RSpec.describe "discourse-presence" do
end.to raise_error(PresenceChannel::NotFound) end.to raise_error(PresenceChannel::NotFound)
end end
it 'handles secure category permissions for reply' do it "handles secure category permissions for reply" do
c = PresenceChannel.new("/discourse-presence/reply/#{private_topic.id}") c = PresenceChannel.new("/discourse-presence/reply/#{private_topic.id}")
expect(c.can_view?(user_id: user.id)).to eq(true) expect(c.can_view?(user_id: user.id)).to eq(true)
expect(c.can_enter?(user_id: user.id)).to eq(true) expect(c.can_enter?(user_id: user.id)).to eq(true)
@ -62,14 +58,14 @@ RSpec.describe "discourse-presence" do
expect(c.can_enter?(user_id: user.id)).to eq(false) expect(c.can_enter?(user_id: user.id)).to eq(false)
end end
it 'handles secure category permissions for edit' do it "handles secure category permissions for edit" do
p = Fabricate(:post, topic: private_topic, user: private_topic.user) p = Fabricate(:post, topic: private_topic, user: private_topic.user)
c = PresenceChannel.new("/discourse-presence/edit/#{p.id}") c = PresenceChannel.new("/discourse-presence/edit/#{p.id}")
expect(c.can_view?(user_id: user.id)).to eq(false) expect(c.can_view?(user_id: user.id)).to eq(false)
expect(c.can_view?(user_id: private_topic.user.id)).to eq(true) expect(c.can_view?(user_id: private_topic.user.id)).to eq(true)
end end
it 'handles category moderators for edit' do it "handles category moderators for edit" do
SiteSetting.trusted_users_can_edit_others = false SiteSetting.trusted_users_can_edit_others = false
p = Fabricate(:post, topic: private_topic, user: private_topic.user) p = Fabricate(:post, topic: private_topic, user: private_topic.user)
@ -83,25 +79,25 @@ RSpec.describe "discourse-presence" do
expect(c.config.allowed_group_ids).to contain_exactly(Group::AUTO_GROUPS[:staff], group.id) expect(c.config.allowed_group_ids).to contain_exactly(Group::AUTO_GROUPS[:staff], group.id)
end end
it 'handles permissions for a public topic' do it "handles permissions for a public topic" do
c = PresenceChannel.new("/discourse-presence/reply/#{public_topic.id}") c = PresenceChannel.new("/discourse-presence/reply/#{public_topic.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly(::Group::AUTO_GROUPS[:trust_level_0]) expect(c.config.allowed_group_ids).to contain_exactly(::Group::AUTO_GROUPS[:trust_level_0])
end end
it 'handles permissions for secure category topics' do it "handles permissions for secure category topics" do
c = PresenceChannel.new("/discourse-presence/reply/#{private_topic.id}") c = PresenceChannel.new("/discourse-presence/reply/#{private_topic.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly(group.id, Group::AUTO_GROUPS[:admins]) expect(c.config.allowed_group_ids).to contain_exactly(group.id, Group::AUTO_GROUPS[:admins])
expect(c.config.allowed_user_ids).to eq(nil) expect(c.config.allowed_user_ids).to eq(nil)
end end
it 'handles permissions for private messages' do it "handles permissions for private messages" do
c = PresenceChannel.new("/discourse-presence/reply/#{private_message.id}") c = PresenceChannel.new("/discourse-presence/reply/#{private_message.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly(group.id, Group::AUTO_GROUPS[:staff]) expect(c.config.allowed_group_ids).to contain_exactly(group.id, Group::AUTO_GROUPS[:staff])
expect(c.config.allowed_user_ids).to contain_exactly( expect(c.config.allowed_user_ids).to contain_exactly(
*private_message.topic_allowed_users.pluck(:user_id) *private_message.topic_allowed_users.pluck(:user_id),
) )
end end
@ -112,7 +108,7 @@ RSpec.describe "discourse-presence" do
expect(c.config.allowed_user_ids).to eq(nil) expect(c.config.allowed_user_ids).to eq(nil)
end end
it 'only allows staff when editing whispers' do it "only allows staff when editing whispers" do
p = Fabricate(:whisper, topic: public_topic, user: admin) p = Fabricate(:whisper, topic: public_topic, user: admin)
c = PresenceChannel.new("/discourse-presence/edit/#{p.id}") c = PresenceChannel.new("/discourse-presence/edit/#{p.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
@ -120,7 +116,7 @@ RSpec.describe "discourse-presence" do
expect(c.config.allowed_user_ids).to eq(nil) expect(c.config.allowed_user_ids).to eq(nil)
end end
it 'only allows staff when editing a locked post' do it "only allows staff when editing a locked post" do
p = Fabricate(:post, topic: public_topic, user: admin, locked_by_id: Discourse.system_user.id) p = Fabricate(:post, topic: public_topic, user: admin, locked_by_id: Discourse.system_user.id)
c = PresenceChannel.new("/discourse-presence/edit/#{p.id}") c = PresenceChannel.new("/discourse-presence/edit/#{p.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
@ -134,7 +130,7 @@ RSpec.describe "discourse-presence" do
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly( expect(c.config.allowed_group_ids).to contain_exactly(
Group::AUTO_GROUPS[:trust_level_4], Group::AUTO_GROUPS[:trust_level_4],
Group::AUTO_GROUPS[:staff] Group::AUTO_GROUPS[:staff],
) )
expect(c.config.allowed_user_ids).to contain_exactly(user.id) expect(c.config.allowed_user_ids).to contain_exactly(user.id)
end end
@ -145,9 +141,7 @@ RSpec.describe "discourse-presence" do
p = Fabricate(:post, topic: public_topic, user: user) p = Fabricate(:post, topic: public_topic, user: user)
c = PresenceChannel.new("/discourse-presence/edit/#{p.id}") c = PresenceChannel.new("/discourse-presence/edit/#{p.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly( expect(c.config.allowed_group_ids).to contain_exactly(Group::AUTO_GROUPS[:staff])
Group::AUTO_GROUPS[:staff]
)
expect(c.config.allowed_user_ids).to contain_exactly(user.id) expect(c.config.allowed_user_ids).to contain_exactly(user.id)
end end
@ -160,7 +154,7 @@ RSpec.describe "discourse-presence" do
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly( expect(c.config.allowed_group_ids).to contain_exactly(
Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:staff],
Group::AUTO_GROUPS[:trust_level_1] Group::AUTO_GROUPS[:trust_level_1],
) )
expect(c.config.allowed_user_ids).to contain_exactly(user.id) expect(c.config.allowed_user_ids).to contain_exactly(user.id)
end end
@ -170,9 +164,7 @@ RSpec.describe "discourse-presence" do
c = PresenceChannel.new("/discourse-presence/edit/#{post.id}") c = PresenceChannel.new("/discourse-presence/edit/#{post.id}")
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly( expect(c.config.allowed_group_ids).to contain_exactly(Group::AUTO_GROUPS[:staff])
Group::AUTO_GROUPS[:staff]
)
expect(c.config.allowed_user_ids).to contain_exactly(user.id) expect(c.config.allowed_user_ids).to contain_exactly(user.id)
end end
@ -183,9 +175,12 @@ RSpec.describe "discourse-presence" do
expect(c.config.public).to eq(false) expect(c.config.public).to eq(false)
expect(c.config.allowed_group_ids).to contain_exactly( expect(c.config.allowed_group_ids).to contain_exactly(
Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:staff],
*private_message.allowed_groups.pluck(:id) *private_message.allowed_groups.pluck(:id),
)
expect(c.config.allowed_user_ids).to contain_exactly(
user.id,
*private_message.allowed_users.pluck(:id),
) )
expect(c.config.allowed_user_ids).to contain_exactly(user.id, *private_message.allowed_users.pluck(:id))
end end
end end
end end

View File

@ -21,11 +21,10 @@ class Onebox::Engine::YoutubeOnebox
alias_method :yt_onebox_to_html, :to_html alias_method :yt_onebox_to_html, :to_html
def to_html def to_html
if video_id && !params['list'] if video_id && !params["list"]
size_restricted = [params["width"], params["height"]].any?
size_restricted = [params['width'], params['height']].any? video_width = (params["width"] && params["width"].to_i <= 695) ? params["width"] : 690 # embed width
video_width = (params['width'] && params['width'].to_i <= 695) ? params['width'] : 690 # embed width video_height = (params["height"] && params["height"].to_i <= 500) ? params["height"] : 388 # embed height
video_height = (params['height'] && params['height'].to_i <= 500) ? params['height'] : 388 # embed height
size_tags = ["width=\"#{video_width}\"", "height=\"#{video_height}\""] size_tags = ["width=\"#{video_width}\"", "height=\"#{video_height}\""]
result = parse_embed_response result = parse_embed_response
@ -40,12 +39,12 @@ class Onebox::Engine::YoutubeOnebox
<div class="onebox lazyYT lazyYT-container" <div class="onebox lazyYT lazyYT-container"
data-youtube-id="#{video_id}" data-youtube-id="#{video_id}"
data-youtube-title="#{escaped_title}" data-youtube-title="#{escaped_title}"
#{size_restricted ? size_tags.map { |t| "data-#{t}" }.join(' ') : ""} #{size_restricted ? size_tags.map { |t| "data-#{t}" }.join(" ") : ""}
data-parameters="#{embed_params}"> data-parameters="#{embed_params}">
<a href="https://www.youtube.com/watch?v=#{video_id}" target="_blank"> <a href="https://www.youtube.com/watch?v=#{video_id}" target="_blank">
<img class="ytp-thumbnail-image" <img class="ytp-thumbnail-image"
src="#{thumbnail_url}" src="#{thumbnail_url}"
#{size_restricted ? size_tags.join(' ') : ""} #{size_restricted ? size_tags.join(" ") : ""}
title="#{escaped_title}"> title="#{escaped_title}">
</a> </a>
</div> </div>
@ -54,23 +53,22 @@ class Onebox::Engine::YoutubeOnebox
yt_onebox_to_html yt_onebox_to_html
end end
end end
end end
after_initialize do after_initialize do
on(:reduce_cooked) do |fragment| on(:reduce_cooked) do |fragment|
fragment.css(".lazyYT").each do |yt| fragment
begin .css(".lazyYT")
youtube_id = yt["data-youtube-id"] .each do |yt|
parameters = yt["data-parameters"] begin
uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1&#{parameters}") youtube_id = yt["data-youtube-id"]
yt.replace %{<p><a href="#{uri.to_s}">https://#{uri.host}#{uri.path}</a></p>} parameters = yt["data-parameters"]
rescue URI::InvalidURIError uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1&#{parameters}")
# remove any invalid/weird URIs yt.replace %{<p><a href="#{uri.to_s}">https://#{uri.host}#{uri.path}</a></p>}
yt.remove rescue URI::InvalidURIError
# remove any invalid/weird URIs
yt.remove
end
end end
end
end end
end end

View File

@ -3,7 +3,7 @@
class DiscoursePoll::PollsController < ::ApplicationController class DiscoursePoll::PollsController < ::ApplicationController
requires_plugin DiscoursePoll::PLUGIN_NAME requires_plugin DiscoursePoll::PLUGIN_NAME
before_action :ensure_logged_in, except: [:voters, :grouped_poll_results] before_action :ensure_logged_in, except: %i[voters grouped_poll_results]
def vote def vote
post_id = params.require(:post_id) post_id = params.require(:post_id)
@ -63,8 +63,14 @@ class DiscoursePoll::PollsController < ::ApplicationController
begin begin
render json: { render json: {
grouped_results: DiscoursePoll::Poll.grouped_poll_results(current_user, post_id, poll_name, user_field_name) grouped_results:
} DiscoursePoll::Poll.grouped_poll_results(
current_user,
post_id,
poll_name,
user_field_name,
),
}
rescue DiscoursePoll::Error => e rescue DiscoursePoll::Error => e
render_json_error e.message render_json_error e.message
end end

View File

@ -9,33 +9,15 @@ class Poll < ActiveRecord::Base
has_many :poll_options, -> { order(:id) }, dependent: :destroy has_many :poll_options, -> { order(:id) }, dependent: :destroy
has_many :poll_votes has_many :poll_votes
enum type: { enum type: { regular: 0, multiple: 1, number: 2 }, _scopes: false
regular: 0,
multiple: 1,
number: 2,
}, _scopes: false
enum status: { enum status: { open: 0, closed: 1 }, _scopes: false
open: 0,
closed: 1,
}, _scopes: false
enum results: { enum results: { always: 0, on_vote: 1, on_close: 2, staff_only: 3 }, _scopes: false
always: 0,
on_vote: 1,
on_close: 2,
staff_only: 3,
}, _scopes: false
enum visibility: { enum visibility: { secret: 0, everyone: 1 }, _scopes: false
secret: 0,
everyone: 1,
}, _scopes: false
enum chart_type: { enum chart_type: { bar: 0, pie: 1 }, _scopes: false
bar: 0,
pie: 1
}, _scopes: false
validates :min, numericality: { allow_nil: true, only_integer: true, greater_than_or_equal_to: 0 } validates :min, numericality: { allow_nil: true, only_integer: true, greater_than_or_equal_to: 0 }
validates :max, numericality: { allow_nil: true, only_integer: true, greater_than: 0 } validates :max, numericality: { allow_nil: true, only_integer: true, greater_than: 0 }

View File

@ -48,13 +48,15 @@ class PollSerializer < ApplicationSerializer
PollOptionSerializer.new( PollOptionSerializer.new(
option, option,
root: false, root: false,
scope: { can_see_results: can_see_results } scope: {
can_see_results: can_see_results,
},
).as_json ).as_json
end end
end end
def voters def voters
object.poll_votes.count('DISTINCT user_id') + object.anonymous_voters.to_i object.poll_votes.count("DISTINCT user_id") + object.anonymous_voters.to_i
end end
def close def close
@ -72,5 +74,4 @@ class PollSerializer < ApplicationSerializer
def include_preloaded_voters? def include_preloaded_voters?
object.can_see_voters?(scope.user) object.can_see_voters?(scope.user)
end end
end end

View File

@ -1,31 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
class RenameTotalVotesToVoters < ActiveRecord::Migration[4.2] class RenameTotalVotesToVoters < ActiveRecord::Migration[4.2]
def up def up
PostCustomField.where(name: "polls").find_each do |pcf| PostCustomField
polls = ::JSON.parse(pcf.value) .where(name: "polls")
polls.each_value do |poll| .find_each do |pcf|
next if poll.has_key?("voters") polls = ::JSON.parse(pcf.value)
poll["voters"] = poll["total_votes"] polls.each_value do |poll|
poll.delete("total_votes") next if poll.has_key?("voters")
poll["voters"] = poll["total_votes"]
poll.delete("total_votes")
end
pcf.value = polls.to_json
pcf.save
end end
pcf.value = polls.to_json
pcf.save
end
end end
def down def down
PostCustomField.where(name: "polls").find_each do |pcf| PostCustomField
polls = ::JSON.parse(pcf.value) .where(name: "polls")
polls.each_value do |poll| .find_each do |pcf|
next if poll.has_key?("total_votes") polls = ::JSON.parse(pcf.value)
poll["total_votes"] = poll["voters"] polls.each_value do |poll|
poll.delete("voters") next if poll.has_key?("total_votes")
poll["total_votes"] = poll["voters"]
poll.delete("voters")
end
pcf.value = polls.to_json
pcf.save
end end
pcf.value = polls.to_json
pcf.save
end
end end
end end

View File

@ -1,22 +1,27 @@
# frozen_string_literal: true # frozen_string_literal: true
class MergePollsVotes < ActiveRecord::Migration[4.2] class MergePollsVotes < ActiveRecord::Migration[4.2]
def up def up
PostCustomField.where(name: "polls").order(:post_id).pluck(:post_id).each do |post_id| PostCustomField
polls_votes = {} .where(name: "polls")
PostCustomField.where(post_id: post_id).where("name LIKE 'polls-votes-%'").find_each do |pcf| .order(:post_id)
user_id = pcf.name["polls-votes-".size..-1] .pluck(:post_id)
polls_votes["#{user_id}"] = ::JSON.parse(pcf.value || "{}") .each do |post_id|
end polls_votes = {}
PostCustomField
.where(post_id: post_id)
.where("name LIKE 'polls-votes-%'")
.find_each do |pcf|
user_id = pcf.name["polls-votes-".size..-1]
polls_votes["#{user_id}"] = ::JSON.parse(pcf.value || "{}")
end
pcf = PostCustomField.find_or_create_by(name: "polls-votes", post_id: post_id) pcf = PostCustomField.find_or_create_by(name: "polls-votes", post_id: post_id)
pcf.value = ::JSON.parse(pcf.value || "{}").merge(polls_votes).to_json pcf.value = ::JSON.parse(pcf.value || "{}").merge(polls_votes).to_json
pcf.save pcf.save
end end
end end
def down def down
end end
end end

View File

@ -1,20 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
class ClosePollsInClosedTopics < ActiveRecord::Migration[4.2] class ClosePollsInClosedTopics < ActiveRecord::Migration[4.2]
def up def up
PostCustomField.joins(post: :topic) PostCustomField
.joins(post: :topic)
.where("post_custom_fields.name = 'polls'") .where("post_custom_fields.name = 'polls'")
.where("topics.closed") .where("topics.closed")
.find_each do |pcf| .find_each do |pcf|
polls = ::JSON.parse(pcf.value || "{}") polls = ::JSON.parse(pcf.value || "{}")
polls.values.each { |poll| poll["status"] = "closed" } polls.values.each { |poll| poll["status"] = "closed" }
pcf.value = polls.to_json pcf.value = polls.to_json
pcf.save pcf.save
end end
end end
def down def down
end end
end end

View File

@ -17,7 +17,7 @@ class CreatePollsTables < ActiveRecord::Migration[5.2]
t.timestamps t.timestamps
end end
add_index :polls, [:post_id, :name], unique: true add_index :polls, %i[post_id name], unique: true
create_table :poll_options do |t| create_table :poll_options do |t|
t.references :poll, index: true, foreign_key: true t.references :poll, index: true, foreign_key: true
@ -27,7 +27,7 @@ class CreatePollsTables < ActiveRecord::Migration[5.2]
t.timestamps t.timestamps
end end
add_index :poll_options, [:poll_id, :digest], unique: true add_index :poll_options, %i[poll_id digest], unique: true
create_table :poll_votes, id: false do |t| create_table :poll_votes, id: false do |t|
t.references :poll, foreign_key: true t.references :poll, foreign_key: true
@ -36,6 +36,6 @@ class CreatePollsTables < ActiveRecord::Migration[5.2]
t.timestamps t.timestamps
end end
add_index :poll_votes, [:poll_id, :poll_option_id, :user_id], unique: true add_index :poll_votes, %i[poll_id poll_option_id user_id], unique: true
end end
end end

View File

@ -5,11 +5,7 @@ class MigratePollsData < ActiveRecord::Migration[5.2]
PG::Connection.escape_string(text) PG::Connection.escape_string(text)
end end
POLL_TYPES ||= { POLL_TYPES ||= { "regular" => 0, "multiple" => 1, "number" => 2 }
"regular" => 0,
"multiple" => 1,
"number" => 2,
}
PG_INTEGER_MAX ||= 2_147_483_647 PG_INTEGER_MAX ||= 2_147_483_647
@ -61,43 +57,59 @@ class MigratePollsData < ActiveRecord::Migration[5.2]
ORDER BY polls.post_id ORDER BY polls.post_id
SQL SQL
DB.query(sql).each do |r| DB
# for some reasons, polls or votes might be an array .query(sql)
r.polls = r.polls[0] if Array === r.polls && r.polls.size > 0 .each do |r|
r.votes = r.votes[0] if Array === r.votes && r.votes.size > 0 # for some reasons, polls or votes might be an array
r.polls = r.polls[0] if Array === r.polls && r.polls.size > 0
r.votes = r.votes[0] if Array === r.votes && r.votes.size > 0
existing_user_ids = User.where(id: r.votes.keys).pluck(:id).to_set existing_user_ids = User.where(id: r.votes.keys).pluck(:id).to_set
# Poll votes are stored in a JSON object with the following hierarchy # Poll votes are stored in a JSON object with the following hierarchy
# user_id -> poll_name -> options # user_id -> poll_name -> options
# Since we're iterating over polls, we need to change the hierarchy to # Since we're iterating over polls, we need to change the hierarchy to
# poll_name -> user_id -> options # poll_name -> user_id -> options
votes = {} votes = {}
r.votes.each do |user_id, user_votes| r.votes.each do |user_id, user_votes|
# don't migrate votes from deleted/non-existing users # don't migrate votes from deleted/non-existing users
next unless existing_user_ids.include?(user_id.to_i) next unless existing_user_ids.include?(user_id.to_i)
user_votes.each do |poll_name, options| user_votes.each do |poll_name, options|
votes[poll_name] ||= {} votes[poll_name] ||= {}
votes[poll_name][user_id] = options votes[poll_name][user_id] = options
end
end end
end
r.polls.values.each do |poll| r.polls.values.each do |poll|
name = escape(poll["name"].presence || "poll") name = escape(poll["name"].presence || "poll")
type = POLL_TYPES[(poll["type"].presence || "")[/(regular|multiple|number)/, 1] || "regular"] type =
status = poll["status"] == "open" ? 0 : 1 POLL_TYPES[(poll["type"].presence || "")[/(regular|multiple|number)/, 1] || "regular"]
visibility = poll["public"] == "true" ? 1 : 0 status = poll["status"] == "open" ? 0 : 1
close_at = (Time.zone.parse(poll["close"]) rescue nil) visibility = poll["public"] == "true" ? 1 : 0
min = poll["min"].to_i.clamp(0, PG_INTEGER_MAX) close_at =
max = poll["max"].to_i.clamp(0, PG_INTEGER_MAX) (
step = poll["step"].to_i.clamp(0, max) begin
anonymous_voters = poll["anonymous_voters"].to_i.clamp(0, PG_INTEGER_MAX) Time.zone.parse(poll["close"])
rescue StandardError
nil
end
)
min = poll["min"].to_i.clamp(0, PG_INTEGER_MAX)
max = poll["max"].to_i.clamp(0, PG_INTEGER_MAX)
step = poll["step"].to_i.clamp(0, max)
anonymous_voters = poll["anonymous_voters"].to_i.clamp(0, PG_INTEGER_MAX)
next if DB.query_single("SELECT COUNT(*) FROM polls WHERE post_id = ? AND name = ? LIMIT 1", r.post_id, name).first > 0 if DB.query_single(
"SELECT COUNT(*) FROM polls WHERE post_id = ? AND name = ? LIMIT 1",
r.post_id,
name,
).first > 0
next
end
poll_id = execute(<<~SQL poll_id = execute(<<~SQL)[0]["id"]
INSERT INTO polls ( INSERT INTO polls (
post_id, post_id,
name, name,
@ -126,38 +138,41 @@ class MigratePollsData < ActiveRecord::Migration[5.2]
'#{r.updated_at}' '#{r.updated_at}'
) RETURNING id ) RETURNING id
SQL SQL
)[0]["id"]
option_ids = Hash[*DB.query_single(<<~SQL option_ids = Hash[*DB.query_single(<<~SQL)]
INSERT INTO poll_options INSERT INTO poll_options
(poll_id, digest, html, anonymous_votes, created_at, updated_at) (poll_id, digest, html, anonymous_votes, created_at, updated_at)
VALUES VALUES
#{poll["options"].map { |option| #{
"(#{poll_id}, '#{escape(option["id"])}', '#{escape(option["html"].strip)}', #{option["anonymous_votes"].to_i}, '#{r.created_at}', '#{r.updated_at}')" }.join(",") poll["options"]
} .map do |option|
"(#{poll_id}, '#{escape(option["id"])}', '#{escape(option["html"].strip)}', #{option["anonymous_votes"].to_i}, '#{r.created_at}', '#{r.updated_at}')"
end
.join(",")
}
RETURNING digest, id RETURNING digest, id
SQL SQL
)]
if votes[name].present? if votes[name].present?
poll_votes = votes[name].map do |user_id, options| poll_votes =
options votes[name].map do |user_id, options|
.select { |o| option_ids.has_key?(o) } options
.map { |o| "(#{poll_id}, #{option_ids[o]}, #{user_id.to_i}, '#{r.created_at}', '#{r.updated_at}')" } .select { |o| option_ids.has_key?(o) }
end .map do |o|
"(#{poll_id}, #{option_ids[o]}, #{user_id.to_i}, '#{r.created_at}', '#{r.updated_at}')"
end
end
poll_votes.flatten! poll_votes.flatten!
poll_votes.uniq! poll_votes.uniq!
if poll_votes.present? execute <<~SQL if poll_votes.present?
execute <<~SQL
INSERT INTO poll_votes (poll_id, poll_option_id, user_id, created_at, updated_at) INSERT INTO poll_votes (poll_id, poll_option_id, user_id, created_at, updated_at)
VALUES #{poll_votes.join(",")} VALUES #{poll_votes.join(",")}
SQL SQL
end end
end end
end end
end
execute <<~SQL execute <<~SQL
INSERT INTO post_custom_fields (name, value, post_id, created_at, updated_at) INSERT INTO post_custom_fields (name, value, post_id, created_at, updated_at)

View File

@ -1,14 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module Jobs module Jobs
class ClosePoll < ::Jobs::Base class ClosePoll < ::Jobs::Base
def execute(args) def execute(args)
%i{ %i[post_id poll_name].each do |key|
post_id
poll_name
}.each do |key|
raise Discourse::InvalidParameters.new(key) if args[key].blank? raise Discourse::InvalidParameters.new(key) if args[key].blank?
end end
@ -17,10 +12,8 @@ module Jobs
args[:post_id], args[:post_id],
args[:poll_name], args[:poll_name],
"closed", "closed",
false false,
) )
end end
end end
end end

View File

@ -4,38 +4,42 @@ class DiscoursePoll::Poll
def self.vote(user, post_id, poll_name, options) def self.vote(user, post_id, poll_name, options)
poll_id = nil poll_id = nil
serialized_poll = DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll| serialized_poll =
poll_id = poll.id DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll|
# remove options that aren't available in the poll poll_id = poll.id
available_options = poll.poll_options.map { |o| o.digest }.to_set # remove options that aren't available in the poll
options.select! { |o| available_options.include?(o) } available_options = poll.poll_options.map { |o| o.digest }.to_set
options.select! { |o| available_options.include?(o) }
raise DiscoursePoll::Error.new I18n.t("poll.requires_at_least_1_valid_option") if options.empty? if options.empty?
raise DiscoursePoll::Error.new I18n.t("poll.requires_at_least_1_valid_option")
end
new_option_ids = poll.poll_options.each_with_object([]) do |option, obj| new_option_ids =
obj << option.id if options.include?(option.digest) poll
end .poll_options
.each_with_object([]) do |option, obj|
obj << option.id if options.include?(option.digest)
end
self.validate_votes!(poll, new_option_ids) self.validate_votes!(poll, new_option_ids)
old_option_ids = poll.poll_options.each_with_object([]) do |option, obj| old_option_ids =
if option.poll_votes.where(user_id: user.id).exists? poll
obj << option.id .poll_options
.each_with_object([]) do |option, obj|
obj << option.id if option.poll_votes.where(user_id: user.id).exists?
end
# remove non-selected votes
PollVote.where(poll: poll, user: user).where.not(poll_option_id: new_option_ids).delete_all
# create missing votes
(new_option_ids - old_option_ids).each do |option_id|
PollVote.create!(poll: poll, user: user, poll_option_id: option_id)
end end
end end
# remove non-selected votes
PollVote
.where(poll: poll, user: user)
.where.not(poll_option_id: new_option_ids)
.delete_all
# create missing votes
(new_option_ids - old_option_ids).each do |option_id|
PollVote.create!(poll: poll, user: user, poll_option_id: option_id)
end
end
# Ensure consistency here as we do not have a unique index to limit the # Ensure consistency here as we do not have a unique index to limit the
# number of votes per the poll's configuration. # number of votes per the poll's configuration.
is_multiple = serialized_poll[:type] == "multiple" is_multiple = serialized_poll[:type] == "multiple"
@ -79,20 +83,26 @@ class DiscoursePoll::Poll
# topic must not be archived # topic must not be archived
if post.topic&.archived if post.topic&.archived
raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_toggle_status") if raise_errors if raise_errors
raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_toggle_status")
end
return return
end end
# either staff member or OP # either staff member or OP
unless post.user_id == user&.id || user&.staff? unless post.user_id == user&.id || user&.staff?
raise DiscoursePoll::Error.new I18n.t("poll.only_staff_or_op_can_toggle_status") if raise_errors if raise_errors
raise DiscoursePoll::Error.new I18n.t("poll.only_staff_or_op_can_toggle_status")
end
return return
end end
poll = Poll.find_by(post_id: post_id, name: poll_name) poll = Poll.find_by(post_id: post_id, name: poll_name)
if !poll if !poll
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) if raise_errors if raise_errors
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name)
end
return return
end end
@ -110,7 +120,7 @@ class DiscoursePoll::Poll
def self.serialized_voters(poll, opts = {}) def self.serialized_voters(poll, opts = {})
limit = (opts["limit"] || 25).to_i limit = (opts["limit"] || 25).to_i
limit = 0 if limit < 0 limit = 0 if limit < 0
limit = 50 if limit > 50 limit = 50 if limit > 50
page = (opts["page"] || 1).to_i page = (opts["page"] || 1).to_i
@ -121,13 +131,14 @@ class DiscoursePoll::Poll
option_digest = opts["option_id"].to_s option_digest = opts["option_id"].to_s
if poll.number? if poll.number?
user_ids = PollVote user_ids =
.where(poll: poll) PollVote
.group(:user_id) .where(poll: poll)
.order("MIN(created_at)") .group(:user_id)
.offset(offset) .order("MIN(created_at)")
.limit(limit) .offset(offset)
.pluck(:user_id) .limit(limit)
.pluck(:user_id)
result = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash } result = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
elsif option_digest.present? elsif option_digest.present?
@ -135,13 +146,14 @@ class DiscoursePoll::Poll
raise Discourse::InvalidParameters.new(:option_id) unless poll_option raise Discourse::InvalidParameters.new(:option_id) unless poll_option
user_ids = PollVote user_ids =
.where(poll: poll, poll_option: poll_option) PollVote
.group(:user_id) .where(poll: poll, poll_option: poll_option)
.order("MIN(created_at)") .group(:user_id)
.offset(offset) .order("MIN(created_at)")
.limit(limit) .offset(offset)
.pluck(:user_id) .limit(limit)
.pluck(:user_id)
user_hashes = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash } user_hashes = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
@ -163,10 +175,11 @@ class DiscoursePoll::Poll
user_ids = votes.map(&:user_id).uniq user_ids = votes.map(&:user_id).uniq
user_hashes = User user_hashes =
.where(id: user_ids) User
.map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] } .where(id: user_ids)
.to_h .map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] }
.to_h
result = {} result = {}
votes.each do |v| votes.each do |v|
@ -186,10 +199,13 @@ class DiscoursePoll::Poll
def self.grouped_poll_results(user, post_id, poll_name, user_field_name) def self.grouped_poll_results(user, post_id, poll_name, user_field_name)
raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists? raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists?
poll = Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name) poll =
Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name)
raise Discourse::InvalidParameters.new(:poll_name) unless poll raise Discourse::InvalidParameters.new(:poll_name) unless poll
raise Discourse::InvalidParameters.new(:user_field_name) unless SiteSetting.poll_groupable_user_fields.split('|').include?(user_field_name) unless SiteSetting.poll_groupable_user_fields.split("|").include?(user_field_name)
raise Discourse::InvalidParameters.new(:user_field_name)
end
poll_votes = poll.poll_votes poll_votes = poll.poll_votes
@ -199,7 +215,11 @@ class DiscoursePoll::Poll
end end
user_ids = poll_votes.map(&:user_id).uniq user_ids = poll_votes.map(&:user_id).uniq
user_fields = UserCustomField.where(user_id: user_ids, name: transform_for_user_field_override(user_field_name)) user_fields =
UserCustomField.where(
user_id: user_ids,
name: transform_for_user_field_override(user_field_name),
)
user_field_map = {} user_field_map = {}
user_fields.each do |f| user_fields.each do |f|
@ -207,78 +227,80 @@ class DiscoursePoll::Poll
user_field_map[f.user_id] = f.value user_field_map[f.user_id] = f.value
end end
votes_with_field = poll_votes.map do |vote| votes_with_field =
v = vote.attributes poll_votes.map do |vote|
v[:field_value] = user_field_map[vote.user_id] v = vote.attributes
v v[:field_value] = user_field_map[vote.user_id]
end v
end
chart_data = [] chart_data = []
votes_with_field.group_by { |vote| vote[:field_value] }.each do |field_answer, votes| votes_with_field
grouped_selected_options = {} .group_by { |vote| vote[:field_value] }
.each do |field_answer, votes|
grouped_selected_options = {}
# Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option. # Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option.
poll_options.each do |id, option| poll_options.each do |id, option|
grouped_selected_options[id] = { grouped_selected_options[id] = { digest: option[:digest], html: option[:html], votes: 0 }
digest: option[:digest], end
html: option[:html],
votes: 0 # Now go back and update the vote counts. Using hashes so we dont have n^2
} votes
.group_by { |v| v["poll_option_id"] }
.each do |option_id, votes_for_option|
grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length
end
group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data")
chart_data << { group: group_label, options: grouped_selected_options.values }
end end
# Now go back and update the vote counts. Using hashes so we dont have n^2
votes.group_by { |v| v["poll_option_id"] }.each do |option_id, votes_for_option|
grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length
end
group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data")
chart_data << { group: group_label, options: grouped_selected_options.values }
end
chart_data chart_data
end end
def self.schedule_jobs(post) def self.schedule_jobs(post)
Poll.where(post: post).find_each do |poll| Poll
job_args = { .where(post: post)
post_id: post.id, .find_each do |poll|
poll_name: poll.name job_args = { post_id: post.id, poll_name: poll.name }
}
Jobs.cancel_scheduled_job(:close_poll, job_args) Jobs.cancel_scheduled_job(:close_poll, job_args)
if poll.open? && poll.close_at && poll.close_at > Time.zone.now if poll.open? && poll.close_at && poll.close_at > Time.zone.now
Jobs.enqueue_at(poll.close_at, :close_poll, job_args) Jobs.enqueue_at(poll.close_at, :close_poll, job_args)
end
end end
end
end end
def self.create!(post_id, poll) def self.create!(post_id, poll)
close_at = begin close_at =
Time.zone.parse(poll["close"] || '') begin
rescue ArgumentError Time.zone.parse(poll["close"] || "")
end rescue ArgumentError
end
created_poll = Poll.create!( created_poll =
post_id: post_id, Poll.create!(
name: poll["name"].presence || "poll", post_id: post_id,
close_at: close_at, name: poll["name"].presence || "poll",
type: poll["type"].presence || "regular", close_at: close_at,
status: poll["status"].presence || "open", type: poll["type"].presence || "regular",
visibility: poll["public"] == "true" ? "everyone" : "secret", status: poll["status"].presence || "open",
title: poll["title"], visibility: poll["public"] == "true" ? "everyone" : "secret",
results: poll["results"].presence || "always", title: poll["title"],
min: poll["min"], results: poll["results"].presence || "always",
max: poll["max"], min: poll["min"],
step: poll["step"], max: poll["max"],
chart_type: poll["charttype"] || "bar", step: poll["step"],
groups: poll["groups"] chart_type: poll["charttype"] || "bar",
) groups: poll["groups"],
)
poll["options"].each do |option| poll["options"].each do |option|
PollOption.create!( PollOption.create!(
poll: created_poll, poll: created_poll,
digest: option["id"].presence, digest: option["id"].presence,
html: option["html"].presence&.strip html: option["html"].presence&.strip,
) )
end end
end end
@ -286,33 +308,38 @@ class DiscoursePoll::Poll
def self.extract(raw, topic_id, user_id = nil) def self.extract(raw, topic_id, user_id = nil)
# TODO: we should fix the callback mess so that the cooked version is available # TODO: we should fix the callback mess so that the cooked version is available
# in the validators instead of cooking twice # in the validators instead of cooking twice
raw = raw.sub(/\[quote.+\/quote\]/m, '') raw = raw.sub(%r{\[quote.+/quote\]}m, "")
cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id) cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id)
Nokogiri::HTML5(cooked).css("div.poll").map do |p| Nokogiri
poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME } .HTML5(cooked)
.css("div.poll")
.map do |p|
poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME }
# attributes # attributes
p.attributes.values.each do |attribute| p.attributes.values.each do |attribute|
if attribute.name.start_with?(DiscoursePoll::DATA_PREFIX) if attribute.name.start_with?(DiscoursePoll::DATA_PREFIX)
poll[attribute.name[DiscoursePoll::DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "") poll[attribute.name[DiscoursePoll::DATA_PREFIX.length..-1]] = CGI.escapeHTML(
attribute.value || "",
)
end
end end
end
# options # options
p.css("li[#{DiscoursePoll::DATA_PREFIX}option-id]").each do |o| p
option_id = o.attributes[DiscoursePoll::DATA_PREFIX + "option-id"].value.to_s .css("li[#{DiscoursePoll::DATA_PREFIX}option-id]")
poll["options"] << { "id" => option_id, "html" => o.inner_html.strip } .each do |o|
end option_id = o.attributes[DiscoursePoll::DATA_PREFIX + "option-id"].value.to_s
poll["options"] << { "id" => option_id, "html" => o.inner_html.strip }
end
# title # title
title_element = p.css(".poll-title").first title_element = p.css(".poll-title").first
if title_element poll["title"] = title_element.inner_html.strip if title_element
poll["title"] = title_element.inner_html.strip
end
poll poll
end end
end end
def self.validate_votes!(poll, options) def self.validate_votes!(poll, options)
@ -320,15 +347,9 @@ class DiscoursePoll::Poll
if poll.multiple? if poll.multiple?
if poll.min && (num_of_options < poll.min) if poll.min && (num_of_options < poll.min)
raise DiscoursePoll::Error.new(I18n.t( raise DiscoursePoll::Error.new(I18n.t("poll.min_vote_per_user", count: poll.min))
"poll.min_vote_per_user",
count: poll.min
))
elsif poll.max && (num_of_options > poll.max) elsif poll.max && (num_of_options > poll.max)
raise DiscoursePoll::Error.new(I18n.t( raise DiscoursePoll::Error.new(I18n.t("poll.max_vote_per_user", count: poll.max))
"poll.max_vote_per_user",
count: poll.max
))
end end
elsif num_of_options > 1 elsif num_of_options > 1
raise DiscoursePoll::Error.new(I18n.t("poll.one_vote_per_user")) raise DiscoursePoll::Error.new(I18n.t("poll.one_vote_per_user"))
@ -341,9 +362,7 @@ class DiscoursePoll::Poll
post = Post.find_by(id: post_id) post = Post.find_by(id: post_id)
# post must not be deleted # post must not be deleted
if post.nil? || post.trashed? raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted") if post.nil? || post.trashed?
raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted")
end
# topic must not be archived # topic must not be archived
if post.topic&.archived if post.topic&.archived
@ -358,7 +377,9 @@ class DiscoursePoll::Poll
poll = Poll.includes(:poll_options).find_by(post_id: post_id, name: poll_name) poll = Poll.includes(:poll_options).find_by(post_id: post_id, name: poll_name)
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) unless poll unless poll
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name)
end
raise DiscoursePoll::Error.new I18n.t("poll.poll_must_be_open_to_vote") if poll.is_closed? raise DiscoursePoll::Error.new I18n.t("poll.poll_must_be_open_to_vote") if poll.is_closed?
if poll.groups if poll.groups

View File

@ -2,8 +2,7 @@
module DiscoursePoll module DiscoursePoll
class PollsUpdater class PollsUpdater
POLL_ATTRIBUTES ||= %w[close_at max min results status step type visibility title groups]
POLL_ATTRIBUTES ||= %w{close_at max min results status step type visibility title groups}
def self.update(post, polls) def self.update(post, polls)
::Poll.transaction do ::Poll.transaction do
@ -24,64 +23,81 @@ module DiscoursePoll
# create polls # create polls
if created_poll_names.present? if created_poll_names.present?
has_changed = true has_changed = true
polls.slice(*created_poll_names).values.each do |poll| polls.slice(*created_poll_names).values.each { |poll| Poll.create!(post.id, poll) }
Poll.create!(post.id, poll)
end
end end
# update polls # update polls
::Poll.includes(:poll_votes, :poll_options).where(post: post).find_each do |old_poll| ::Poll
new_poll = polls[old_poll.name] .includes(:poll_votes, :poll_options)
new_poll_options = new_poll["options"] .where(post: post)
.find_each do |old_poll|
new_poll = polls[old_poll.name]
new_poll_options = new_poll["options"]
attributes = new_poll.slice(*POLL_ATTRIBUTES) attributes = new_poll.slice(*POLL_ATTRIBUTES)
attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret" attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret"
attributes["close_at"] = Time.zone.parse(new_poll["close"]) rescue nil attributes["close_at"] = begin
attributes["status"] = old_poll["status"] Time.zone.parse(new_poll["close"])
attributes["groups"] = new_poll["groups"] rescue StandardError
poll = ::Poll.new(attributes) nil
end
attributes["status"] = old_poll["status"]
attributes["groups"] = new_poll["groups"]
poll = ::Poll.new(attributes)
if is_different?(old_poll, poll, new_poll_options) if is_different?(old_poll, poll, new_poll_options)
# only prevent changes when there's at least 1 vote
if old_poll.poll_votes.size > 0
# can't change after edit window (when enabled)
if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago
error =
(
if poll.name == DiscoursePoll::DEFAULT_POLL_NAME
I18n.t(
"poll.edit_window_expired.cannot_edit_default_poll_with_votes",
minutes: edit_window,
)
else
I18n.t(
"poll.edit_window_expired.cannot_edit_named_poll_with_votes",
minutes: edit_window,
name: poll.name,
)
end
)
# only prevent changes when there's at least 1 vote post.errors.add(:base, error)
if old_poll.poll_votes.size > 0 return
# can't change after edit window (when enabled) end
if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago
error = poll.name == DiscoursePoll::DEFAULT_POLL_NAME ?
I18n.t("poll.edit_window_expired.cannot_edit_default_poll_with_votes", minutes: edit_window) :
I18n.t("poll.edit_window_expired.cannot_edit_named_poll_with_votes", minutes: edit_window, name: poll.name)
post.errors.add(:base, error)
return
end end
# update poll
POLL_ATTRIBUTES.each do |attr|
old_poll.public_send("#{attr}=", poll.public_send(attr))
end
old_poll.save!
# keep track of anonymous votes
anonymous_votes =
old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h
# destroy existing options & votes
::PollOption.where(poll: old_poll).destroy_all
# create new options
new_poll_options.each do |option|
::PollOption.create!(
poll: old_poll,
digest: option["id"],
html: option["html"].strip,
anonymous_votes: anonymous_votes[option["id"]],
)
end
has_changed = true
end end
# update poll
POLL_ATTRIBUTES.each do |attr|
old_poll.public_send("#{attr}=", poll.public_send(attr))
end
old_poll.save!
# keep track of anonymous votes
anonymous_votes = old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h
# destroy existing options & votes
::PollOption.where(poll: old_poll).destroy_all
# create new options
new_poll_options.each do |option|
::PollOption.create!(
poll: old_poll,
digest: option["id"],
html: option["html"].strip,
anonymous_votes: anonymous_votes[option["id"]],
)
end
has_changed = true
end end
end
if ::Poll.exists?(post: post) if ::Poll.exists?(post: post)
post.custom_fields[HAS_POLLS] = true post.custom_fields[HAS_POLLS] = true
@ -93,7 +109,13 @@ module DiscoursePoll
if has_changed if has_changed
polls = ::Poll.includes(poll_options: :poll_votes).where(post: post) polls = ::Poll.includes(poll_options: :poll_votes).where(post: post)
polls = ActiveModel::ArraySerializer.new(polls, each_serializer: PollSerializer, root: false, scope: Guardian.new(nil)).as_json polls =
ActiveModel::ArraySerializer.new(
polls,
each_serializer: PollSerializer,
root: false,
scope: Guardian.new(nil),
).as_json
post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls) post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls)
end end
end end
@ -108,11 +130,12 @@ module DiscoursePoll
end end
# an option was changed? # an option was changed?
return true if old_poll.poll_options.map { |o| o.digest }.sort != new_options.map { |o| o["id"] }.sort if old_poll.poll_options.map { |o| o.digest }.sort != new_options.map { |o| o["id"] }.sort
return true
end
# it's the same! # it's the same!
false false
end end
end end
end end

View File

@ -2,7 +2,6 @@
module DiscoursePoll module DiscoursePoll
class PollsValidator class PollsValidator
MAX_VALUE = 2_147_483_647 MAX_VALUE = 2_147_483_647
def initialize(post) def initialize(post)
@ -12,17 +11,19 @@ module DiscoursePoll
def validate_polls def validate_polls
polls = {} polls = {}
DiscoursePoll::Poll::extract(@post.raw, @post.topic_id, @post.user_id).each do |poll| DiscoursePoll::Poll
return false unless valid_arguments?(poll) .extract(@post.raw, @post.topic_id, @post.user_id)
return false unless valid_numbers?(poll) .each do |poll|
return false unless unique_poll_name?(polls, poll) return false unless valid_arguments?(poll)
return false unless unique_options?(poll) return false unless valid_numbers?(poll)
return false unless any_blank_options?(poll) return false unless unique_poll_name?(polls, poll)
return false unless at_least_one_option?(poll) return false unless unique_options?(poll)
return false unless valid_number_of_options?(poll) return false unless any_blank_options?(poll)
return false unless valid_multiple_choice_settings?(poll) return false unless at_least_one_option?(poll)
polls[poll["name"]] = poll return false unless valid_number_of_options?(poll)
end return false unless valid_multiple_choice_settings?(poll)
polls[poll["name"]] = poll
end
polls polls
end end
@ -33,17 +34,26 @@ module DiscoursePoll
valid = true valid = true
if poll["type"].present? && !::Poll.types.has_key?(poll["type"]) if poll["type"].present? && !::Poll.types.has_key?(poll["type"])
@post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "type", value: poll["type"])) @post.errors.add(
:base,
I18n.t("poll.invalid_argument", argument: "type", value: poll["type"]),
)
valid = false valid = false
end end
if poll["status"].present? && !::Poll.statuses.has_key?(poll["status"]) if poll["status"].present? && !::Poll.statuses.has_key?(poll["status"])
@post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "status", value: poll["status"])) @post.errors.add(
:base,
I18n.t("poll.invalid_argument", argument: "status", value: poll["status"]),
)
valid = false valid = false
end end
if poll["results"].present? && !::Poll.results.has_key?(poll["results"]) if poll["results"].present? && !::Poll.results.has_key?(poll["results"])
@post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "results", value: poll["results"])) @post.errors.add(
:base,
I18n.t("poll.invalid_argument", argument: "results", value: poll["results"]),
)
valid = false valid = false
end end
@ -69,7 +79,10 @@ module DiscoursePoll
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_must_have_different_options")) @post.errors.add(:base, I18n.t("poll.default_poll_must_have_different_options"))
else else
@post.errors.add(:base, I18n.t("poll.named_poll_must_have_different_options", name: poll["name"])) @post.errors.add(
:base,
I18n.t("poll.named_poll_must_have_different_options", name: poll["name"]),
)
end end
return false return false
@ -83,7 +96,10 @@ module DiscoursePoll
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_must_not_have_any_empty_options")) @post.errors.add(:base, I18n.t("poll.default_poll_must_not_have_any_empty_options"))
else else
@post.errors.add(:base, I18n.t("poll.named_poll_must_not_have_any_empty_options", name: poll["name"])) @post.errors.add(
:base,
I18n.t("poll.named_poll_must_not_have_any_empty_options", name: poll["name"]),
)
end end
return false return false
@ -97,7 +113,10 @@ module DiscoursePoll
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option")) @post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option"))
else else
@post.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"])) @post.errors.add(
:base,
I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"]),
)
end end
return false return false
@ -109,9 +128,22 @@ module DiscoursePoll
def valid_number_of_options?(poll) def valid_number_of_options?(poll)
if poll["options"].size > SiteSetting.poll_maximum_options if poll["options"].size > SiteSetting.poll_maximum_options
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options)) @post.errors.add(
:base,
I18n.t(
"poll.default_poll_must_have_less_options",
count: SiteSetting.poll_maximum_options,
),
)
else else
@post.errors.add(:base, I18n.t("poll.named_poll_must_have_less_options", name: poll["name"], count: SiteSetting.poll_maximum_options)) @post.errors.add(
:base,
I18n.t(
"poll.named_poll_must_have_less_options",
name: poll["name"],
count: SiteSetting.poll_maximum_options,
),
)
end end
return false return false
@ -128,9 +160,18 @@ module DiscoursePoll
if min > max || min <= 0 || max <= 0 || max > options || min >= options if min > max || min <= 0 || max <= 0 || max > options || min >= options
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters")) @post.errors.add(
:base,
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
else else
@post.errors.add(:base, I18n.t("poll.named_poll_with_multiple_choices_has_invalid_parameters", name: poll["name"])) @post.errors.add(
:base,
I18n.t(
"poll.named_poll_with_multiple_choices_has_invalid_parameters",
name: poll["name"],
),
)
end end
return false return false
@ -172,7 +213,10 @@ module DiscoursePoll
if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME
@post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option")) @post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option"))
else else
@post.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"])) @post.errors.add(
:base,
I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"]),
)
end end
valid = false valid = false
end end

View File

@ -9,7 +9,13 @@ module DiscoursePoll
def validate_post def validate_post
min_trust_level = SiteSetting.poll_minimum_trust_level_to_create min_trust_level = SiteSetting.poll_minimum_trust_level_to_create
if (@post.acting_user && (@post.acting_user.staff? || @post.acting_user.trust_level >= TrustLevel[min_trust_level])) || @post.topic&.pm_with_non_human_user? if (
@post.acting_user &&
(
@post.acting_user.staff? ||
@post.acting_user.trust_level >= TrustLevel[min_trust_level]
)
) || @post.topic&.pm_with_non_human_user?
true true
else else
@post.errors.add(:base, I18n.t("poll.insufficient_rights_to_create")) @post.errors.add(:base, I18n.t("poll.insufficient_rights_to_create"))

View File

@ -28,57 +28,63 @@ end
desc "Migrate old polls to new syntax" desc "Migrate old polls to new syntax"
task "poll:migrate_old_polls" => :environment do task "poll:migrate_old_polls" => :environment do
# iterate over all polls # iterate over all polls
PluginStoreRow.where(plugin_name: "poll") PluginStoreRow
.where(plugin_name: "poll")
.where("key LIKE 'poll_options_%'") .where("key LIKE 'poll_options_%'")
.pluck(:key) .pluck(:key)
.each do |poll_options_key| .each do |poll_options_key|
# extract the post_id # extract the post_id
post_id = poll_options_key["poll_options_".length..-1].to_i post_id = poll_options_key["poll_options_".length..-1].to_i
# load the post from the db # load the post from the db
if post = Post.find_by(id: post_id) if post = Post.find_by(id: post_id)
putc "." putc "."
# skip if already migrated # skip if already migrated
next if post.custom_fields.include?("polls") next if post.custom_fields.include?("polls")
# go back in time # go back in time
freeze_time(post.created_at + 1.minute) do freeze_time(post.created_at + 1.minute) do
raw = post.raw.gsub(/\n\n([ ]*[-\*\+] )/, "\n\\1") + "\n\n" raw = post.raw.gsub(/\n\n([ ]*[-\*\+] )/, "\n\\1") + "\n\n"
# fix the RAW when needed # fix the RAW when needed
if raw !~ /\[poll\]/ if raw !~ /\[poll\]/
lists = /^[ ]*[-\*\+] .+?$\n\n/m.match(raw) lists = /^[ ]*[-\*\+] .+?$\n\n/m.match(raw)
next if lists.blank? || lists.length == 0 next if lists.blank? || lists.length == 0
first_list = lists[0] first_list = lists[0]
raw = raw.sub(first_list, "\n[poll]\n#{first_list}\n[/poll]\n") raw = raw.sub(first_list, "\n[poll]\n#{first_list}\n[/poll]\n")
end end
# save the poll # save the poll
post.raw = raw post.raw = raw
post.save post.save
# make sure we have a poll # make sure we have a poll
next if post.custom_fields.blank? || !post.custom_fields.include?("polls") next if post.custom_fields.blank? || !post.custom_fields.include?("polls")
# retrieve the new options # retrieve the new options
options = post.custom_fields["polls"]["poll"]["options"] options = post.custom_fields["polls"]["poll"]["options"]
# iterate over all votes # iterate over all votes
PluginStoreRow.where(plugin_name: "poll") PluginStoreRow
.where("key LIKE ?", "poll_vote_#{post_id}_%") .where(plugin_name: "poll")
.pluck(:key, :value) .where("key LIKE ?", "poll_vote_#{post_id}_%")
.each do |poll_vote_key, vote| .pluck(:key, :value)
# extract the user_id .each do |poll_vote_key, vote|
user_id = poll_vote_key["poll_vote_#{post_id}_%".length..-1].to_i # extract the user_id
# find the selected option user_id = poll_vote_key["poll_vote_#{post_id}_%".length..-1].to_i
vote = vote.strip # find the selected option
selected_option = options.detect { |o| o["html"].strip === vote } vote = vote.strip
# make sure we have a match selected_option = options.detect { |o| o["html"].strip === vote }
next if selected_option.blank? # make sure we have a match
# submit vote next if selected_option.blank?
DiscoursePoll::Poll.vote(post_id, "poll", [selected_option["id"]], user_id) rescue nil # submit vote
end begin
# close the poll DiscoursePoll::Poll.vote(post_id, "poll", [selected_option["id"]], user_id)
if post.topic.archived? || post.topic.closed? || poll_was_closed?(post.topic.title) rescue StandardError
post.custom_fields["polls"]["poll"]["status"] = "closed" nil
post.save_custom_fields(true) end
end
# close the poll
if post.topic.archived? || post.topic.closed? || poll_was_closed?(post.topic.title)
post.custom_fields["polls"]["poll"]["status"] = "closed"
post.save_custom_fields(true)
end
end end
end end
end end
end
puts "", "Done!" puts "", "Done!"
end end

View File

@ -30,7 +30,8 @@ after_initialize do
isolate_namespace DiscoursePoll isolate_namespace DiscoursePoll
end end
class Error < StandardError; end class Error < StandardError
end
end end
require_relative "app/controllers/polls_controller.rb" require_relative "app/controllers/polls_controller.rb"
@ -49,13 +50,11 @@ after_initialize do
put "/vote" => "polls#vote" put "/vote" => "polls#vote"
delete "/vote" => "polls#remove_vote" delete "/vote" => "polls#remove_vote"
put "/toggle_status" => "polls#toggle_status" put "/toggle_status" => "polls#toggle_status"
get "/voters" => 'polls#voters' get "/voters" => "polls#voters"
get "/grouped_poll_results" => 'polls#grouped_poll_results' get "/grouped_poll_results" => "polls#grouped_poll_results"
end end
Discourse::Application.routes.append do Discourse::Application.routes.append { mount ::DiscoursePoll::Engine, at: "/polls" }
mount ::DiscoursePoll::Engine, at: "/polls"
end
allow_new_queued_post_payload_attribute("is_poll") allow_new_queued_post_payload_attribute("is_poll")
register_post_custom_field_type(DiscoursePoll::HAS_POLLS, :boolean) register_post_custom_field_type(DiscoursePoll::HAS_POLLS, :boolean)
@ -74,18 +73,14 @@ after_initialize do
post = self post = self
Poll.transaction do Poll.transaction do
polls.values.each do |poll| polls.values.each { |poll| DiscoursePoll::Poll.create!(post.id, poll) }
DiscoursePoll::Poll.create!(post.id, poll)
end
post.custom_fields[DiscoursePoll::HAS_POLLS] = true post.custom_fields[DiscoursePoll::HAS_POLLS] = true
post.save_custom_fields(true) post.save_custom_fields(true)
end end
end end
end end
User.class_eval do User.class_eval { has_many :poll_votes, dependent: :delete_all }
has_many :poll_votes, dependent: :delete_all
end
end end
validate(:post, :validate_polls) do |force = nil| validate(:post, :validate_polls) do |force = nil|
@ -115,9 +110,7 @@ after_initialize do
if !DiscoursePoll::PollsValidator.new(post).validate_polls if !DiscoursePoll::PollsValidator.new(post).validate_polls
result = NewPostResult.new(:poll, false) result = NewPostResult.new(:poll, false)
post.errors.full_messages.each do |message| post.errors.full_messages.each { |message| result.add_error(message) }
result.add_error(message)
end
result result
else else
@ -127,9 +120,7 @@ after_initialize do
end end
on(:approved_post) do |queued_post, created_post| on(:approved_post) do |queued_post, created_post|
if queued_post.payload["is_poll"] created_post.validate_polls(true) if queued_post.payload["is_poll"]
created_post.validate_polls(true)
end
end end
on(:reduce_cooked) do |fragment, post| on(:reduce_cooked) do |fragment, post|
@ -137,22 +128,27 @@ after_initialize do
fragment.css(".poll, [data-poll-name]").each(&:remove) fragment.css(".poll, [data-poll-name]").each(&:remove)
else else
post_url = post.full_url post_url = post.full_url
fragment.css(".poll, [data-poll-name]").each do |poll| fragment
poll.replace "<p><a href='#{post_url}'>#{I18n.t("poll.email.link_to_poll")}</a></p>" .css(".poll, [data-poll-name]")
end .each do |poll|
poll.replace "<p><a href='#{post_url}'>#{I18n.t("poll.email.link_to_poll")}</a></p>"
end
end end
end end
on(:reduce_excerpt) do |doc, options| on(:reduce_excerpt) do |doc, options|
post = options[:post] post = options[:post]
replacement = post&.url.present? ? replacement =
"<a href='#{UrlHelper.normalized_encode(post.url)}'>#{I18n.t("poll.poll")}</a>" : (
I18n.t("poll.poll") if post&.url.present?
"<a href='#{UrlHelper.normalized_encode(post.url)}'>#{I18n.t("poll.poll")}</a>"
else
I18n.t("poll.poll")
end
)
doc.css("div.poll").each do |poll| doc.css("div.poll").each { |poll| poll.replace(replacement) }
poll.replace(replacement)
end
end end
on(:post_created) do |post, _opts, user| on(:post_created) do |post, _opts, user|
@ -162,7 +158,13 @@ after_initialize do
next if post.is_first_post? next if post.is_first_post?
next if post.custom_fields[DiscoursePoll::HAS_POLLS].blank? next if post.custom_fields[DiscoursePoll::HAS_POLLS].blank?
polls = ActiveModel::ArraySerializer.new(post.polls, each_serializer: PollSerializer, root: false, scope: guardian).as_json polls =
ActiveModel::ArraySerializer.new(
post.polls,
each_serializer: PollSerializer,
root: false,
scope: guardian,
).as_json
post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls) post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls)
end end
@ -171,63 +173,62 @@ after_initialize do
end end
add_to_class(:topic_view, :polls) do add_to_class(:topic_view, :polls) do
@polls ||= begin @polls ||=
polls = {} begin
polls = {}
post_with_polls = @post_custom_fields.each_with_object([]) do |fields, obj| post_with_polls =
obj << fields[0] if fields[1][DiscoursePoll::HAS_POLLS] @post_custom_fields.each_with_object([]) do |fields, obj|
end obj << fields[0] if fields[1][DiscoursePoll::HAS_POLLS]
if post_with_polls.present?
Poll
.where(post_id: post_with_polls)
.each do |p|
polls[p.post_id] ||= []
polls[p.post_id] << p
end end
end
polls if post_with_polls.present?
end Poll
.where(post_id: post_with_polls)
.each do |p|
polls[p.post_id] ||= []
polls[p.post_id] << p
end
end
polls
end
end end
add_to_serializer(:post, :preloaded_polls, false) do add_to_serializer(:post, :preloaded_polls, false) do
@preloaded_polls ||= if @topic_view.present? @preloaded_polls ||=
@topic_view.polls[object.id] if @topic_view.present?
else @topic_view.polls[object.id]
Poll.includes(:poll_options).where(post: object) else
end Poll.includes(:poll_options).where(post: object)
end
end end
add_to_serializer(:post, :include_preloaded_polls?) do add_to_serializer(:post, :include_preloaded_polls?) { false }
false
end
add_to_serializer(:post, :polls, false) do add_to_serializer(:post, :polls, false) do
preloaded_polls.map { |p| PollSerializer.new(p, root: false, scope: self.scope) } preloaded_polls.map { |p| PollSerializer.new(p, root: false, scope: self.scope) }
end end
add_to_serializer(:post, :include_polls?) do add_to_serializer(:post, :include_polls?) { SiteSetting.poll_enabled && preloaded_polls.present? }
SiteSetting.poll_enabled && preloaded_polls.present?
end
add_to_serializer(:post, :polls_votes, false) do add_to_serializer(:post, :polls_votes, false) do
preloaded_polls.map do |poll| preloaded_polls
user_poll_votes = .map do |poll|
poll user_poll_votes =
.poll_votes poll
.where(user_id: scope.user.id) .poll_votes
.joins(:poll_option) .where(user_id: scope.user.id)
.pluck("poll_options.digest") .joins(:poll_option)
.pluck("poll_options.digest")
[poll.name, user_poll_votes] [poll.name, user_poll_votes]
end.to_h end
.to_h
end end
add_to_serializer(:post, :include_polls_votes?) do add_to_serializer(:post, :include_polls_votes?) do
SiteSetting.poll_enabled && SiteSetting.poll_enabled && scope.user&.id.present? && preloaded_polls.present? &&
scope.user&.id.present? && preloaded_polls.any? { |p| p.has_voted?(scope.user) }
preloaded_polls.present? &&
preloaded_polls.any? { |p| p.has_voted?(scope.user) }
end end
end end

View File

@ -7,20 +7,48 @@ RSpec.describe ::DiscoursePoll::PollsController do
let!(:user) { log_in } let!(:user) { log_in }
let(:topic) { Fabricate(:topic) } let(:topic) { Fabricate(:topic) }
let(:poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") } let(:poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") }
let(:multi_poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll min=1 max=2 type=multiple public=true]\n- A\n- B\n[/poll]") } let(:multi_poll) do
let(:public_poll_on_vote) { Fabricate(:post, topic: topic, user: user, raw: "[poll public=true results=on_vote]\n- A\n- B\n[/poll]") } Fabricate(
let(:public_poll_on_close) { Fabricate(:post, topic: topic, user: user, raw: "[poll public=true results=on_close]\n- A\n- B\n[/poll]") } :post,
topic: topic,
user: user,
raw: "[poll min=1 max=2 type=multiple public=true]\n- A\n- B\n[/poll]",
)
end
let(:public_poll_on_vote) do
Fabricate(
:post,
topic: topic,
user: user,
raw: "[poll public=true results=on_vote]\n- A\n- B\n[/poll]",
)
end
let(:public_poll_on_close) do
Fabricate(
:post,
topic: topic,
user: user,
raw: "[poll public=true results=on_close]\n- A\n- B\n[/poll]",
)
end
describe "#vote" do describe "#vote" do
it "works" do it "works" do
channel = "/polls/#{poll.topic_id}" channel = "/polls/#{poll.topic_id}"
message = MessageBus.track_publish(channel) do message =
put :vote, params: { MessageBus
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] .track_publish(channel) do
}, format: :json put :vote,
end.first params: {
post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
end
.first
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -36,19 +64,30 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "works in PM" do it "works in PM" do
user2 = Fabricate(:user) user2 = Fabricate(:user)
topic = Fabricate(:private_message_topic, topic_allowed_users: [ topic =
Fabricate.build(:topic_allowed_user, user: user), Fabricate(
Fabricate.build(:topic_allowed_user, user: user2) :private_message_topic,
]) topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user),
Fabricate.build(:topic_allowed_user, user: user2),
],
)
poll = Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") poll = Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]")
channel = "/polls/#{poll.topic_id}" channel = "/polls/#{poll.topic_id}"
message = MessageBus.track_publish(channel) do message =
put :vote, params: { MessageBus
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] .track_publish(channel) do
}, format: :json put :vote,
end.first params: {
post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
end
.first
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -71,11 +110,18 @@ RSpec.describe ::DiscoursePoll::PollsController do
channel = "/polls/#{poll.topic_id}" channel = "/polls/#{poll.topic_id}"
message = MessageBus.track_publish(channel) do message =
put :vote, params: { MessageBus
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] .track_publish(channel) do
}, format: :json put :vote,
end.first params: {
post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
end
.first
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -90,9 +136,7 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "requires at least 1 valid option" do it "requires at least 1 valid option" do
put :vote, params: { put :vote, params: { post_id: poll.id, poll_name: "poll", options: %w[A B] }, format: :json
post_id: poll.id, poll_name: "poll", options: ["A", "B"]
}, format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -100,15 +144,23 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "supports vote changes" do it "supports vote changes" do
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["e89dec30bbd9bf50fabf6a05b4324edf"],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -118,15 +170,17 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "supports removing votes" do it "supports removing votes" do
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
delete :remove_vote, params: { delete :remove_vote, params: { post_id: poll.id, poll_name: "poll" }, format: :json
post_id: poll.id, poll_name: "poll"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -138,9 +192,13 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "works on closed topics" do it "works on closed topics" do
topic.update_attribute(:closed, true) topic.update_attribute(:closed, true)
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
@ -148,9 +206,7 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures topic is not archived" do it "ensures topic is not archived" do
topic.update_attribute(:archived, true) topic.update_attribute(:archived, true)
put :vote, params: { put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json
post_id: poll.id, poll_name: "poll", options: ["A"]
}, format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -160,9 +216,7 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures post is not trashed" do it "ensures post is not trashed" do
poll.trash! poll.trash!
put :vote, params: { put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json
post_id: poll.id, poll_name: "poll", options: ["A"]
}, format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -172,9 +226,7 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures user can post in topic" do it "ensures user can post in topic" do
Guardian.any_instance.expects(:can_create_post?).returns(false) Guardian.any_instance.expects(:can_create_post?).returns(false)
put :vote, params: { put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json
post_id: poll.id, poll_name: "poll", options: ["A"]
}, format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -182,9 +234,7 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "checks the name of the poll" do it "checks the name of the poll" do
put :vote, params: { put :vote, params: { post_id: poll.id, poll_name: "foobar", options: ["A"] }, format: :json
post_id: poll.id, poll_name: "foobar", options: ["A"]
}, format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -194,9 +244,13 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures poll is open" do it "ensures poll is open" do
closed_poll = create_post(raw: "[poll status=closed]\n- A\n- B\n[/poll]") closed_poll = create_post(raw: "[poll status=closed]\n- A\n- B\n[/poll]")
put :vote, params: { put :vote,
post_id: closed_poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: closed_poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -206,13 +260,19 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures user has required trust level" do it "ensures user has required trust level" do
poll = create_post(raw: "[poll groups=#{Fabricate(:group).name}]\n- A\n- B\n[/poll]") poll = create_post(raw: "[poll groups=#{Fabricate(:group).name}]\n- A\n- B\n[/poll]")
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("js.poll.results.groups.title", groups: poll.polls.first.groups)) expect(json["errors"][0]).to eq(
I18n.t("js.poll.results.groups.title", groups: poll.polls.first.groups),
)
end end
it "doesn't discard anonymous votes when someone votes" do it "doesn't discard anonymous votes when someone votes" do
@ -221,9 +281,13 @@ RSpec.describe ::DiscoursePoll::PollsController do
the_poll.poll_options[0].update_attribute(:anonymous_votes, 11) the_poll.poll_options[0].update_attribute(:anonymous_votes, 11)
the_poll.poll_options[1].update_attribute(:anonymous_votes, 6) the_poll.poll_options[1].update_attribute(:anonymous_votes, 6)
put :vote, params: { put :vote,
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
options: ["5c24fc1df56d764b550ceae1b9319125"],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -238,13 +302,20 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "works for OP" do it "works for OP" do
channel = "/polls/#{poll.topic_id}" channel = "/polls/#{poll.topic_id}"
message = MessageBus.track_publish(channel) do message =
put :toggle_status, params: { MessageBus
post_id: poll.id, poll_name: "poll", status: "closed" .track_publish(channel) do
}, format: :json put :toggle_status,
params: {
post_id: poll.id,
poll_name: "poll",
status: "closed",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
end.first end
.first
json = response.parsed_body json = response.parsed_body
expect(json["poll"]["status"]).to eq("closed") expect(json["poll"]["status"]).to eq("closed")
@ -256,13 +327,20 @@ RSpec.describe ::DiscoursePoll::PollsController do
channel = "/polls/#{poll.topic_id}" channel = "/polls/#{poll.topic_id}"
message = MessageBus.track_publish(channel) do message =
put :toggle_status, params: { MessageBus
post_id: poll.id, poll_name: "poll", status: "closed" .track_publish(channel) do
}, format: :json put :toggle_status,
params: {
post_id: poll.id,
poll_name: "poll",
status: "closed",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
end.first end
.first
json = response.parsed_body json = response.parsed_body
expect(json["poll"]["status"]).to eq("closed") expect(json["poll"]["status"]).to eq("closed")
@ -272,9 +350,13 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "ensures post is not trashed" do it "ensures post is not trashed" do
poll.trash! poll.trash!
put :toggle_status, params: { put :toggle_status,
post_id: poll.id, poll_name: "poll", status: "closed" params: {
}, format: :json post_id: poll.id,
poll_name: "poll",
status: "closed",
},
format: :json
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
json = response.parsed_body json = response.parsed_body
@ -289,31 +371,41 @@ RSpec.describe ::DiscoursePoll::PollsController do
it "correctly handles offset" do it "correctly handles offset" do
user1 = log_in user1 = log_in
put :vote, params: { put :vote,
post_id: multi_poll.id, poll_name: "poll", options: [first] params: {
}, format: :json post_id: multi_poll.id,
poll_name: "poll",
options: [first],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
user2 = log_in user2 = log_in
put :vote, params: { put :vote,
post_id: multi_poll.id, poll_name: "poll", options: [first] params: {
}, format: :json post_id: multi_poll.id,
poll_name: "poll",
options: [first],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
user3 = log_in user3 = log_in
put :vote, params: { put :vote,
post_id: multi_poll.id, poll_name: "poll", options: [first, second] params: {
}, format: :json post_id: multi_poll.id,
poll_name: "poll",
options: [first, second],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: multi_poll.id, limit: 2 }, format: :json
poll_name: 'poll', post_id: multi_poll.id, limit: 2
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -325,15 +417,17 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "ensures voters can only be seen after casting a vote" do it "ensures voters can only be seen after casting a vote" do
put :vote, params: { put :vote,
post_id: public_poll_on_vote.id, poll_name: "poll", options: [first] params: {
}, format: :json post_id: public_poll_on_vote.id,
poll_name: "poll",
options: [first],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json
poll_name: "poll", post_id: public_poll_on_vote.id
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -343,21 +437,21 @@ RSpec.describe ::DiscoursePoll::PollsController do
_user2 = log_in _user2 = log_in
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json
poll_name: "poll", post_id: public_poll_on_vote.id
}, format: :json
expect(response.status).to eq(400) expect(response.status).to eq(400)
put :vote, params: { put :vote,
post_id: public_poll_on_vote.id, poll_name: "poll", options: [second] params: {
}, format: :json post_id: public_poll_on_vote.id,
poll_name: "poll",
options: [second],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json
poll_name: "poll", post_id: public_poll_on_vote.id
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -368,27 +462,31 @@ RSpec.describe ::DiscoursePoll::PollsController do
end end
it "ensures voters can only be seen when poll is closed" do it "ensures voters can only be seen when poll is closed" do
put :vote, params: { put :vote,
post_id: public_poll_on_close.id, poll_name: "poll", options: [first] params: {
}, format: :json post_id: public_poll_on_close.id,
poll_name: "poll",
options: [first],
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: public_poll_on_close.id }, format: :json
poll_name: "poll", post_id: public_poll_on_close.id
}, format: :json
expect(response.status).to eq(400) expect(response.status).to eq(400)
put :toggle_status, params: { put :toggle_status,
post_id: public_poll_on_close.id, poll_name: "poll", status: "closed" params: {
}, format: :json post_id: public_poll_on_close.id,
poll_name: "poll",
status: "closed",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
get :voters, params: { get :voters, params: { poll_name: "poll", post_id: public_poll_on_close.id }, format: :json
poll_name: "poll", post_id: public_poll_on_close.id
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)

View File

@ -6,15 +6,11 @@ RSpec.describe PostsController do
let!(:user) { log_in } let!(:user) { log_in }
let!(:title) { "Testing Poll Plugin" } let!(:title) { "Testing Poll Plugin" }
before do before { SiteSetting.min_first_post_typing_time = 0 }
SiteSetting.min_first_post_typing_time = 0
end
describe "polls" do describe "polls" do
it "works" do it "works" do
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -25,9 +21,12 @@ RSpec.describe PostsController do
it "works on any post" do it "works on any post" do
post_1 = Fabricate(:post) post_1 = Fabricate(:post)
post :create, params: { post :create,
topic_id: post_1.topic.id, raw: "[poll]\n- A\n- B\n[/poll]" params: {
}, format: :json topic_id: post_1.topic.id,
raw: "[poll]\n- A\n- B\n[/poll]",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -41,12 +40,13 @@ RSpec.describe PostsController do
close_date = 1.month.from_now.round close_date = 1.month.from_now.round
expect do expect do
post :create, params: { post :create,
title: title, params: {
raw: "[poll name=#{name} close=#{close_date.iso8601}]\n- A\n- B\n[/poll]" title: title,
}, format: :json raw: "[poll name=#{name} close=#{close_date.iso8601}]\n- A\n- B\n[/poll]",
end.to change { Jobs::ClosePoll.jobs.size }.by(1) & },
change { Poll.count }.by(1) format: :json
end.to change { Jobs::ClosePoll.jobs.size }.by(1) & change { Poll.count }.by(1)
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -62,9 +62,7 @@ RSpec.describe PostsController do
end end
it "should have different options" do it "should have different options" do
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- A\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- A\n[/poll]"
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
@ -72,19 +70,20 @@ RSpec.describe PostsController do
end end
it "accepts different Chinese options" do it "accepts different Chinese options" do
SiteSetting.default_locale = 'zh_CN' SiteSetting.default_locale = "zh_CN"
post :create, params: { post :create,
title: title, raw: "[poll]\n- Microsoft Edge\n- Microsoft Edge\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll]\n- Microsoft Edge\n- Microsoft Edge\n[/poll]",
},
format: :json
expect(response).to be_successful expect(response).to be_successful
end end
it "should have at least 1 options" do it "should have at least 1 options" do
post :create, params: { post :create, params: { title: title, raw: "[poll]\n[/poll]" }, format: :json
title: title, raw: "[poll]\n[/poll]"
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
@ -96,41 +95,54 @@ RSpec.describe PostsController do
(SiteSetting.poll_maximum_options + 1).times { |n| raw << "\n- #{n}" } (SiteSetting.poll_maximum_options + 1).times { |n| raw << "\n- #{n}" }
raw << "\n[/poll]" raw << "\n[/poll]"
post :create, params: { post :create, params: { title: title, raw: raw }, format: :json
title: title, raw: raw
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options)) expect(json["errors"][0]).to eq(
I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options),
)
end end
it "should have valid parameters" do it "should have valid parameters" do
post :create, params: { post :create,
title: title, raw: "[poll type=multiple min=5]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll type=multiple min=5]\n- A\n- B\n[/poll]",
},
format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters")) expect(json["errors"][0]).to eq(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end end
it "prevents self-xss" do it "prevents self-xss" do
post :create, params: { post :create,
title: title, raw: "[poll name=<script>alert('xss')</script>]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll name=<script>alert('xss')</script>]\n- A\n- B\n[/poll]",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json["cooked"]).to match("data-poll-") expect(json["cooked"]).to match("data-poll-")
expect(json["cooked"]).to include("&lt;script&gt;") expect(json["cooked"]).to include("&lt;script&gt;")
expect(Poll.find_by(post_id: json["id"]).name).to eq("&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;") expect(Poll.find_by(post_id: json["id"]).name).to eq(
"&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;",
)
end end
it "also works when there is a link starting with '[poll'" do it "also works when there is a link starting with '[poll'" do
post :create, params: { post :create,
title: title, raw: "[Polls are awesome](/foobar)\n[poll]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[Polls are awesome](/foobar)\n[poll]\n- A\n- B\n[/poll]",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -139,9 +151,12 @@ RSpec.describe PostsController do
end end
it "prevents poll-inception" do it "prevents poll-inception" do
post :create, params: { post :create,
title: title, raw: "[poll name=1]\n- A\n[poll name=2]\n- B\n- C\n[/poll]\n- D\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll name=1]\n- A\n[poll name=2]\n- B\n- C\n[/poll]\n- D\n[/poll]",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -150,9 +165,12 @@ RSpec.describe PostsController do
end end
it "accepts polls with titles" do it "accepts polls with titles" do
post :create, params: { post :create,
title: title, raw: "[poll]\n# What's up?\n- one\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll]\n# What's up?\n- one\n[/poll]",
},
format: :json
expect(response).to be_successful expect(response).to be_successful
poll = Poll.last poll = Poll.last
@ -161,23 +179,24 @@ RSpec.describe PostsController do
end end
describe "edit window" do describe "edit window" do
describe "within the first 5 minutes" do describe "within the first 5 minutes" do
let(:post_id) do let(:post_id) do
freeze_time(4.minutes.ago) do freeze_time(4.minutes.ago) do
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
response.parsed_body["id"] response.parsed_body["id"]
end end
end end
it "can be changed" do it "can be changed" do
put :update, params: { put :update,
id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } params: {
}, format: :json id: post_id,
post: {
raw: "[poll]\n- A\n- B\n- C\n[/poll]",
},
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -187,28 +206,29 @@ RSpec.describe PostsController do
it "resets the votes" do it "resets the votes" do
DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"]) DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"])
put :update, params: { put :update,
id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } params: {
}, format: :json id: post_id,
post: {
raw: "[poll]\n- A\n- B\n- C\n[/poll]",
},
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json["post"]["polls_votes"]).to_not be expect(json["post"]["polls_votes"]).to_not be
end end
end end
describe "after the poll edit window has expired" do describe "after the poll edit window has expired" do
let(:poll) { "[poll]\n- A\n- B\n[/poll]" } let(:poll) { "[poll]\n- A\n- B\n[/poll]" }
let(:new_option) { "[poll]\n- A\n- C\n[/poll]" } let(:new_option) { "[poll]\n- A\n- C\n[/poll]" }
let(:updated) { "before\n\n[poll]\n- A\n- B\n[/poll]\n\nafter" } let(:updated) { "before\n\n[poll]\n- A\n- B\n[/poll]\n\nafter" }
let(:post_id) do let(:post_id) do
freeze_time(6.minutes.ago) do freeze_time(6.minutes.ago) do
post :create, params: { post :create, params: { title: title, raw: poll }, format: :json
title: title, raw: poll
}, format: :json
response.parsed_body["id"] response.parsed_body["id"]
end end
@ -216,16 +236,11 @@ RSpec.describe PostsController do
let(:poll_edit_window_mins) { 6 } let(:poll_edit_window_mins) { 6 }
before do before { SiteSetting.poll_edit_window_mins = poll_edit_window_mins }
SiteSetting.poll_edit_window_mins = poll_edit_window_mins
end
describe "with no vote" do describe "with no vote" do
it "can change the options" do it "can change the options" do
put :update, params: { put :update, params: { id: post_id, post: { raw: new_option } }, format: :json
id: post_id, post: { raw: new_option }
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -238,26 +253,24 @@ RSpec.describe PostsController do
json = response.parsed_body json = response.parsed_body
expect(json["post"]["cooked"]).to match("before") expect(json["post"]["cooked"]).to match("before")
end end
end end
describe "with at least one vote" do describe "with at least one vote" do
before do before do
DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"]) DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"])
end end
it "cannot change the options" do it "cannot change the options" do
put :update, params: { put :update, params: { id: post_id, post: { raw: new_option } }, format: :json
id: post_id, post: { raw: new_option }
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t( expect(json["errors"][0]).to eq(
"poll.edit_window_expired.cannot_edit_default_poll_with_votes", I18n.t(
minutes: poll_edit_window_mins "poll.edit_window_expired.cannot_edit_default_poll_with_votes",
)) minutes: poll_edit_window_mins,
),
)
end end
it "support changes on the post" do it "support changes on the post" do
@ -266,45 +279,49 @@ RSpec.describe PostsController do
json = response.parsed_body json = response.parsed_body
expect(json["post"]["cooked"]).to match("before") expect(json["post"]["cooked"]).to match("before")
end end
end end
end end
end end
end end
describe "named polls" do describe "named polls" do
it "should have different options" do it "should have different options" do
post :create, params: { post :create,
title: title, raw: "[poll name=""foo""]\n- A\n- A\n[/poll]" params: {
}, format: :json title: title,
raw:
"[poll name=" \
"foo" \
"]\n- A\n- A\n[/poll]",
},
format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_different_options", name: "foo")) expect(json["errors"][0]).to eq(
I18n.t("poll.named_poll_must_have_different_options", name: "foo"),
)
end end
it "should have at least 1 option" do it "should have at least 1 option" do
post :create, params: { post :create, params: { title: title, raw: "[poll name='foo']\n[/poll]" }, format: :json
title: title, raw: "[poll name='foo']\n[/poll]"
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_at_least_1_option", name: "foo")) expect(json["errors"][0]).to eq(
I18n.t("poll.named_poll_must_have_at_least_1_option", name: "foo"),
)
end end
end end
describe "multiple polls" do describe "multiple polls" do
it "works" do it "works" do
post :create, params: { post :create,
title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]",
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -313,9 +330,12 @@ RSpec.describe PostsController do
end end
it "should have a name" do it "should have a name" do
post :create, params: { post :create,
title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]",
},
format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
@ -323,46 +343,42 @@ RSpec.describe PostsController do
end end
it "should have unique name" do it "should have unique name" do
post :create, params: { post :create,
title: title, raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" params: {
}, format: :json title: title,
raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]",
},
format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_with_same_name", name: "foo")) expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_with_same_name", name: "foo"))
end end
end end
describe "disabled polls" do describe "disabled polls" do
before do before { SiteSetting.poll_enabled = false }
SiteSetting.poll_enabled = false
end
it "doesnt cook the poll" do it "doesnt cook the poll" do
log_in_user(Fabricate(:user, admin: true, trust_level: 4)) log_in_user(Fabricate(:user, admin: true, trust_level: 4))
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json["cooked"]).to eq("<p>[poll]</p>\n<ul>\n<li>A</li>\n<li>B<br>\n[/poll]</li>\n</ul>") expect(json["cooked"]).to eq(
"<p>[poll]</p>\n<ul>\n<li>A</li>\n<li>B<br>\n[/poll]</li>\n</ul>",
)
end end
end end
describe "regular user with insufficient trust level" do describe "regular user with insufficient trust level" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 2 }
SiteSetting.poll_minimum_trust_level_to_create = 2
end
it "invalidates the post" do it "invalidates the post" do
log_in_user(Fabricate(:user, trust_level: 1)) log_in_user(Fabricate(:user, trust_level: 1))
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response).not_to be_successful expect(response).not_to be_successful
json = response.parsed_body json = response.parsed_body
@ -371,33 +387,31 @@ RSpec.describe PostsController do
it "skips the check in PMs with bots" do it "skips the check in PMs with bots" do
user = Fabricate(:user, trust_level: 1) user = Fabricate(:user, trust_level: 1)
topic = Fabricate(:private_message_topic, topic_allowed_users: [ topic =
Fabricate.build(:topic_allowed_user, user: user), Fabricate(
Fabricate.build(:topic_allowed_user, user: Discourse.system_user) :private_message_topic,
]) topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user),
Fabricate.build(:topic_allowed_user, user: Discourse.system_user),
],
)
Fabricate(:post, topic_id: topic.id, user_id: Discourse::SYSTEM_USER_ID) Fabricate(:post, topic_id: topic.id, user_id: Discourse::SYSTEM_USER_ID)
log_in_user(user) log_in_user(user)
post :create, params: { post :create, params: { topic_id: topic.id, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
topic_id: topic.id, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.parsed_body["errors"]).to eq(nil) expect(response.parsed_body["errors"]).to eq(nil)
end end
end end
describe "regular user with equal trust level" do describe "regular user with equal trust level" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 2 }
SiteSetting.poll_minimum_trust_level_to_create = 2
end
it "validates the post" do it "validates the post" do
log_in_user(Fabricate(:user, trust_level: 2)) log_in_user(Fabricate(:user, trust_level: 2))
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -407,16 +421,12 @@ RSpec.describe PostsController do
end end
describe "regular user with superior trust level" do describe "regular user with superior trust level" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 2 }
SiteSetting.poll_minimum_trust_level_to_create = 2
end
it "validates the post" do it "validates the post" do
log_in_user(Fabricate(:user, trust_level: 3)) log_in_user(Fabricate(:user, trust_level: 3))
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -426,16 +436,12 @@ RSpec.describe PostsController do
end end
describe "staff with insufficient trust level" do describe "staff with insufficient trust level" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 2 }
SiteSetting.poll_minimum_trust_level_to_create = 2
end
it "validates the post" do it "validates the post" do
log_in_user(Fabricate(:user, moderator: true, trust_level: 1)) log_in_user(Fabricate(:user, moderator: true, trust_level: 1))
post :create, params: { post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json
title: title, raw: "[poll]\n- A\n- B\n[/poll]"
}, format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -445,9 +451,7 @@ RSpec.describe PostsController do
end end
describe "staff editing posts of users with insufficient trust level" do describe "staff editing posts of users with insufficient trust level" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 2 }
SiteSetting.poll_minimum_trust_level_to_create = 2
end
it "validates the post" do it "validates the post" do
log_in_user(Fabricate(:user, trust_level: 1)) log_in_user(Fabricate(:user, trust_level: 1))
@ -459,9 +463,14 @@ RSpec.describe PostsController do
log_in_user(Fabricate(:admin)) log_in_user(Fabricate(:admin))
put :update, params: { put :update,
id: post_id, post: { raw: "#{title}\n[poll]\n- A\n- B\n- C\n[/poll]" } params: {
}, format: :json id: post_id,
post: {
raw: "#{title}\n[poll]\n- A\n- B\n- C\n[/poll]",
},
},
format: :json
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["post"]["polls"][0]["options"][2]["html"]).to eq("C") expect(response.parsed_body["post"]["polls"][0]["options"][2]["html"]).to eq("C")

View File

@ -7,31 +7,25 @@ RSpec.describe "DiscoursePoll endpoints" do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:post) { Fabricate(:post, raw: "[poll public=true]\n- A\n- B\n[/poll]") } fab!(:post) { Fabricate(:post, raw: "[poll public=true]\n- A\n- B\n[/poll]") }
fab!(:post_with_multiple_poll) do fab!(:post_with_multiple_poll) { Fabricate(:post, raw: <<~SQL) }
Fabricate(:post, raw: <<~SQL)
[poll type=multiple public=true min=1 max=2] [poll type=multiple public=true min=1 max=2]
- A - A
- B - B
- C - C
[/poll] [/poll]
SQL SQL
end
let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" } let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" }
let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" } let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" }
it "should return the right response" do it "should return the right response" do
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(user, post.id, DiscoursePoll::DEFAULT_POLL_NAME, [option_a])
user,
post.id,
DiscoursePoll::DEFAULT_POLL_NAME,
[option_a]
)
get "/polls/voters.json", params: { get "/polls/voters.json",
post_id: post.id, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME post_id: post.id,
} poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -43,19 +37,20 @@ RSpec.describe "DiscoursePoll endpoints" do
expect(option.first["username"]).to eq(user.username) expect(option.first["username"]).to eq(user.username)
end end
it 'should return the right response for a single option' do it "should return the right response for a single option" do
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
DiscoursePoll::DEFAULT_POLL_NAME, DiscoursePoll::DEFAULT_POLL_NAME,
[option_a, option_b] [option_a, option_b],
) )
get "/polls/voters.json", params: { get "/polls/voters.json",
post_id: post_with_multiple_poll.id, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME, post_id: post_with_multiple_poll.id,
option_id: option_b poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
} option_id: option_b,
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -70,56 +65,60 @@ RSpec.describe "DiscoursePoll endpoints" do
expect(option.first["username"]).to eq(user.username) expect(option.first["username"]).to eq(user.username)
end end
describe 'when post_id is blank' do describe "when post_id is blank" do
it 'should raise the right error' do it "should raise the right error" do
get "/polls/voters.json", params: { poll_name: DiscoursePoll::DEFAULT_POLL_NAME } get "/polls/voters.json", params: { poll_name: DiscoursePoll::DEFAULT_POLL_NAME }
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
describe 'when post_id is not valid' do describe "when post_id is not valid" do
it 'should raise the right error' do it "should raise the right error" do
get "/polls/voters.json", params: { get "/polls/voters.json",
post_id: -1, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME post_id: -1,
} poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
}
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(response.body).to include('post_id') expect(response.body).to include("post_id")
end end
end end
describe 'when poll_name is blank' do describe "when poll_name is blank" do
it 'should raise the right error' do it "should raise the right error" do
get "/polls/voters.json", params: { post_id: post.id } get "/polls/voters.json", params: { post_id: post.id }
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
describe 'when poll_name is not valid' do describe "when poll_name is not valid" do
it 'should raise the right error' do it "should raise the right error" do
get "/polls/voters.json", params: { post_id: post.id, poll_name: 'wrongpoll' } get "/polls/voters.json", params: { post_id: post.id, poll_name: "wrongpoll" }
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(response.body).to include('poll_name') expect(response.body).to include("poll_name")
end end
end end
context "with number poll" do context "with number poll" do
let(:post) { Fabricate(:post, raw: "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]") } let(:post) do
Fabricate(:post, raw: "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]")
end
it 'should return the right response' do it "should return the right response" do
post post
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user, user,
post.id, post.id,
DiscoursePoll::DEFAULT_POLL_NAME, DiscoursePoll::DEFAULT_POLL_NAME,
["4d8a15e3cc35750f016ce15a43937620"] ["4d8a15e3cc35750f016ce15a43937620"],
) )
get "/polls/voters.json", params: { get "/polls/voters.json",
post_id: post.id, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME post_id: post.id,
} poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -137,31 +136,25 @@ RSpec.describe "DiscoursePoll endpoints" do
fab!(:user3) { Fabricate(:user) } fab!(:user3) { Fabricate(:user) }
fab!(:user4) { Fabricate(:user) } fab!(:user4) { Fabricate(:user) }
fab!(:post) do fab!(:post) { Fabricate(:post, raw: <<~SQL) }
Fabricate(:post, raw: <<~SQL)
[poll type=multiple public=true min=1 max=2] [poll type=multiple public=true min=1 max=2]
- A - A
- B - B
[/poll] [/poll]
SQL SQL
end
let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" } let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" }
let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" } let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" }
before do before do
user_votes = { user_votes = { user_0: option_a, user_1: option_a, user_2: option_b }
user_0: option_a,
user_1: option_a,
user_2: option_b,
}
[user1, user2, user3].each_with_index do |user, index| [user1, user2, user3].each_with_index do |user, index|
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user, user,
post.id, post.id,
DiscoursePoll::DEFAULT_POLL_NAME, DiscoursePoll::DEFAULT_POLL_NAME,
[user_votes["user_#{index}".to_sym]] [user_votes["user_#{index}".to_sym]],
) )
UserCustomField.create(user_id: user.id, name: "something", value: "value#{index}") UserCustomField.create(user_id: user.id, name: "something", value: "value#{index}")
end end
@ -171,7 +164,7 @@ RSpec.describe "DiscoursePoll endpoints" do
user4, user4,
post.id, post.id,
DiscoursePoll::DEFAULT_POLL_NAME, DiscoursePoll::DEFAULT_POLL_NAME,
[option_a, option_b] [option_a, option_b],
) )
UserCustomField.create(user_id: user4.id, name: "something", value: "value1") UserCustomField.create(user_id: user4.id, name: "something", value: "value1")
end end
@ -179,32 +172,52 @@ RSpec.describe "DiscoursePoll endpoints" do
it "returns grouped poll results based on user field" do it "returns grouped poll results based on user field" do
SiteSetting.poll_groupable_user_fields = "something" SiteSetting.poll_groupable_user_fields = "something"
get "/polls/grouped_poll_results.json", params: { get "/polls/grouped_poll_results.json",
post_id: post.id, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME, post_id: post.id,
user_field_name: "something" poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
} user_field_name: "something",
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body.deep_symbolize_keys).to eq( expect(response.parsed_body.deep_symbolize_keys).to eq(
grouped_results: [ grouped_results: [
{ group: "Value0", options: [{ digest: option_a, html: "A", votes: 1 }, { digest: option_b, html: "B", votes: 0 }] }, {
{ group: "Value1", options: [{ digest: option_a, html: "A", votes: 2 }, { digest: option_b, html: "B", votes: 1 }] }, group: "Value0",
{ group: "Value2", options: [{ digest: option_a, html: "A", votes: 0 }, { digest: option_b, html: "B", votes: 1 }] }, options: [
] { digest: option_a, html: "A", votes: 1 },
{ digest: option_b, html: "B", votes: 0 },
],
},
{
group: "Value1",
options: [
{ digest: option_a, html: "A", votes: 2 },
{ digest: option_b, html: "B", votes: 1 },
],
},
{
group: "Value2",
options: [
{ digest: option_a, html: "A", votes: 0 },
{ digest: option_b, html: "B", votes: 1 },
],
},
],
) )
end end
it "returns an error when poll_groupable_user_fields is empty" do it "returns an error when poll_groupable_user_fields is empty" do
SiteSetting.poll_groupable_user_fields = "" SiteSetting.poll_groupable_user_fields = ""
get "/polls/grouped_poll_results.json", params: { get "/polls/grouped_poll_results.json",
post_id: post.id, params: {
poll_name: DiscoursePoll::DEFAULT_POLL_NAME, post_id: post.id,
user_field_name: "something" poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
} user_field_name: "something",
}
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(response.body).to include('user_field_name') expect(response.body).to include("user_field_name")
end end
end end
end end

View File

@ -5,15 +5,17 @@ require "rails_helper"
RSpec.describe Jobs::ClosePoll do RSpec.describe Jobs::ClosePoll do
let(:post) { Fabricate(:post, raw: "[poll]\n- A\n- B\n[/poll]") } let(:post) { Fabricate(:post, raw: "[poll]\n- A\n- B\n[/poll]") }
describe 'missing arguments' do describe "missing arguments" do
it 'should raise the right error' do it "should raise the right error" do
expect do expect do Jobs::ClosePoll.new.execute(post_id: post.id) end.to raise_error(
Jobs::ClosePoll.new.execute(post_id: post.id) Discourse::InvalidParameters,
end.to raise_error(Discourse::InvalidParameters, "poll_name") "poll_name",
)
expect do expect do Jobs::ClosePoll.new.execute(poll_name: "poll") end.to raise_error(
Jobs::ClosePoll.new.execute(poll_name: "poll") Discourse::InvalidParameters,
end.to raise_error(Discourse::InvalidParameters, "post_id") "post_id",
)
end end
end end
@ -24,5 +26,4 @@ RSpec.describe Jobs::ClosePoll do
expect(post.polls.first.closed?).to eq(true) expect(post.polls.first.closed?).to eq(true)
end end
end end

View File

@ -7,9 +7,7 @@ RSpec.describe NewPostManager do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
describe "when new post containing a poll is queued for approval" do describe "when new post containing a poll is queued for approval" do
before do before { SiteSetting.poll_minimum_trust_level_to_create = 0 }
SiteSetting.poll_minimum_trust_level_to_create = 0
end
let(:params) do let(:params) do
{ {
@ -23,9 +21,10 @@ RSpec.describe NewPostManager do
is_warning: false, is_warning: false,
title: "This is a test post with a poll", title: "This is a test post with a poll",
ip_address: "127.0.0.1", ip_address: "127.0.0.1",
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", user_agent:
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
referrer: "http://localhost:3000/", referrer: "http://localhost:3000/",
first_post_checks: true first_post_checks: true,
} }
end end
@ -38,7 +37,7 @@ RSpec.describe NewPostManager do
expect(Poll.where(post: review_result.created_post).exists?).to eq(true) expect(Poll.where(post: review_result.created_post).exists?).to eq(true)
end end
it 're-validates the poll when the approve_post event is triggered' do it "re-validates the poll when the approve_post event is triggered" do
invalid_raw_poll = <<~MD invalid_raw_poll = <<~MD
[poll type=multiple min=0] [poll type=multiple min=0]
* 1 * 1

View File

@ -4,17 +4,14 @@ RSpec.describe DiscoursePoll::Poll do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) } fab!(:user_2) { Fabricate(:user) }
fab!(:post_with_regular_poll) do fab!(:post_with_regular_poll) { Fabricate(:post, raw: <<~RAW) }
Fabricate(:post, raw: <<~RAW)
[poll] [poll]
* 1 * 1
* 2 * 2
[/poll] [/poll]
RAW RAW
end
fab!(:post_with_multiple_poll) do fab!(:post_with_multiple_poll) { Fabricate(:post, raw: <<~RAW) }
Fabricate(:post, raw: <<~RAW)
[poll type=multiple min=2 max=3] [poll type=multiple min=2 max=3]
* 1 * 1
* 2 * 2
@ -23,10 +20,9 @@ RSpec.describe DiscoursePoll::Poll do
* 5 * 5
[/poll] [/poll]
RAW RAW
end
describe '.vote' do describe ".vote" do
it 'should only allow one vote per user for a regular poll' do it "should only allow one vote per user for a regular poll" do
poll = post_with_regular_poll.polls.first poll = post_with_regular_poll.polls.first
expect do expect do
@ -34,46 +30,35 @@ RSpec.describe DiscoursePoll::Poll do
user, user,
post_with_regular_poll.id, post_with_regular_poll.id,
"poll", "poll",
poll.poll_options.map(&:digest) poll.poll_options.map(&:digest),
) )
end.to raise_error(DiscoursePoll::Error, I18n.t("poll.one_vote_per_user")) end.to raise_error(DiscoursePoll::Error, I18n.t("poll.one_vote_per_user"))
end end
it 'should clean up bad votes for a regular poll' do it "should clean up bad votes for a regular poll" do
poll = post_with_regular_poll.polls.first poll = post_with_regular_poll.polls.first
PollVote.create!( PollVote.create!(poll: poll, poll_option: poll.poll_options.first, user: user)
poll: poll,
poll_option: poll.poll_options.first,
user: user
)
PollVote.create!( PollVote.create!(poll: poll, poll_option: poll.poll_options.last, user: user)
poll: poll,
poll_option: poll.poll_options.last,
user: user
)
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user, user,
post_with_regular_poll.id, post_with_regular_poll.id,
"poll", "poll",
[poll.poll_options.first.digest] [poll.poll_options.first.digest],
) )
expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly(
.to contain_exactly(poll.poll_options.first.id) poll.poll_options.first.id,
)
end end
it 'allows user to vote on multiple options correctly for a multiple poll' do it "allows user to vote on multiple options correctly for a multiple poll" do
poll = post_with_multiple_poll.polls.first poll = post_with_multiple_poll.polls.first
poll_options = poll.poll_options poll_options = poll.poll_options
[ [poll_options.first, poll_options.second, poll_options.third].each do |poll_option|
poll_options.first,
poll_options.second,
poll_options.third,
].each do |poll_option|
PollVote.create!(poll: poll, poll_option: poll_option, user: user) PollVote.create!(poll: poll, poll_option: poll_option, user: user)
end end
@ -81,24 +66,28 @@ RSpec.describe DiscoursePoll::Poll do
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
[poll_options.first.digest, poll_options.second.digest] [poll_options.first.digest, poll_options.second.digest],
) )
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user_2, user_2,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
[poll_options.third.digest, poll_options.fourth.digest] [poll_options.third.digest, poll_options.fourth.digest],
) )
expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly(
.to contain_exactly(poll_options.first.id, poll_options.second.id) poll_options.first.id,
poll_options.second.id,
)
expect(PollVote.where(poll: poll, user: user_2).pluck(:poll_option_id)) expect(PollVote.where(poll: poll, user: user_2).pluck(:poll_option_id)).to contain_exactly(
.to contain_exactly(poll_options.third.id, poll_options.fourth.id) poll_options.third.id,
poll_options.fourth.id,
)
end end
it 'should respect the min/max votes per user for a multiple poll' do it "should respect the min/max votes per user for a multiple poll" do
poll = post_with_multiple_poll.polls.first poll = post_with_multiple_poll.polls.first
expect do expect do
@ -106,27 +95,21 @@ RSpec.describe DiscoursePoll::Poll do
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
poll.poll_options.map(&:digest) poll.poll_options.map(&:digest),
) )
end.to raise_error( end.to raise_error(DiscoursePoll::Error, I18n.t("poll.max_vote_per_user", count: poll.max))
DiscoursePoll::Error,
I18n.t("poll.max_vote_per_user", count: poll.max)
)
expect do expect do
DiscoursePoll::Poll.vote( DiscoursePoll::Poll.vote(
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
[poll.poll_options.first.digest] [poll.poll_options.first.digest],
) )
end.to raise_error( end.to raise_error(DiscoursePoll::Error, I18n.t("poll.min_vote_per_user", count: poll.min))
DiscoursePoll::Error,
I18n.t("poll.min_vote_per_user", count: poll.min)
)
end end
it 'should allow user to vote on a multiple poll even if min option is not configured' do it "should allow user to vote on a multiple poll even if min option is not configured" do
post_with_multiple_poll = Fabricate(:post, raw: <<~RAW) post_with_multiple_poll = Fabricate(:post, raw: <<~RAW)
[poll type=multiple max=3] [poll type=multiple max=3]
* 1 * 1
@ -143,14 +126,15 @@ RSpec.describe DiscoursePoll::Poll do
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
[poll.poll_options.first.digest] [poll.poll_options.first.digest],
) )
expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly(
.to contain_exactly(poll.poll_options.first.id) poll.poll_options.first.id,
)
end end
it 'should allow user to vote on a multiple poll even if max option is not configured' do it "should allow user to vote on a multiple poll even if max option is not configured" do
post_with_multiple_poll = Fabricate(:post, raw: <<~RAW) post_with_multiple_poll = Fabricate(:post, raw: <<~RAW)
[poll type=multiple min=1] [poll type=multiple min=1]
* 1 * 1
@ -167,11 +151,13 @@ RSpec.describe DiscoursePoll::Poll do
user, user,
post_with_multiple_poll.id, post_with_multiple_poll.id,
"poll", "poll",
[poll.poll_options.first.digest, poll.poll_options.second.digest] [poll.poll_options.first.digest, poll.poll_options.second.digest],
) )
expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly(
.to contain_exactly(poll.poll_options.first.id, poll.poll_options.second.id) poll.poll_options.first.id,
poll.poll_options.second.id,
)
end end
end end
@ -179,19 +165,14 @@ RSpec.describe DiscoursePoll::Poll do
it "publishes on message bus if a there are polls" do it "publishes on message bus if a there are polls" do
first_post = Fabricate(:post) first_post = Fabricate(:post)
topic = first_post.topic topic = first_post.topic
creator = PostCreator.new(user, creator = PostCreator.new(user, topic_id: topic.id, raw: <<~RAW)
topic_id: topic.id,
raw: <<~RAW
[poll] [poll]
* 1 * 1
* 2 * 2
[/poll] [/poll]
RAW RAW
)
messages = MessageBus.track_publish("/polls/#{topic.id}") do messages = MessageBus.track_publish("/polls/#{topic.id}") { creator.create! }
creator.create!
end
expect(messages.count).to eq(1) expect(messages.count).to eq(1)
end end
@ -199,20 +180,16 @@ RSpec.describe DiscoursePoll::Poll do
it "does not publish on message bus when a post with no polls is created" do it "does not publish on message bus when a post with no polls is created" do
first_post = Fabricate(:post) first_post = Fabricate(:post)
topic = first_post.topic topic = first_post.topic
creator = PostCreator.new(user, creator =
topic_id: topic.id, PostCreator.new(user, topic_id: topic.id, raw: "Just a post with definitely no polls")
raw: "Just a post with definitely no polls"
)
messages = MessageBus.track_publish("/polls/#{topic.id}") do messages = MessageBus.track_publish("/polls/#{topic.id}") { creator.create! }
creator.create!
end
expect(messages.count).to eq(0) expect(messages.count).to eq(0)
end end
end end
describe '.extract' do describe ".extract" do
it "skips the polls inside quote" do it "skips the polls inside quote" do
raw = <<~RAW raw = <<~RAW
[quote="username, post:1, topic:2"] [quote="username, post:1, topic:2"]
@ -230,18 +207,17 @@ RSpec.describe DiscoursePoll::Poll do
Post with a poll and a quoted poll. Post with a poll and a quoted poll.
RAW RAW
expect(DiscoursePoll::Poll.extract(raw, 2)).to contain_exactly({ expect(DiscoursePoll::Poll.extract(raw, 2)).to contain_exactly(
"name" => "poll", {
"options" => [{ "name" => "poll",
"html" => "3", "options" => [
"id" => "68b434ff88aeae7054e42cd05a4d9056" { "html" => "3", "id" => "68b434ff88aeae7054e42cd05a4d9056" },
}, { { "html" => "4", "id" => "aa2393b424f2f395abb63bf785760a3b" },
"html" => "4", ],
"id" => "aa2393b424f2f395abb63bf785760a3b" "status" => "open",
}], "type" => "regular",
"status" => "open", },
"type" => "regular" )
})
end end
end end
end end

View File

@ -1,68 +1,54 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe DiscoursePoll::PollsUpdater do RSpec.describe DiscoursePoll::PollsUpdater do
def update(post, polls) def update(post, polls)
DiscoursePoll::PollsUpdater.update(post, polls) DiscoursePoll::PollsUpdater.update(post, polls)
end end
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:post) { let(:post) { Fabricate(:post, raw: <<~RAW) }
Fabricate(:post, raw: <<~RAW)
[poll] [poll]
* 1 * 1
* 2 * 2
[/poll] [/poll]
RAW RAW
}
let(:post_with_3_options) { let(:post_with_3_options) { Fabricate(:post, raw: <<~RAW) }
Fabricate(:post, raw: <<~RAW)
[poll] [poll]
- a - a
- b - b
- c - c
[/poll] [/poll]
RAW RAW
}
let(:post_with_some_attributes) { let(:post_with_some_attributes) { Fabricate(:post, raw: <<~RAW) }
Fabricate(:post, raw: <<~RAW)
[poll close=#{1.week.from_now.to_formatted_s(:iso8601)} results=on_close] [poll close=#{1.week.from_now.to_formatted_s(:iso8601)} results=on_close]
- A - A
- B - B
- C - C
[/poll] [/poll]
RAW RAW
}
let(:polls) { let(:polls) { DiscoursePoll::PollsValidator.new(post).validate_polls }
DiscoursePoll::PollsValidator.new(post).validate_polls
}
let(:polls_with_3_options) { let(:polls_with_3_options) do
DiscoursePoll::PollsValidator.new(post_with_3_options).validate_polls DiscoursePoll::PollsValidator.new(post_with_3_options).validate_polls
} end
let(:polls_with_some_attributes) { let(:polls_with_some_attributes) do
DiscoursePoll::PollsValidator.new(post_with_some_attributes).validate_polls DiscoursePoll::PollsValidator.new(post_with_some_attributes).validate_polls
} end
describe "update" do describe "update" do
it "does nothing when there are no changes" do it "does nothing when there are no changes" do
message = MessageBus.track_publish("/polls/#{post.topic_id}") do message = MessageBus.track_publish("/polls/#{post.topic_id}") { update(post, polls) }.first
update(post, polls)
end.first
expect(message).to be(nil) expect(message).to be(nil)
end end
describe "when editing" do describe "when editing" do
let(:raw) { <<~RAW }
let(:raw) do
<<~RAW
This is a new poll with three options. This is a new poll with three options.
[poll type=multiple results=always min=1 max=2] [poll type=multiple results=always min=1 max=2]
@ -71,7 +57,6 @@ RSpec.describe DiscoursePoll::PollsUpdater do
* third * third
[/poll] [/poll]
RAW RAW
end
let(:post) { Fabricate(:post, raw: raw) } let(:post) { Fabricate(:post, raw: raw) }
@ -84,11 +69,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(post.errors[:base].size).to equal(0) expect(post.errors[:base].size).to equal(0)
end end
end end
describe "deletes polls" do describe "deletes polls" do
it "that were removed" do it "that were removed" do
update(post, {}) update(post, {})
@ -97,19 +80,15 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(Poll.where(post: post).exists?).to eq(false) expect(Poll.where(post: post).exists?).to eq(false)
expect(post.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(nil) expect(post.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(nil)
end end
end end
describe "creates polls" do describe "creates polls" do
it "that were added" do it "that were added" do
post = Fabricate(:post) post = Fabricate(:post)
expect(Poll.find_by(post: post)).to_not be expect(Poll.find_by(post: post)).to_not be
message = MessageBus.track_publish("/polls/#{post.topic_id}") do message = MessageBus.track_publish("/polls/#{post.topic_id}") { update(post, polls) }.first
update(post, polls)
end.first
poll = Poll.find_by(post: post) poll = Poll.find_by(post: post)
@ -121,21 +100,19 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(message.data[:post_id]).to eq(post.id) expect(message.data[:post_id]).to eq(post.id)
expect(message.data[:polls][0][:name]).to eq(poll.name) expect(message.data[:polls][0][:name]).to eq(poll.name)
end end
end end
describe "updates polls" do describe "updates polls" do
describe "when there are no votes" do describe "when there are no votes" do
it "at any time" do it "at any time" do
post # create the post post # create the post
freeze_time 1.month.from_now freeze_time 1.month.from_now
message = MessageBus.track_publish("/polls/#{post.topic_id}") do message =
update(post, polls_with_some_attributes) MessageBus
end.first .track_publish("/polls/#{post.topic_id}") { update(post, polls_with_some_attributes) }
.first
poll = Poll.find_by(post: post) poll = Poll.find_by(post: post)
@ -150,11 +127,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(message.data[:post_id]).to eq(post.id) expect(message.data[:post_id]).to eq(post.id)
expect(message.data[:polls][0][:name]).to eq(poll.name) expect(message.data[:polls][0][:name]).to eq(poll.name)
end end
end end
describe "when there are votes" do describe "when there are votes" do
before do before do
expect { expect {
DiscoursePoll::Poll.vote(user, post.id, "poll", [polls["poll"]["options"][0]["id"]]) DiscoursePoll::Poll.vote(user, post.id, "poll", [polls["poll"]["options"][0]["id"]])
@ -162,11 +137,13 @@ RSpec.describe DiscoursePoll::PollsUpdater do
end end
describe "inside the edit window" do describe "inside the edit window" do
it "and deletes the votes" do it "and deletes the votes" do
message = MessageBus.track_publish("/polls/#{post.topic_id}") do message =
update(post, polls_with_some_attributes) MessageBus
end.first .track_publish("/polls/#{post.topic_id}") do
update(post, polls_with_some_attributes)
end
.first
poll = Poll.find_by(post: post) poll = Poll.find_by(post: post)
@ -181,11 +158,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(message.data[:post_id]).to eq(post.id) expect(message.data[:post_id]).to eq(post.id)
expect(message.data[:polls][0][:name]).to eq(poll.name) expect(message.data[:polls][0][:name]).to eq(poll.name)
end end
end end
describe "outside the edit window" do describe "outside the edit window" do
it "throws an error" do it "throws an error" do
edit_window = SiteSetting.poll_edit_window_mins edit_window = SiteSetting.poll_edit_window_mins
@ -204,17 +179,12 @@ RSpec.describe DiscoursePoll::PollsUpdater do
expect(post.errors[:base]).to include( expect(post.errors[:base]).to include(
I18n.t( I18n.t(
"poll.edit_window_expired.cannot_edit_default_poll_with_votes", "poll.edit_window_expired.cannot_edit_default_poll_with_votes",
minutes: edit_window minutes: edit_window,
) ),
) )
end end
end end
end end
end end
end end
end end

Some files were not shown because too many files have changed in this diff Show More