thredded 0.14.4 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/assets/javascripts/thredded/components/preview_area.es6 +3 -0
  4. data/app/assets/javascripts/thredded/components/spoilers.es6 +37 -0
  5. data/app/assets/stylesheets/thredded/_email.scss +25 -0
  6. data/app/assets/stylesheets/thredded/_thredded.scss +1 -0
  7. data/app/assets/stylesheets/thredded/base/_typography.scss +1 -1
  8. data/app/assets/stylesheets/thredded/base/_variables.scss +2 -3
  9. data/app/assets/stylesheets/thredded/components/_post.scss +0 -11
  10. data/app/assets/stylesheets/thredded/components/_spoiler.scss +41 -0
  11. data/app/commands/thredded/mark_all_read.rb +1 -7
  12. data/app/controllers/concerns/thredded/new_post_params.rb +1 -1
  13. data/app/controllers/concerns/thredded/new_private_post_params.rb +1 -1
  14. data/app/controllers/concerns/thredded/new_private_topic_params.rb +1 -2
  15. data/app/controllers/concerns/thredded/new_topic_params.rb +0 -1
  16. data/app/controllers/thredded/application_controller.rb +10 -0
  17. data/app/controllers/thredded/posts_controller.rb +1 -2
  18. data/app/controllers/thredded/private_posts_controller.rb +1 -2
  19. data/app/controllers/thredded/private_topics_controller.rb +10 -12
  20. data/app/controllers/thredded/topics_controller.rb +14 -17
  21. data/app/forms/thredded/edit_topic_form.rb +2 -1
  22. data/app/forms/thredded/post_form.rb +3 -1
  23. data/app/forms/thredded/private_post_form.rb +3 -1
  24. data/app/forms/thredded/private_topic_form.rb +1 -0
  25. data/app/jobs/thredded/activity_updater_job.rb +18 -8
  26. data/app/jobs/thredded/auto_follow_and_notify_job.rb +2 -1
  27. data/app/models/concerns/thredded/post_common.rb +3 -4
  28. data/app/models/concerns/thredded/topic_common.rb +7 -0
  29. data/app/models/concerns/thredded/user_topic_read_state_common.rb +62 -5
  30. data/app/models/thredded/null_user_topic_read_state.rb +8 -0
  31. data/app/policies/thredded/private_post_policy.rb +16 -0
  32. data/app/view_hooks/thredded/all_view_hooks.rb +3 -0
  33. data/app/view_models/thredded/base_topic_view.rb +5 -1
  34. data/app/view_models/thredded/post_view.rb +13 -1
  35. data/app/view_models/thredded/posts_page_view.rb +1 -1
  36. data/app/view_models/thredded/topic_posts_page_view.rb +13 -1
  37. data/app/views/thredded/posts/_post.html.erb +1 -0
  38. data/app/views/thredded/posts/edit.html.erb +1 -0
  39. data/app/views/thredded/posts_common/_before_first_unread_post.html.erb +7 -0
  40. data/app/views/thredded/posts_common/form/_after_content.html.erb +8 -0
  41. data/app/views/thredded/posts_common/form/_before_content.html.erb +8 -0
  42. data/app/views/thredded/private_posts/_private_post.html.erb +2 -1
  43. data/app/views/thredded/private_posts/edit.html.erb +1 -0
  44. data/app/views/thredded/private_topics/_form.html.erb +1 -0
  45. data/app/views/thredded/private_topics/edit.html.erb +2 -1
  46. data/app/views/thredded/shared/_field_errors.html.erb +3 -0
  47. data/app/views/thredded/shared/_nav.html.erb +1 -1
  48. data/app/views/thredded/shared/_page.html.erb +1 -1
  49. data/app/views/thredded/topics/_form.html.erb +1 -0
  50. data/app/views/thredded/topics/edit.html.erb +2 -1
  51. data/config/locales/de.yml +2 -0
  52. data/config/locales/en.yml +2 -0
  53. data/config/locales/es.yml +2 -0
  54. data/config/locales/fr.yml +2 -0
  55. data/config/locales/it.yml +2 -0
  56. data/config/locales/pl.yml +2 -0
  57. data/config/locales/pt-BR.yml +2 -0
  58. data/config/locales/ru.yml +2 -0
  59. data/config/locales/zh-CN.yml +2 -0
  60. data/db/migrate/20160329231848_create_thredded.rb +37 -23
  61. data/db/upgrade_migrations/{20170811090735_upgrade_thredded_v0_13_to_v_014.rb → 20170811090735_upgrade_thredded_v0_13_to_v0_14.rb} +0 -0
  62. data/db/upgrade_migrations/20180110200009_upgrade_thredded_v0_14_to_v0_15.rb +91 -0
  63. data/lib/generators/thredded/install/templates/initializer.rb +16 -7
  64. data/lib/thredded.rb +143 -125
  65. data/lib/thredded/arel_compat.rb +57 -0
  66. data/lib/thredded/base_migration.rb +10 -0
  67. data/lib/thredded/collection_to_strings_with_cache_renderer.rb +35 -9
  68. data/lib/thredded/content_formatter.rb +27 -18
  69. data/lib/thredded/database_seeder.rb +218 -64
  70. data/lib/thredded/email_transformer.rb +5 -2
  71. data/lib/thredded/email_transformer/spoiler.rb +25 -0
  72. data/lib/thredded/formatting_demo_content.rb +12 -0
  73. data/lib/thredded/html_pipeline/onebox_filter.rb +3 -38
  74. data/lib/thredded/html_pipeline/spoiler_tag_filter.rb +128 -0
  75. data/lib/thredded/html_pipeline/utils.rb +47 -0
  76. data/lib/thredded/rails_lt_5_2_arel_case_node.rb +119 -0
  77. data/lib/thredded/version.rb +1 -1
  78. metadata +17 -21
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR >= 2 || Rails::VERSION::MAJOR > 5
4
+ require 'thredded/rails_lt_5_2_arel_case_node.rb'
5
+ end
6
+
7
+ module Thredded
8
+ module ArelCompat
9
+ module_function
10
+
11
+ # @param [#connection] engine
12
+ # @param [Arel::Nodes::Node] a integer node
13
+ # @param [Arel::Nodes::Node] b integer node
14
+ # @return [Arel::Nodes::Node] a / b
15
+ def integer_division(engine, a, b)
16
+ if engine.connection.adapter_name =~ /mysql|mariadb/i
17
+ Arel::Nodes::InfixOperation.new('DIV', a, b)
18
+ else
19
+ Arel::Nodes::Division.new(a, b)
20
+ end
21
+ end
22
+
23
+ if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR >= 2 || Rails::VERSION::MAJOR > 5
24
+ # @param [ActiveRecord::Relation] relation
25
+ # @return [Arel::Nodes::Node]
26
+ def relation_to_arel(relation)
27
+ relation.arel
28
+ end
29
+ else
30
+ def relation_to_arel(relation)
31
+ Arel.sql("(#{relation.to_sql})")
32
+ end
33
+ end
34
+
35
+ if Rails::VERSION::MAJOR >= 5
36
+ # @param [Arel::Nodes::Node] table
37
+ # @return [Arel::SelectManager]
38
+ def new_arel_select_manager(table)
39
+ Arel::SelectManager.new(table)
40
+ end
41
+ else
42
+ def new_arel_select_manager(table)
43
+ Arel::SelectManager.new(ActiveRecord::Base, table)
44
+ end
45
+ end
46
+
47
+ if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR >= 2 || Rails::VERSION::MAJOR > 5
48
+ def true_value(_engine)
49
+ true
50
+ end
51
+ else
52
+ def true_value(engine)
53
+ engine.connection.adapter_name =~ /sqlite|mysql|mariadb/i ? 1 : true
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Thredded
4
4
  class BaseMigration < (Thredded.rails_gte_51? ? ActiveRecord::Migration[5.1] : ActiveRecord::Migration)
