thredded 0.14.4 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
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