diff --git a/Gemfile.lock b/Gemfile.lock
index 53ede95..4c42077 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,11 +1,30 @@
GEM
remote: https://rubygems.org/
specs:
+ activesupport (7.1.3.2)
+ base64
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ connection_pool (>= 2.2.5)
+ drb
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ mutex_m
+ tzinfo (~> 2.0)
ast (2.4.2)
+ base64 (0.2.0)
+ bigdecimal (3.1.6)
+ concurrent-ruby (1.2.3)
+ connection_pool (2.4.1)
+ drb (2.2.1)
+ i18n (1.14.1)
+ concurrent-ruby (~> 1.0)
json (2.7.1)
language_server-protocol (3.17.0.3)
+ minitest (5.22.2)
+ mutex_m (0.2.0)
parallel (1.24.0)
- parser (3.3.0.4)
+ parser (3.3.0.5)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
@@ -13,7 +32,7 @@ GEM
rainbow (3.1.1)
regexp_parser (2.9.0)
rexml (3.2.6)
- rubocop (1.60.0)
+ rubocop (1.61.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -24,22 +43,27 @@ GEM
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.30.0)
- parser (>= 3.2.1.0)
+ rubocop-ast (1.31.1)
+ parser (>= 3.3.0.4)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
- rubocop-discourse (3.6.0)
+ rubocop-discourse (3.7.1)
+ activesupport (>= 6.1)
rubocop (>= 1.59.0)
+ rubocop-capybara (>= 2.0.0)
+ rubocop-factory_bot (>= 2.0.0)
rubocop-rspec (>= 2.25.0)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
- rubocop-rspec (2.26.1)
+ rubocop-rspec (2.27.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
PLATFORMS
diff --git a/app/controllers/answer_controller.rb b/app/controllers/answer_controller.rb
new file mode 100644
index 0000000..ced07b0
--- /dev/null
+++ b/app/controllers/answer_controller.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class DiscourseSolved::AnswerController < ::ApplicationController
+ requires_plugin DiscourseSolved::PLUGIN_NAME
+
+ def accept
+ limit_accepts
+
+ post = Post.find(params[:id].to_i)
+
+ topic = post.topic
+ topic ||= Topic.with_deleted.find(post.topic_id) if guardian.is_staff?
+
+ guardian.ensure_can_accept_answer!(topic, post)
+
+ DiscourseSolved.accept_answer!(post, current_user, topic: topic)
+
+ render json: success_json
+ end
+
+ def unaccept
+ limit_accepts
+
+ post = Post.find(params[:id].to_i)
+
+ topic = post.topic
+ topic ||= Topic.with_deleted.find(post.topic_id) if guardian.is_staff?
+
+ guardian.ensure_can_accept_answer!(topic, post)
+
+ DiscourseSolved.unaccept_answer!(post, topic: topic)
+
+ render json: success_json
+ end
+
+ def limit_accepts
+ return if current_user.staff?
+ RateLimiter.new(nil, "accept-hr-#{current_user.id}", 20, 1.hour).performed!
+ RateLimiter.new(nil, "accept-min-#{current_user.id}", 4, 30.seconds).performed!
+ end
+end
diff --git a/app/lib/before_head_close.rb b/app/lib/before_head_close.rb
new file mode 100644
index 0000000..b07b9bf
--- /dev/null
+++ b/app/lib/before_head_close.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+class DiscourseSolved::BeforeHeadClose
+ attr_reader :controller
+
+ def initialize(controller)
+ @controller = controller
+ end
+
+ def html
+ return "" if !controller.instance_of? TopicsController
+
+ topic_view = controller.instance_variable_get(:@topic_view)
+ topic = topic_view&.topic
+ return "" if !topic
+ # note, we have canonicals so we only do this for page 1 at the moment
+ # it can get confusing to have this on every page and it should make page 1
+ # a bit more prominent + cut down on pointless work
+
+ return "" if SiteSetting.solved_add_schema_markup == "never"
+
+ allowed =
+ controller.guardian.allow_accepted_answers?(topic.category_id, topic.tags.pluck(:name))
+ return "" if !allowed
+
+ first_post = topic_view.posts&.first
+ return "" if first_post&.post_number != 1
+
+ question_json = {
+ "@type" => "Question",
+ "name" => topic.title,
+ "text" => get_schema_text(first_post),
+ "upvoteCount" => first_post.like_count,
+ "answerCount" => 0,
+ "datePublished" => topic.created_at,
+ "author" => {
+ "@type" => "Person",
+ "name" => topic.user&.username,
+ "url" => topic.user&.full_url,
+ },
+ }
+
+ if accepted_answer =
+ Post.find_by(
+ id: topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD],
+ )
+ question_json["answerCount"] = 1
+ question_json[:acceptedAnswer] = {
+ "@type" => "Answer",
+ "text" => get_schema_text(accepted_answer),
+ "upvoteCount" => accepted_answer.like_count,
+ "datePublished" => accepted_answer.created_at,
+ "url" => accepted_answer.full_url,
+ "author" => {
+ "@type" => "Person",
+ "name" => accepted_answer.user&.username,
+ "url" => accepted_answer.user&.full_url,
+ },
+ }
+ else
+ return "" if SiteSetting.solved_add_schema_markup == "answered only"
+ end
+
+ [
+ '",
+ ].join("")
+ end
+
+ private
+
+ def get_schema_text(post)
+ post.excerpt(nil, keep_onebox_body: true).presence ||
+ post.excerpt(nil, keep_onebox_body: true, keep_quotes: true)
+ end
+end
diff --git a/app/lib/category_extension.rb b/app/lib/category_extension.rb
new file mode 100644
index 0000000..80dd9ae
--- /dev/null
+++ b/app/lib/category_extension.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::CategoryExtension
+ extend ActiveSupport::Concern
+
+ prepended { after_save :reset_accepted_cache, if: -> { SiteSetting.solved_enabled? } }
+
+ private
+
+ def reset_accepted_cache
+ ::DiscourseSolved::AcceptedAnswerCache.reset_accepted_answer_cache
+ end
+end
diff --git a/app/lib/post_serializer_extension.rb b/app/lib/post_serializer_extension.rb
new file mode 100644
index 0000000..f571440
--- /dev/null
+++ b/app/lib/post_serializer_extension.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::PostSerializerExtension
+ extend ActiveSupport::Concern
+
+ private
+
+ def topic
+ topic_view&.topic || object.topic
+ end
+end
diff --git a/app/lib/topic_posters_summary_extension.rb b/app/lib/topic_posters_summary_extension.rb
new file mode 100644
index 0000000..edbed74
--- /dev/null
+++ b/app/lib/topic_posters_summary_extension.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::TopicPostersSummaryExtension
+ extend ActiveSupport::Concern
+
+ def descriptions_by_id
+ if !defined?(@descriptions_by_id)
+ super(ids: old_user_ids)
+
+ if id = topic.accepted_answer_user_id
+ @descriptions_by_id[id] ||= []
+ @descriptions_by_id[id] << I18n.t(:accepted_answer)
+ end
+ end
+
+ super
+ end
+
+ def last_poster_is_topic_creator?
+ super || topic.accepted_answer_user_id == topic.last_post_user_id
+ end
+
+ def user_ids
+ if id = topic.accepted_answer_user_id
+ super.insert(1, id)
+ else
+ super
+ end
+ end
+end
diff --git a/app/lib/topic_view_serializer_extension.rb b/app/lib/topic_view_serializer_extension.rb
new file mode 100644
index 0000000..05008cc
--- /dev/null
+++ b/app/lib/topic_view_serializer_extension.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::TopicViewSerializerExtension
+ extend ActiveSupport::Concern
+
+ prepended { attributes :accepted_answer }
+
+ def include_accepted_answer?
+ SiteSetting.solved_enabled? && accepted_answer_post_id
+ end
+
+ def accepted_answer
+ if info = accepted_answer_post_info
+ { post_number: info[0], username: info[1], excerpt: info[2], name: info[3] }
+ end
+ end
+
+ private
+
+ def accepted_answer_post_info
+ post_info =
+ if post = object.posts.find { |p| p.post_number == accepted_answer_post_id }
+ [post.post_number, post.user.username, post.cooked, post.user.name]
+ else
+ Post
+ .where(id: accepted_answer_post_id, topic_id: object.topic.id)
+ .joins(:user)
+ .pluck("post_number", "username", "cooked", "name")
+ .first
+ end
+
+ if post_info
+ post_info[2] = if SiteSetting.solved_quote_length > 0
+ PrettyText.excerpt(post_info[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
+ else
+ nil
+ end
+
+ post_info[3] = nil if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
+
+ post_info
+ end
+ end
+
+ def accepted_answer_post_id
+ id = object.topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD]
+ # a bit messy but race conditions can give us an array here, avoid
+ begin
+ id && id.to_i
+ rescue StandardError
+ nil
+ end
+ end
+end
diff --git a/app/lib/user_summary_extension.rb b/app/lib/user_summary_extension.rb
new file mode 100644
index 0000000..5194c03
--- /dev/null
+++ b/app/lib/user_summary_extension.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::UserSummaryExtension
+ extend ActiveSupport::Concern
+
+ def solved_count
+ UserAction.where(user: @user).where(action_type: UserAction::SOLVED).count
+ end
+end
diff --git a/app/lib/web_hook_extension.rb b/app/lib/web_hook_extension.rb
new file mode 100644
index 0000000..4bb84e0
--- /dev/null
+++ b/app/lib/web_hook_extension.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module DiscourseSolved::WebHookExtension
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def enqueue_solved_hooks(event, post, payload = nil)
+ if active_web_hooks(event).exists? && post.present?
+ payload ||= WebHook.generate_payload(:post, post)
+
+ WebHook.enqueue_hooks(
+ :solved,
+ event,
+ id: post.id,
+ category_id: post.topic&.category_id,
+ tag_ids: post.topic&.tags&.pluck(:id),
+ payload: payload,
+ )
+ end
+ end
+ end
+end
diff --git a/plugin.rb b/plugin.rb
index 87fa045..31dad15 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -15,74 +15,39 @@
register_svg_icon "far fa-square"
end
-PLUGIN_NAME = "discourse_solved"
-
register_asset "stylesheets/solutions.scss"
register_asset "stylesheets/mobile/solutions.scss", :mobile
after_initialize do
- SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s
-
- %w[
- ../app/lib/first_accepted_post_solution_validator.rb
- ../app/lib/accepted_answer_cache.rb
- ../app/lib/guardian_extensions.rb
- ../app/serializers/concerns/topic_answer_mixin.rb
- ].each { |path| load File.expand_path(path, __FILE__) }
-
- skip_db = defined?(GlobalSetting.skip_db?) && GlobalSetting.skip_db?
-
- reloadable_patch { |plugin| Guardian.prepend(DiscourseSolved::GuardianExtensions) }
-
- # we got to do a one time upgrade
- if !skip_db && defined?(UserAction::SOLVED)
- unless Discourse.redis.get("solved_already_upgraded")
- unless UserAction.where(action_type: UserAction::SOLVED).exists?
- Rails.logger.info("Upgrading storage for solved")
- sql = < "Question",
- "name" => topic.title,
- "text" => get_schema_text(first_post),
- "upvoteCount" => first_post.like_count,
- "answerCount" => 0,
- "datePublished" => topic.created_at,
- "author" => {
- "@type" => "Person",
- "name" => topic.user&.username,
- "url" => topic.user&.full_url,
- },
- }
-
- if accepted_answer =
- Post.find_by(
- id: topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD],
- )
- question_json["answerCount"] = 1
- question_json[:acceptedAnswer] = {
- "@type" => "Answer",
- "text" => get_schema_text(accepted_answer),
- "upvoteCount" => accepted_answer.like_count,
- "datePublished" => accepted_answer.created_at,
- "url" => accepted_answer.full_url,
- "author" => {
- "@type" => "Person",
- "name" => accepted_answer.user&.username,
- "url" => accepted_answer.user&.full_url,
- },
- }
- else
- return "" if SiteSetting.solved_add_schema_markup == "answered only"
- end
-
- [
- '",
- ].join("")
- end
-
register_html_builder("server:before-head-close-crawler") do |controller|
- before_head_close_meta(controller)
+ DiscourseSolved::BeforeHeadClose.new(controller).html
end
register_html_builder("server:before-head-close") do |controller|
- before_head_close_meta(controller)
+ DiscourseSolved::BeforeHeadClose.new(controller).html
end
if Report.respond_to?(:add_report)
@@ -414,129 +328,22 @@ def before_head_close_meta(controller)
end
end
- if defined?(UserAction::SOLVED)
- require_dependency "user_summary"
- class ::UserSummary
- def solved_count
- UserAction.where(user: @user).where(action_type: UserAction::SOLVED).count
- end
- end
-
- require_dependency "user_summary_serializer"
- class ::UserSummarySerializer
- attributes :solved_count
-
- def solved_count
- object.solved_count
- end
- end
+ if defined?(::UserAction::SOLVED)
+ add_to_serializer(:user_summary, :solved_count) { object.solved_count }
end
-
- class ::WebHook
- def self.enqueue_solved_hooks(event, post, payload = nil)
- if active_web_hooks(event).exists? && post.present?
- payload ||= WebHook.generate_payload(:post, post)
-
- WebHook.enqueue_hooks(
- :solved,
- event,
- id: post.id,
- category_id: post.topic&.category_id,
- tag_ids: post.topic&.tags&.pluck(:id),
- payload: payload,
- )
- end
- end
+ add_to_serializer(:post, :can_accept_answer) do
+ scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
end
-
- require_dependency "topic_view_serializer"
- class ::TopicViewSerializer
- attributes :accepted_answer
-
- def include_accepted_answer?
- accepted_answer_post_id
- end
-
- def accepted_answer
- if info = accepted_answer_post_info
- { post_number: info[0], username: info[1], excerpt: info[2], name: info[3] }
- end
- end
-
- def accepted_answer_post_info
- post_info =
- if post = object.posts.find { |p| p.post_number == accepted_answer_post_id }
- [post.post_number, post.user.username, post.cooked, post.user.name]
- else
- Post
- .where(id: accepted_answer_post_id, topic_id: object.topic.id)
- .joins(:user)
- .pluck("post_number", "username", "cooked", "name")
- .first
- end
-
- if post_info
- post_info[2] = if SiteSetting.solved_quote_length > 0
- PrettyText.excerpt(post_info[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
- else
- nil
- end
-
- post_info[3] = nil if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
-
- post_info
- end
- end
-
- def accepted_answer_post_id
- id = object.topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD]
- # a bit messy but race conditions can give us an array here, avoid
- begin
- id && id.to_i
- rescue StandardError
- nil
- end
- end
+ add_to_serializer(:post, :can_unaccept_answer) do
+ scope.can_accept_answer?(topic, object) && accepted_answer
end
-
- class ::Category
- after_save :reset_accepted_cache
-
- protected
-
- def reset_accepted_cache
- ::DiscourseSolved::AcceptedAnswerCache.reset_accepted_answer_cache
- end
+ add_to_serializer(:post, :accepted_answer) do
+ post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
end
-
- require_dependency "post_serializer"
-
- class ::PostSerializer
- attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer, :topic_accepted_answer
-
- def can_accept_answer
- scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
- end
-
- def can_unaccept_answer
- scope.can_accept_answer?(topic, object) && accepted_answer
- end
-
- def accepted_answer
- post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
- end
-
- def topic_accepted_answer
- topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present?
- end
-
- def topic
- topic_view&.topic || object.topic
- end
+ add_to_serializer(:post, :topic_accepted_answer) do
+ topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present?
end
- require_dependency "search"
-
#TODO Remove when plugin is 1.0
if Search.respond_to? :advanced_filter
Search.advanced_filter(/status:solved/) do |posts|
@@ -581,8 +388,6 @@ def topic
end
if Discourse.has_needed_version?(Discourse::VERSION::STRING, "1.8.0.beta6")
- require_dependency "topic_query"
-
TopicQuery.add_custom_filter(:solved) do |results, topic_query|
if topic_query.options[:solved] == "yes"
results =
@@ -609,31 +414,6 @@ def topic
end
end
- require_dependency "topic_list_item_serializer"
- require_dependency "search_topic_list_item_serializer"
- require_dependency "suggested_topic_serializer"
- require_dependency "user_summary_serializer"
-
- class ::TopicListItemSerializer
- include TopicAnswerMixin
- end
-
- class ::SearchTopicListItemSerializer
- include TopicAnswerMixin
- end
-
- class ::SuggestedTopicSerializer
- include TopicAnswerMixin
- end
-
- class ::UserSummarySerializer::TopicSerializer
- include TopicAnswerMixin
- end
-
- class ::ListableTopicSerializer
- include TopicAnswerMixin
- end
-
if TopicList.respond_to? :preloaded_custom_fields
TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
end
@@ -756,10 +536,6 @@ class ::ListableTopicSerializer
end
if respond_to?(:register_topic_list_preload_user_ids)
- class ::Topic
- attr_accessor :accepted_answer_user_id
- end
-
register_topic_list_preload_user_ids do |topics, user_ids, topic_list|
answer_post_ids =
TopicCustomField
@@ -770,39 +546,6 @@ class ::Topic
topics.each { |topic| topic.accepted_answer_user_id = answer_user_ids[topic.id] }
user_ids.concat(answer_user_ids.values)
end
-
- module AddSolvedToTopicPostersSummary
- def descriptions_by_id
- if !defined?(@descriptions_by_id)
- super(ids: old_user_ids)
-
- if id = topic.accepted_answer_user_id
- @descriptions_by_id[id] ||= []
- @descriptions_by_id[id] << I18n.t(:accepted_answer)
- end
- end
-
- super
- end
-
- def last_poster_is_topic_creator?
- super || topic.accepted_answer_user_id == topic.last_post_user_id
- end
-
- def user_ids
- if id = topic.accepted_answer_user_id
- super.insert(1, id)
- else
- super
- end
- end
- end
-
- TopicPostersSummary.class_eval do
- alias old_user_ids user_ids
-
- prepend AddSolvedToTopicPostersSummary
- end
end
if defined?(DiscourseAutomation)
@@ -830,29 +573,6 @@ def user_ids
end
end
- TRUST_LEVELS = [
- {
- id: 1,
- name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
- },
- {
- id: 2,
- name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2",
- },
- {
- id: 3,
- name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3",
- },
- {
- id: 4,
- name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4",
- },
- {
- id: "any",
- name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.any",
- },
- ]
-
add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
@@ -861,7 +581,33 @@ def user_ids
field :maximum_trust_level,
component: :choices,
extra: {
- content: TRUST_LEVELS,
+ content: [
+ {
+ id: 1,
+ name:
+ "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
+ },
+ {
+ id: 2,
+ name:
+ "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2",
+ },
+ {
+ id: 3,
+ name:
+ "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3",
+ },
+ {
+ id: 4,
+ name:
+ "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4",
+ },
+ {
+ id: "any",
+ name:
+ "discourse_automation.triggerables.first_accepted_solution.max_trust_level.any",
+ },
+ ],
},
required: true
end
diff --git a/spec/components/composer_messages_finder_spec.rb b/spec/components/composer_messages_finder_spec.rb
index 8cd15ec..944fa8b 100644
--- a/spec/components/composer_messages_finder_spec.rb
+++ b/spec/components/composer_messages_finder_spec.rb
@@ -6,8 +6,8 @@
describe ComposerMessagesFinder do
describe ".check_topic_is_solved" do
- fab!(:user) { Fabricate(:user) }
- fab!(:topic) { Fabricate(:topic) }
+ fab!(:user)
+ fab!(:topic)
fab!(:post) { Fabricate(:post, topic: topic, user: Fabricate(:user)) }
before { SiteSetting.disable_solved_education_message = false }
diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb
index 2d4be63..d35cb82 100644
--- a/spec/components/post_revisor_spec.rb
+++ b/spec/components/post_revisor_spec.rb
@@ -41,7 +41,7 @@
fab!(:tag1) { Fabricate(:tag) }
fab!(:tag2) { Fabricate(:tag) }
- fab!(:topic) { Fabricate(:topic) }
+ fab!(:topic)
let(:post) { Fabricate(:post, topic: topic) }
it "sets the refresh option after adding an allowed tag" do
diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb
index 3fd6c42..93dc089 100644
--- a/spec/integration/solved_spec.rb
+++ b/spec/integration/solved_spec.rb
@@ -340,7 +340,7 @@
end
context "with group moderators" do
- fab!(:group_user) { Fabricate(:group_user) }
+ fab!(:group_user)
let(:user_gm) { group_user.user }
let(:group) { group_user.group }