5
+ protected
6
+
5
7
  def user_id_type
6
8
  Thredded.user_class.columns.find { |c| c.name == Thredded.user_class.primary_key }.sql_type
7
9
  end
@@ -10,5 +12,13 @@ module Thredded
10
12
  column_name = column_name.to_s
11
13
  columns(table).find { |c| c.name == column_name }.sql_type
12
14
  end
15
+
16
+ # @return [Integer, nil] the maximum number of codepoints that can be indexed for a primary key or index.
17
+ def max_key_length
18
+ return nil unless connection.adapter_name =~ /mysql|maria/i
19
+ # Conservatively assume that innodb_large_prefix is **disabled**.
20
+ # If it were enabled, the maximum key length would instead be 768 utf8mb4 characters.
21
+ 191
22
+ end
13
23
  end
14
24
  end
@@ -1,15 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'action_view/renderer/abstract_renderer'
4
+
4
5
  module Thredded
5
6
  class CollectionToStringsWithCacheRenderer < ActionView::AbstractRenderer
7
+ class << self
8
+ # The default number of threads to use for rendering.
9
+ attr_accessor :render_threads
10
+ end
11
+
12
+ self.render_threads = 50
13
+
6
14
  # @param view_context
7
- # @param collection [Array<T>]
8
- # @param partial [String]
9
- # @param expires_in [ActiveSupport::Duration]
15
+ # @param [Array<T>] collection
16
+ # @param [String] partial
17
+ # @param [ActiveSupport::Duration] expires_in
18
+ # @param [Integer] render_threads the number of threads to use for rendering. This is useful even on MRI ruby
19
+ # for IO-bound operations.
20
+ # @param [Hash] locals
10
21
  # @return Array<[T, String]>
11
22
  def render_collection_to_strings_with_cache( # rubocop:disable Metrics/ParameterLists
12
- view_context, collection:, partial:, expires_in:, locals: {}, **opts
23
+ view_context, collection:, partial:, expires_in:, render_threads: self.class.render_threads, locals: {}, **opts
13
24
  )
14
25
  template = @lookup_context.find_template(partial, [], true, locals, {})
15
26
  collection = collection.to_a
@@ -28,7 +39,9 @@ module Thredded
28
39
 
29
40
  collection_to_render = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
30
41
  rendered_partials = render_partials(
31
- view_context, collection: collection_to_render, partial: partial, locals: locals, **opts
42
+ view_context,
43
+ collection: collection_to_render, render_threads: render_threads,
44
+ partial: partial, locals: locals, **opts
32
45
  ).each
33
46
 
34
47
  keyed_collection.map do |cache_key, item|
@@ -52,12 +65,25 @@ module Thredded
52
65
  end
53
66
 
54
67
  # @return [Array<String>]
55
- def render_partials(view_context, collection:, **opts)
68
+ def render_partials(view_context, collection:, render_threads:, **opts)
56
69
  return [] if collection.empty?
57
- partial_renderer = ActionView::PartialRenderer.new(@lookup_context)
58
- collection.map do |item|
59
- partial_renderer.render(view_context, opts.merge(object: item), nil)
70
+ num_threads = [render_threads, collection.size].min
71
+ if num_threads == 1
72
+ render_partials_serial(view_context, collection, opts)
73
+ else
74
+ collection.each_slice(collection.size / num_threads).map do |slice|
75
+ Thread.start { render_partials_serial(view_context.dup, slice, opts) }
76
+ end.flat_map(&:value)
60
77
  end
61
78
  end
79
+
80
+ # @param [Array<Object>] collection
81
+ # @param [Hash] opts
82
+ # @param view_context
83
+ # @return [Array<String>]
84
+ def render_partials_serial(view_context, collection, opts)
85
+ partial_renderer = ActionView::PartialRenderer.new(@lookup_context)
86
+ collection.map { |object| partial_renderer.render(view_context, opts.merge(object: object), nil) }
87
+ end
62
88
  end
63
89
  end
@@ -3,8 +3,30 @@
3
3
  module Thredded
4
4
  # Generates HTML from content source.
5
5
  class ContentFormatter
6
- # Sanitization whitelist options.
7
- mattr_accessor :whitelist
6
+ class << self
7
+ # Sanitization whitelist options.
8
+ attr_accessor :whitelist
9
+
10
+ # Filters that run before processing the markup.
11
+ # input: markup, output: markup.
12
+ attr_accessor :before_markup_filters
13
+
14
+ # Markup filters, such as BBCode, Markdown, Autolink, etc.
15
+ # input: markup, output: html.
16
+ attr_accessor :markup_filters
17
+
18
+ # Filters that run after processing the markup.
19
+ # input: html, output: html.
20
+ attr_accessor :after_markup_filters
21
+
22
+ # Filters that sanitize the resulting HTML.
23
+ # input: html, output: sanitized html.
24
+ attr_accessor :sanitization_filters
25
+
26
+ # Filters that run after sanitization
27
+ # input: sanitized html, output: html
28
+ attr_accessor :after_sanitization_filters
29
+ end
8
30
 
9
31
  self.whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST.deep_merge(
10
32
  elements: HTML::Pipeline::SanitizationFilter::WHITELIST[:elements] + %w[abbr iframe span figure figcaption],
@@ -25,44 +47,31 @@ module Thredded
25
47
  'span' => %w[class],
26
48
  'div' => %w[class],
27
49
  :all => HTML::Pipeline::SanitizationFilter::WHITELIST[:attributes][:all] +
28
- %w[aria-label aria-labelledby aria-hidden],
50
+ %w[aria-expanded aria-label aria-labelledby aria-live aria-hidden aria-pressed role],
29
51
  }
30
52
  )
31
53
 
32
- # Filters that run before processing the markup.
33
- # input: markup, output: markup.
34
- mattr_accessor :before_markup_filters
35
54
  self.before_markup_filters = [
55
+ Thredded::HtmlPipeline::SpoilerTagFilter::BeforeMarkup
36
56
  ]
37
57
 
38
- # Markup filters, such as BBCode, Markdown, Autolink, etc.
39
- # input: markup, output: html.
40
- mattr_accessor :markup_filters
41
58
  self.markup_filters = [
42
59
  Thredded::HtmlPipeline::KramdownFilter,
43
60
  ]
44
61
 
45
- # Filters that run after processing the markup.
46
- # input: html, output: html.
47
- mattr_accessor :after_markup_filters
48
62
  self.after_markup_filters = [
49
63
  # AutolinkFilter is required because Kramdown does not autolink by default.
50
64
  # https://github.com/gettalong/kramdown/issues/306
51
65
  Thredded::HtmlPipeline::AutolinkFilter,
52
66
  HTML::Pipeline::EmojiFilter,
53
67
  Thredded::HtmlPipeline::AtMentionFilter,
68
+ Thredded::HtmlPipeline::SpoilerTagFilter::AfterMarkup,
54
69
  ]
55
70
 
56
- # Filters that sanitize the resulting HTML.
57
- # input: html, output: sanitized html.
58
- mattr_accessor :sanitization_filters
59
71
  self.sanitization_filters = [
60
72
  HTML::Pipeline::SanitizationFilter,
61
73
  ]
62
74
 
63
- # Filters that run after sanitization
64
- # input: sanitized html, output: html
65
- mattr_accessor :after_sanitization_filters
66
75
  self.after_sanitization_filters = [
67
76
  Thredded::HtmlPipeline::OneboxFilter,
68
77
  Thredded::HtmlPipeline::WrapIframesFilter,
@@ -12,40 +12,128 @@ rescue NameError
12
12
  end
13
13
  # rubocop:enable HandleExceptions
14
14
  module Thredded
15
- class DatabaseSeeder
15
+ class DatabaseSeeder # rubocop:disable Metrics/ClassLength
16
+ module LogTime
17
+ def self.included(base)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ def log_time
22
+ start = Time.now.to_f
23
+ result = yield
24
+ print_time_diff start
25
+ result
26
+ end
27
+
28
+ def print_time_diff(from, to = Time.now.to_f)
29
+ log " [#{format('%.2f', to - from)}s]\n"
30
+ end
31
+
32
+ module ClassMethods
33
+ def log_method_time(method_name)
34
+ prepend(Module.new do
35
+ define_method method_name do |*args, **kwargs|
36
+ log_time { super(*args, **kwargs) }
37
+ end
38
+ end)
39
+ method_name
40
+ end
41
+ end
42
+ end
43
+
44
+ include LogTime
45
+
16
46
  SKIP_CALLBACKS = [
17
- [Thredded::Post, :commit, :after, :auto_follow_and_notify],
18
- [Thredded::PrivatePost, :commit, :after, :notify_users],
47
+ [Thredded::Post, :commit, :after, :update_parent_last_user_and_time_from_last_post, on: %i[create destroy]],
48
+ [Thredded::Post, :commit, :after, :update_parent_last_user_and_time_from_last_post_if_moderation_state_changed,
49
+ on: :update],
50
+ [Thredded::Post, :commit, :after, :auto_follow_and_notify, on: %i[create update]],
51
+ [Thredded::PrivatePost, :commit, :after, :update_parent_last_user_and_timestamp, on: %i[create destroy]],
52
+ [Thredded::PrivatePost, :commit, :after, :notify_users, on: [:create]],
53
+ ].freeze
54
+ DISABLE_COUNTER_CACHE = [Thredded::Post, Thredded::PrivatePost].freeze
55
+ WRITEABLE_READONLY_ATTRIBUTES = [
56
+ [Thredded::Topic, 'posts_count'],
57
+ [Thredded::PrivateTopic, 'posts_count'],
19
58
  ].freeze
20
59
 
21
- def self.run(users: 200, topics: 55, posts: (1..60))
22
- STDERR.puts 'Seeding the database...'
60
+ # Applies global tweaks required to run seeder methods for the given block.
61
+ def self.with_seeder_tweaks
23
62
  # Disable callbacks to avoid creating notifications and performing unnecessary updates
24
- SKIP_CALLBACKS.each { |(klass, *args)| klass.skip_callback(*args) }
25
- s = new
26
- Messageboard.transaction do
27
- s.seed(users: users, topics: topics, posts: posts)
28
- s.log 'Running after_commit callbacks'
63
+ DISABLE_COUNTER_CACHE.each do |klass|
64
+ klass.send(:alias_method, :original_each_counter_cached_associations, :each_counter_cached_associations)
65
+ klass.send(:define_method, :each_counter_cached_associations) {}
29
66
  end
67
+ SKIP_CALLBACKS.each { |(klass, *args)| delete_callbacks(klass, *args) }
68
+ WRITEABLE_READONLY_ATTRIBUTES.each { |(klass, attr)| klass.readonly_attributes.delete(attr) }
69
+ logger_was = ActiveRecord::Base.logger
70
+ ActiveRecord::Base.logger = nil
71
+ yield
30
72
  ensure
31
- # Re-enable callbacks
32
- SKIP_CALLBACKS.each { |(klass, *args)| klass.set_callback(*args) }
73
+ # Re-enable callbacks and counter cache
74
+ DISABLE_COUNTER_CACHE.each do |klass|
75
+ klass.send(:remove_method, :each_counter_cached_associations)
76
+ klass.send(:alias_method, :each_counter_cached_associations, :original_each_counter_cached_associations)
77
+ klass.send(:remove_method, :original_each_counter_cached_associations)
78
+ end
79
+ SKIP_CALLBACKS.each do |(klass, *args)|
80
+ args = args.dup
81
+ klass.send(:set_options_for_callbacks!, args)
82
+ klass.set_callback(*args)
83
+ end
84
+ WRITEABLE_READONLY_ATTRIBUTES.each { |(klass, attr)| klass.readonly_attributes << attr }
85
+ ActiveRecord::Base.logger = logger_was
86
+ end
87
+
88
+ def self.delete_callbacks(klass, name, *filter_list, &block)
89
+ type, filters, _options = klass.normalize_callback_params(filter_list, block)
90
+ klass.__update_callbacks(name) do |target, chain|
91
+ filters.each do |filter|
92
+ chain.delete(chain.find { |c| c.matches?(type, filter) })
93
+ end
94
+ target.send :set_callbacks, name, chain
95
+ end
96
+ end
97
+
98
+ def self.run(**kwargs)
99
+ new.run(**kwargs)
33
100
  end
34
101
 
35
- def seed(users: 200, topics: 55, posts: (1..60))
36
- users(count: users)
37
- first_messageboard
38
- topics(count: topics)
39
- private_topics(count: topics)
40
- posts(count: posts)
41
- private_posts(count: posts)
42
- create_additional_messageboards
43
- follow_some_topics
44
- read_some_topics
102
+ def run(users: 200, topics: 70, posts: (1..70))
103
+ log "Seeding the database...\n"
104
+ self.class.with_seeder_tweaks do
105
+ t_txn_0 = nil
106
+ Messageboard.transaction do
107
+ initialize_fake_post_contents(topics: topics, posts: posts)
108
+ users(count: users)
109
+ first_messageboard
110
+ topics(count: topics)
111
+ private_topics(count: topics)
112
+ posts(count: posts)
113
+ private_posts(count: posts)
114
+ create_additional_messageboards
115
+ follow_some_topics
116
+ read_some_topics(count: (topics / 4..topics / 3))
117
+ update_messageboards_data
118
+ t_txn_0 = Time.now.to_f
119
+ log 'Committing transaction and running after_commit callbacks'
120
+ end
121
+ print_time_diff t_txn_0
122
+ end
45
123
  end
46
124
 
47
125
  def log(message)
48
- STDERR.puts "- #{message}"
126
+ STDERR.write "- #{message}"
127
+ STDERR.flush
128
+ end
129
+
130
+ log_method_time def initialize_fake_post_contents(topics:, posts:)
131
+ log 'Initializing fake post contents...'
132
+ @fake_post_contents = Array.new([topics * (posts.min + posts.max) / 2, 1000].min) { FakeContent.post_content }
133
+ end
134
+
135
+ def fake_post_contents
136
+ @fake_post_contents ? @fake_post_contents.sample : FakeContent.post_content
49
137
  end
50
138
 
51
139
  def first_user
@@ -56,6 +144,12 @@ module Thredded
56
144
  @users ||= Users.new(self).find_or_create(count: count)
57
145
  end
58
146
 
147
+ def user_details
148
+ @user_details ||= users.each_with_object({}) do |user, hash|
149
+ hash[user] = user.thredded_user_detail
150
+ end
151
+ end
152
+
59
153
  def first_messageboard
60
154
  @first_messageboard ||= FirstMessageboard.new(self).find_or_create
61
155
  end
@@ -68,10 +162,19 @@ module Thredded
68
162
  'Need help using the forum? Want to report a bug or make a suggestion? This is the place.', meta_group_id],
69
163
  ['Praise', 'Want to tell us how great we are? This is the place.', meta_group_id]
70
164
  ]
71
- log "Creating #{additional_messageboards.length} additional messageboards..."
165
+ log "Creating #{additional_messageboards.length} additional messageboards...\n"
72
166
  additional_messageboards.each do |(name, description, group_id)|
73
167
  messageboard = Messageboard.create!(name: name, description: description, messageboard_group_id: group_id)
74
- FactoryBot.create_list(:topic, 1 + rand(3), messageboard: messageboard, with_posts: 1)
168
+ topics = Topics.new(self).create(count: 1 + rand(3), messageboard: messageboard)
169
+ Posts.new(self).create(count: (1..2), topics: topics)
170
+ end
171
+ end
172
+
173
+ log_method_time def update_messageboards_data(**) # `**` for Ruby < 2.5, see https://bugs.ruby-lang.org/issues/10856
174
+ log 'Updating messageboards data...'
175
+ Messageboard.all.each do |messageboard|
176
+ messageboard.update_last_topic!
177
+ Thredded::Messageboard.reset_counters(messageboard.id, :posts)
75
178
  end
76
179
  end
77
180
 
@@ -91,10 +194,12 @@ module Thredded
91
194
  @private_posts ||= PrivatePosts.new(self).find_or_create(count: count)
92
195
  end
93
196
 
94
- def follow_some_topics(count: (5..10), count_users: (1..5))
197
+ log_method_time def follow_some_topics(count: (5..10), count_users: (1..5))
95
198
  log 'Following some topics...'
96
199
  posts.each do |post|
97
- Thredded::UserTopicFollow.create_unless_exists(post.user_id, post.postable_id, :posted) if post.user_id
200
+ next unless post.user_id
201
+ Thredded::UserTopicFollow.create_with(reason: :posted)
202
+ .find_or_create_by(user_id: post.user_id, topic_id: post.postable_id)
98
203
  end
99
204
  follow_some_topics_by_user(first_user, count: count)
100
205
  users.sample(count_users.min + rand(count_users.max - count_users.min + 2)).each do |user|
@@ -104,11 +209,11 @@ module Thredded
104
209
 
105
210
  def follow_some_topics_by_user(user, count: (1..10))
106
211
  topics.sample(count.min + rand(count.max - count.min + 2)).each do |topic|
107
- Thredded::UserTopicFollow.create_unless_exists(user.id, topic.id)
212
+ Thredded::UserTopicFollow.create_with(reason: :manual).find_or_create_by(user_id: user.id, topic_id: topic.id)
108
213
  end
109
214
  end
110
215
 
111
- def read_some_topics(count: (5..10), count_users: (1..5))
216
+ log_method_time def read_some_topics(count: (5..10), count_users: (1..5))
112
217
  log 'Reading some topics...'
113
218
  topics.each do |topic|
114
219
  read_topic(topic, topic.last_user_id) if topic.last_user_id
@@ -126,11 +231,18 @@ module Thredded
126
231
  end
127
232
 
128
233
  def read_topic(topic, user_id)
129
- Thredded::UserTopicReadState.find_or_initialize_by(user_id: user_id, postable_id: topic.id)
130
- .update!(read_at: topic.updated_at, page: 1)
234
+ read_state = Thredded::UserTopicReadState.find_or_initialize_by(user_id: user_id, postable_id: topic.id)
235
+ if rand(2).zero?
236
+ read_state.update!(read_at: topic.updated_at)
237
+ else
238
+ read_state.update!(read_at: topic.posts.order_newest_first.first(2).last.created_at)
239
+ end
131
240
  end
132
241
 
133
242
  class BaseSeedData
243
+ include LogTime
244
+
245
+ # @return [Thredded::DatabaseSeeder]
134
246
  attr_reader :seeder
135
247
 
136
248
  def initialize(seed_database = DatabaseSeeder.new)
@@ -149,6 +261,11 @@ module Thredded
149
261
  @stored = (find || create(*args))
150
262
  end
151
263
 
264
+ def range_of_dates_in_order(up_to: Time.zone.now, count: 1)
265
+ written = up_to
266
+ Array.new(count - 1) { written -= random_duration(10.minutes..6.hours) }.reverse + [up_to]
267
+ end
268
+
152
269
  protected
153
270
 
154
271
  def model_class
@@ -164,6 +281,10 @@ module Thredded
164
281
  def find
165
282
  fail 'Unimplemented'
166
283
  end
284
+
285
+ def random_duration(range)
286
+ (range.min.to_i + rand(range.max.to_i)).seconds
287
+ end
167
288
  end
168
289
 
169
290
  class FirstSeedData < BaseSeedData
@@ -183,7 +304,6 @@ module Thredded
183
304
  MODEL_CLASS = User
184
305
 
185
306
  def create
186
- log 'Creating first user...'
187
307
  FactoryBot.create(:user, :approved, :admin, name: 'Joe', email: 'joe@example.com')
188
308
  end
189
309
  end
@@ -192,19 +312,19 @@ module Thredded
192
312
  class Users < CollectionSeedData
193
313
  MODEL_CLASS = User
194
314
 
195
- def create(count: 1)
315
+ log_method_time def create(count: 1)
196
316
  log "Creating #{count} users..."
197
317
  approved_users_count = (count * 0.97).round
198
318
  [seeder.first_user] +
199
- FactoryBot.create_list(:user, approved_users_count, :approved) +
200
- FactoryBot.create_list(:user, count - approved_users_count)
319
+ FactoryBot.create_list(:user, approved_users_count, :approved, :with_user_details) +
320
+ FactoryBot.create_list(:user, count - approved_users_count, :with_user_details)
201
321
  end
202
322
  end
203
323
 
204
324
  class FirstMessageboard < FirstSeedData
205
325
  MODEL_CLASS = Messageboard
206
326
 
207
- def create
327
+ log_method_time def create(**) # `**` for Ruby < 2.5, see https://bugs.ruby-lang.org/issues/10856
208
328
  log 'Creating a messageboard...'
209
329
  @first_messageboard = FactoryBot.create(
210
330
  :messageboard,
@@ -218,28 +338,39 @@ module Thredded
218
338
  class Topics < CollectionSeedData
219
339
  MODEL_CLASS = Topic
220
340
 
221
- def create(count: 1, messageboard: seeder.first_messageboard)
341
+ log_method_time def create(count: 1, messageboard: seeder.first_messageboard)
222
342
  log "Creating #{count} topics in #{messageboard.name}..."
223
- FactoryBot.create_list(
224
- :topic, count,
225
- messageboard: messageboard,
226
- user: seeder.users.sample,
227
- last_user: seeder.users.sample
228
- )
343
+ Array.new(count) do
344
+ FactoryBot.build(
345
+ :topic,
346
+ messageboard: messageboard,
347
+ user: seeder.users.sample,
348
+ last_user: seeder.users.sample
349
+ ).tap do |topic|
350
+ topic.user_detail = seeder.user_details[topic.user]
351
+ topic.send :set_slug
352
+ topic.send :set_default_moderation_state
353
+ topic.save(validate: false)
354
+ end
355
+ end
229
356
  end
230
357
  end
231
358
 
232
359
  class PrivateTopics < CollectionSeedData
233
360
  MODEL_CLASS = PrivateTopic
234
361
 
235
- def create(count: 1)
362
+ log_method_time def create(count: 1)
363
+ log "Creating #{count} private topics..."
236
364
  Array.new(count) do
237
- FactoryBot.create(
365
+ FactoryBot.build(
238
366
  :private_topic,
239
367
  user: seeder.users[1..-1].sample,
240
368
  last_user: seeder.users.sample,
241
369
  users: [seeder.first_user, *seeder.users.sample(1 + rand(3))]
242
- )
370
+ ).tap do |topic|
371
+ topic.send :set_slug
372
+ topic.save(validate: false)
373
+ end
243
374
  end
244
375
  end
245
376
  end
@@ -247,41 +378,64 @@ module Thredded
247
378
  class Posts < CollectionSeedData
248
379
  MODEL_CLASS = Post
249
380
 
250
- def create(count: (1..1))
381
+ log_method_time def create(count: (1..1), topics: seeder.topics) # rubocop:disable Metrics/MethodLength
251
382
  log "Creating #{count} additional posts in each topic..."
252
- seeder.topics.flat_map do |topic|
253
- last_post_at = random_duration(0..72.hours).ago
383
+ topics.flat_map do |topic|
384
+ last_post_at = random_duration(0..256.hours).ago
254
385
  posts_count = (count.min + rand(count.max + 1))
255
386
  posts = range_of_dates_in_order(up_to: last_post_at, count: posts_count).map.with_index do |written_at, i|
256
387
  author = i.zero? ? topic.user : seeder.users.sample
257
- FactoryBot.create(:post, postable: topic, messageboard: seeder.first_messageboard,
258
- user: author, created_at: written_at, updated_at: written_at)
388
+ Post.new(
389
+ content: seeder.fake_post_contents,
390
+ messageboard_id: topic.messageboard_id,
391
+ postable: topic,
392
+ user: author,
393
+ user_detail: seeder.user_details[author],
394
+ created_at: written_at,
395
+ updated_at: written_at,
396
+ ).tap do |post|
397
+ post.send :set_default_moderation_state
398
+ post.save(validate: false)
399
+ end
259
400
  end
260
- topic.update!(last_user_id: posts.last.user.id, updated_at: last_post_at, last_post_at: last_post_at)
401
+ topic.update_columns(
402
+ posts_count: posts_count,
403
+ last_user_id: posts.last.user.id,
404
+ updated_at: last_post_at,
405
+ last_post_at: last_post_at
406
+ )
261
407
  posts
262
408
  end
263
409
  end
264
-
265
- def range_of_dates_in_order(up_to: Time.zone.now, count: 1)
266
- written = up_to
267
- Array.new(count - 1) { written -= random_duration(10.minutes..6.hours) }.reverse + [up_to]
268
- end
269
-
270
- def random_duration(range)
271
- (range.min.to_i + rand(range.max.to_i)).seconds
272
- end
273
410
  end
274
411
 
275
412
  class PrivatePosts < CollectionSeedData
276
413
  MODEL_CLASS = PrivatePost
277
414
 
278
- def create(count: (1..1))
415
+ log_method_time def create(count: (1..1))
279
416
  log "Creating #{count} additional posts in each private topic..."
280
417
  seeder.private_topics.flat_map do |topic|
281
- (count.min + rand(count.max + 1)).times do |i|
418
+ last_post_at = random_duration(0..256.hours).ago
419
+ posts_count = (count.min + rand(count.max + 1))
420
+ posts = range_of_dates_in_order(up_to: last_post_at, count: posts_count).map.with_index do |written_at, i|
282
421
  author = i.zero? ? topic.user : topic.users.sample
283
- FactoryBot.create(:private_post, postable: topic, user: author)
422
+ PrivatePost.new(
423
+ postable: topic,
424
+ user: author,
425
+ created_at: written_at,
426
+ updated_at: written_at,
427
+ content: seeder.fake_post_contents,
428
+ ).tap do |post|
429
+ post.save(validate: false)
430
+ end
284
431
  end
432
+ topic.update_columns(
433
+ posts_count: posts_count,
434
+ last_user_id: posts.last.user.id,
435
+ updated_at: last_post_at,
436
+ last_post_at: last_post_at
437
+ )
438
+ posts
285
439
  end
286
440
  end
287
441
  end