thredded 0.5.1 → 0.6.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.mkdn +21 -0
  3. data/README.mkdn +17 -1
  4. data/app/assets/stylesheets/thredded/_thredded.scss +1 -0
  5. data/app/assets/stylesheets/thredded/base/_grid.scss +6 -0
  6. data/app/assets/stylesheets/thredded/base/_nav.scss +51 -0
  7. data/app/assets/stylesheets/thredded/base/_variables.scss +1 -0
  8. data/app/assets/stylesheets/thredded/components/_base.scss +4 -0
  9. data/app/assets/stylesheets/thredded/components/_post.scss +19 -6
  10. data/app/assets/stylesheets/thredded/components/_topics.scss +6 -0
  11. data/app/assets/stylesheets/thredded/layout/_moderation.scss +53 -8
  12. data/app/assets/stylesheets/thredded/layout/_navigation.scss +1 -1
  13. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +7 -47
  14. data/app/assets/stylesheets/thredded/layout/_user.scss +10 -0
  15. data/app/commands/thredded/moderate_post.rb +1 -1
  16. data/app/controllers/thredded/messageboards_controller.rb +1 -1
  17. data/app/controllers/thredded/moderation_controller.rb +39 -5
  18. data/app/helpers/thredded/application_helper.rb +4 -2
  19. data/app/models/concerns/thredded/content_moderation_state.rb +15 -8
  20. data/app/models/concerns/thredded/post_common.rb +1 -0
  21. data/app/models/concerns/thredded/topic_common.rb +0 -1
  22. data/app/models/thredded/messageboard.rb +4 -4
  23. data/app/models/thredded/post.rb +2 -0
  24. data/app/models/thredded/post_moderation_record.rb +3 -1
  25. data/app/models/thredded/topic.rb +17 -0
  26. data/app/models/thredded/user_extender.rb +7 -0
  27. data/app/policies/thredded/post_policy.rb +2 -0
  28. data/app/policies/thredded/topic_policy.rb +6 -6
  29. data/app/view_models/thredded/post_view.rb +19 -1
  30. data/app/view_models/thredded/posts_page_view.rb +1 -0
  31. data/app/view_models/thredded/topic_view.rb +5 -1
  32. data/app/views/thredded/messageboards/_messageboard.html.erb +3 -3
  33. data/app/views/thredded/moderation/_nav.html.erb +16 -0
  34. data/app/views/thredded/moderation/_post.html.erb +9 -2
  35. data/app/views/thredded/moderation/_post_moderation_record.html.erb +22 -19
  36. data/app/views/thredded/moderation/_user_moderation_state.html.erb +3 -0
  37. data/app/views/thredded/moderation/_user_post.html.erb +7 -0
  38. data/app/views/thredded/moderation/_users_search_form.html.erb +10 -0
  39. data/app/views/thredded/moderation/history.html.erb +2 -8
  40. data/app/views/thredded/moderation/pending.html.erb +3 -10
  41. data/app/views/thredded/moderation/user.html.erb +42 -0
  42. data/app/views/thredded/moderation/users.html.erb +38 -0
  43. data/app/views/thredded/posts/_post.html.erb +4 -0
  44. data/app/views/thredded/posts_common/_header.html.erb +1 -0
  45. data/app/views/thredded/posts_common/_header_with_topic.html.erb +15 -0
  46. data/app/views/thredded/posts_common/_header_with_user_and_topic.html.erb +18 -0
  47. data/app/views/thredded/search/_form.html.erb +3 -1
  48. data/app/views/thredded/shared/_content_moderation_blocked_state.html.erb +8 -0
  49. data/app/views/thredded/shared/_nav.html.erb +8 -4
  50. data/app/views/thredded/topics/_topic.html.erb +6 -0
  51. data/app/views/thredded/users/_post.html.erb +6 -0
  52. data/app/views/thredded/users/_posts.html.erb +7 -0
  53. data/config/locales/en.yml +29 -3
  54. data/config/locales/pt-BR.yml +34 -9
  55. data/config/routes.rb +7 -2
  56. data/db/migrate/20160329231848_create_thredded.rb +22 -21
  57. data/db/upgrade_migrations/20160611094616_upgrade_v0_5_to_v0_6.rb +25 -0
  58. data/heroku.gemfile.lock +2 -2
  59. data/lib/thredded.rb +17 -5
  60. data/lib/thredded/version.rb +1 -1
  61. metadata +15 -2
@@ -18,12 +18,14 @@ module Thredded
18
18
  end
19
19
 
20
20
  # @param datetime [DateTime]
21
+ # @param default [String] a string to return if time is nil.
21
22
  # @return [String] html_safe datetime presentation
22
- def time_ago(datetime)
23
+ def time_ago(datetime, default: '-')
23
24
  timeago_tag datetime,
24
25
  lang: I18n.locale.to_s.downcase,
25
26
  format: -> (t, _opts) { t.year == Time.current.year ? :short : :long },
26
- nojs: true
27
+ nojs: true,
28
+ default: default
27
29
  end
28
30
 
29
31
  def paginate(collection, args = {})
@@ -14,19 +14,24 @@ module Thredded
14
14
  # @type [Arel::Table]
15
15
  table = arel_table
16
16
  # @type [Arel::Nodes::Node]
17
- visible_to_all =
17
+ visible =
18
18
  if Thredded.content_visible_while_pending_moderation
19
+ # All non-blocked content
19
20
  table[:moderation_state].not_eq(moderation_states[:blocked])
20
21
  else
22
+ # Only approved content
21
23
  table[:moderation_state].eq(moderation_states[:approved])
22
24
  end
23
- where(
24
- if user && !user.thredded_anonymous?
25
- visible_to_all.or(table[:user_id].eq(user.id))
26
- else
27
- visible_to_all
25
+ if user && !user.thredded_anonymous?
26
+ # Own content
27
+ visible = visible.or(table[:user_id].eq(user.id))
28
+ # Content that one can moderate
29
+ moderatable_messageboard_ids = user.thredded_can_moderate_messageboards.map(&:id)
30
+ if moderatable_messageboard_ids.present?
31
+ visible = visible.or(table[:messageboard_id].in(moderatable_messageboard_ids))
28
32
  end
29
- )
33
+ end
34
+ where(visible)
30
35
  end)
31
36
  end
32
37
 
@@ -41,7 +46,9 @@ module Thredded
41
46
 
42
47
  # Whether this is visible to the given user based on the moderation state.
43
48
  def moderation_state_visible_to_user?(user)
44
- moderation_state_visible_to_all? || (!user.thredded_anonymous? && user_id == user.id)
49
+ moderation_state_visible_to_all? ||
50
+ (!user.thredded_anonymous? &&
51
+ (user_id == user.id || user.thredded_can_moderate_messageboards.map(&:id).include?(messageboard_id)))
45
52
  end
46
53
 
47
54
  private
@@ -14,6 +14,7 @@ module Thredded
14
14
  validates :content, presence: true
15
15
 
16
16
  scope :order_oldest_first, -> { order(id: :asc) }
17
+ scope :order_newest_first, -> { order(id: :desc) }
17
18
 
18
19
  after_commit :update_parent_last_user_and_timestamp, on: [:create, :destroy]
19
20
  end
@@ -13,7 +13,6 @@ module Thredded
13
13
  scope :on_page, -> (page_num) { page(page_num).per(30) }
14
14
 
15
15
  validates :hash_id, presence: true, uniqueness: true
16
- validates :last_user_id, presence: true
17
16
  validates :posts_count, numericality: true
18
17
 
19
18
  before_validation do
@@ -25,8 +25,8 @@ module Thredded
25
25
  has_many :user_messageboard_preferences, dependent: :destroy
26
26
  has_many :posts, dependent: :destroy
27
27
  has_many :topics, dependent: :destroy, inverse_of: :messageboard
28
- has_one :latest_topic, -> { order_recently_updated_first },
29
- class_name: 'Thredded::Topic'
28
+
29
+ belongs_to :last_topic, class_name: 'Thredded::Topic'
30
30
 
31
31
  has_many :user_details, through: :posts
32
32
  has_many :messageboard_users,
@@ -54,8 +54,8 @@ module Thredded
54
54
  scope :top_level_messageboards, -> { where(group: nil) }
55
55
  scope :by_messageboard_group, ->(group) { where(group: group.id) }
56
56
 
57
- def latest_user
58
- latest_topic.last_user
57
+ def last_user
58
+ last_topic.try(:last_user)
59
59
  end
60
60
 
61
61
  def slug_candidates
@@ -21,6 +21,8 @@ module Thredded
21
21
  has_many :moderation_records,
22
22
  class_name: 'Thredded::PostModerationRecord',
23
23
  dependent: :nullify
24
+ has_one :last_moderation_record, -> { order_newest_first },
25
+ class_name: 'Thredded::PostModerationRecord'
24
26
 
25
27
  validates :messageboard_id, presence: true
26
28
 
@@ -8,6 +8,8 @@ module Thredded
8
8
  end
9
9
  validates :previous_moderation_state, presence: true
10
10
 
11
+ scope :order_newest_first, -> { order(created_at: :desc, id: :desc) }
12
+
11
13
  belongs_to :messageboard, inverse_of: :post_moderation_records
12
14
  validates :messageboard_id, presence: true
13
15
  belongs_to :post, inverse_of: :moderation_records
@@ -37,7 +39,7 @@ module Thredded
37
39
  post: post,
38
40
  post_content: post.content,
39
41
  post_user: post.user,
40
- post_user_name: post.user.send(Thredded.user_name_column),
42
+ post_user_name: post.user.try(:send, Thredded.user_name_column),
41
43
  messageboard_id: post.messageboard_id,
42
44
  )
43
45
  end
@@ -63,6 +63,8 @@ module Thredded
63
63
  source: :user,
64
64
  through: :user_follows
65
65
 
66
+ after_commit :update_messageboard_last_topic, on: [:create, :destroy]
67
+
66
68
  def self.find_by_slug!(slug)
67
69
  friendly.find(slug)
68
70
  rescue ActiveRecord::RecordNotFound
@@ -107,6 +109,11 @@ module Thredded
107
109
  title_changed?
108
110
  end
109
111
 
112
+ # @return [Thredded::PostModerationRecord, nil]
113
+ def last_moderation_record
114
+ first_post.try(:last_moderation_record)
115
+ end
116
+
110
117
  private
111
118
 
112
119
  def slug_candidates
@@ -115,5 +122,15 @@ module Thredded
115
122
  [:title, '-topic'],
116
123
  ]
117
124
  end
125
+
126
+ def update_messageboard_last_topic
127
+ return if messageboard.destroyed?
128
+ last_topic = if destroyed?
129
+ messageboard.topics.order_recently_updated_first.select(:id).first
130
+ else
131
+ self
132
+ end
133
+ messageboard.update!(last_topic_id: last_topic.try(:id))
134
+ end
118
135
  end
119
136
  end
@@ -41,6 +41,13 @@ module Thredded
41
41
  opt.has_many :thredded_post_moderation_records, foreign_key: 'post_user_id', inverse_of: :post_user
42
42
  opt.has_many :thredded_post_moderated_records, foreign_key: 'moderator_id', inverse_of: :moderator
43
43
  end
44
+
45
+ scope :left_join_thredded_user_details, (lambda do
46
+ users = arel_table
47
+ user_details = Thredded::UserDetail.arel_table
48
+ joins(users.join(user_details, Arel::Nodes::OuterJoin)
49
+ .on(users[:id].eq(user_details[:user_id])).join_sources)
50
+ end)
44
51
  end
45
52
 
46
53
  def thredded_user_preference
@@ -41,6 +41,8 @@ module Thredded
41
41
  @post.postable.first_post != @post && update?
42
42
  end
43
43
 
44
+ delegate :moderate?, to: :messageboard_policy
45
+
44
46
  private
45
47
 
46
48
  def messageboard_policy
@@ -21,17 +21,17 @@ module Thredded
21
21
  # @param user [Thredded.user_class]
22
22
  # @param topic [Thredded::Topic]
23
23
  def initialize(user, topic)
24
- @user = user
25
- @topic = topic
26
- @messageboard_user_permission = MessageboardPolicy.new(user, topic.messageboard)
24
+ @user = user
25
+ @topic = topic
26
+ @messageboard_policy = MessageboardPolicy.new(user, topic.messageboard)
27
27
  end
28
28
 
29
29
  def create?
30
- @messageboard_user_permission.post?
30
+ @messageboard_policy.post?
31
31
  end
32
32
 
33
33
  def read?
34
- @messageboard_user_permission.read? && @topic.moderation_state_visible_to_user?(@user)
34
+ @messageboard_policy.read? && @topic.moderation_state_visible_to_user?(@user)
35
35
  end
36
36
 
37
37
  def update?
@@ -43,7 +43,7 @@ module Thredded
43
43
  end
44
44
 
45
45
  def moderate?
46
- @messageboard_user_permission.moderate?
46
+ @messageboard_policy.moderate?
47
47
  end
48
48
  end
49
49
  end
@@ -11,6 +11,7 @@ module Thredded
11
11
  :pending_moderation?,
12
12
  :approved?,
13
13
  :blocked?,
14
+ :last_moderation_record,
14
15
  to: :@post
15
16
 
16
17
  # @param post [Thredded::PostCommon]
@@ -28,6 +29,10 @@ module Thredded
28
29
  @can_destroy ||= @policy.destroy?
29
30
  end
30
31
 
32
+ def can_moderate?
33
+ @can_moderate ||= @policy.moderate?
34
+ end
35
+
31
36
  def edit_path
32
37
  Thredded::UrlsHelper.edit_post_path(@post)
33
38
  end
@@ -36,17 +41,30 @@ module Thredded
36
41
  Thredded::UrlsHelper.delete_post_path(@post)
37
42
  end
38
43
 
44
+ def permalink_path
45
+ Thredded::UrlsHelper.post_permalink_path(@post.id)
46
+ end
47
+
48
+ # rubocop:disable Metrics/CyclomaticComplexity
39
49
  def cache_key
50
+ moderation_state = unless @post.private_topic_post?
51
+ if @post.pending_moderation? && !Thredded.content_visible_while_pending_moderation
52
+ 'p'
53
+ elsif @post.blocked?
54
+ '-'
55
+ end
56
+ end
40
57
  [
41
58
  I18n.locale,
42
59
  @post,
43
60
  @post.user,
61
+ moderation_state || '+',
44
62
  [
45
- !@post.private_topic_post? && @post.pending_moderation? && !Thredded.content_visible_while_pending_moderation,
46
63
  can_update?,
47
64
  can_destroy?
48
65
  ].map { |p| p ? '+' : '-' } * ''
49
66
  ]
50
67
  end
68
+ # rubocop:enable Metrics/CyclomaticComplexity
51
69
  end
52
70
  end
@@ -6,6 +6,7 @@ module Thredded
6
6
  # A view model for a page of PostViews.
7
7
  class PostsPageView
8
8
  delegate :to_ary,
9
+ :present?,
9
10
  to: :@post_views
10
11
  delegate :total_pages,
11
12
  :current_page,
@@ -2,7 +2,7 @@
2
2
  module Thredded
3
3
  # A view model for Topic.
4
4
  class TopicView < BaseTopicView
5
- delegate :categories, :id,
5
+ delegate :categories, :id, :blocked?, :last_moderation_record,
6
6
  to: :@topic
7
7
 
8
8
  # @param topic [TopicCommon]
@@ -30,6 +30,10 @@ module Thredded
30
30
  ].compact
31
31
  end
32
32
 
33
+ def can_moderate?
34
+ @policy.moderate?
35
+ end
36
+
33
37
  def edit_path
34
38
  Thredded::UrlsHelper.edit_messageboard_topic_path(@topic.messageboard, @topic)
35
39
  end
@@ -11,11 +11,11 @@
11
11
 
12
12
  <p class="thredded--messageboard--description"><%= messageboard.description %></p>
13
13
 
14
- <% if messageboard.latest_topic %>
14
+ <% if messageboard.last_topic %>
15
15
  <p class="thredded--messageboard--byline">
16
16
  <%= t 'thredded.messageboard.last_updated_by_html',
17
- time_ago: time_ago(messageboard.latest_topic.updated_at),
18
- user: messageboard.latest_user %>
17
+ time_ago: time_ago(messageboard.last_topic.updated_at),
18
+ user: messageboard.last_user %>
19
19
  </p>
20
20
  <% end %>
21
21
  <% end %>
@@ -0,0 +1,16 @@
1
+ <% content_for :thredded_main_navigation do %>
2
+ <nav class="thredded--moderation-navigation">
3
+ <ul class="thredded--moderation-navigation--items">
4
+ <li class="thredded--moderation-navigation--item thredded--moderation-navigation--pending">
5
+ <%= link_to t('thredded.nav.moderation_pending'), pending_moderation_path %>
6
+ </li>
7
+ <li class="thredded--moderation-navigation--item thredded--moderation-navigation--history">
8
+ <%= link_to t('thredded.nav.moderation_history'), moderation_history_path %>
9
+ </li>
10
+ <li class="thredded--moderation-navigation--item thredded--moderation-navigation--users">
11
+ <%= link_to t('thredded.nav.moderation_users'), users_moderation_path %>
12
+ </li>
13
+ </ul>
14
+ <%= render 'users_search_form' %>
15
+ </nav>
16
+ <% end %>
@@ -1,6 +1,13 @@
1
1
  <%# @param post [Thredded::PostView] %>
2
- <%= content_tag :article, id: dom_id(post), class: 'thredded--post' do %>
3
- <%= render 'thredded/posts_common/header', post: post %>
2
+ <%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
3
+ <%= render 'thredded/posts_common/header_with_user_and_topic',
4
+ post: post,
5
+ post_user_link: if post.user
6
+ link_to(post.user, user_moderation_path(post.user.id))
7
+ else
8
+ content_tag :em, t('thredded.null_user_name')
9
+ end
10
+ %>
4
11
  <%= render 'thredded/posts_common/content', post: post %>
5
12
  <%= render 'thredded/posts_common/actions', post: post %>
6
13
  <%= render 'post_moderation_actions', post: post %>
@@ -1,20 +1,24 @@
1
1
  <%
2
- post = post_moderation_record.post
2
+ record = post_moderation_record
3
+ post = record.post
4
+ if post
5
+ post_view = Thredded::PostView.new(post, policy(post))
6
+ end
3
7
  moderation_state_notice_args = {
4
- moderator: user_link(post_moderation_record.moderator),
5
- time_ago: time_ago(post_moderation_record.created_at)
8
+ moderator: user_link(record.moderator),
9
+ time_ago: time_ago(record.created_at)
6
10
  }
7
11
  %>
8
- <article class="thredded--post-moderation-record thredded--post-moderation-record-<%= post_moderation_record.moderation_state %>">
12
+ <article class="thredded--post-moderation-record thredded--post-moderation-record-<%= record.moderation_state %>">
9
13
  <header class="thredded--post-moderation-record--header">
10
14
  <p class="thredded--post-moderation-record--moderation-state-notice">
11
- <% if post_moderation_record.approved? %>
15
+ <% if record.approved? %>
12
16
  <%= t('thredded.moderation.post_approved_html', moderation_state_notice_args) %>
13
- <% elsif post_moderation_record.blocked? %>
17
+ <% elsif record.blocked? %>
14
18
  <%= t('thredded.moderation.post_blocked_html', moderation_state_notice_args) %>
15
19
  <% end %>
16
20
  </p>
17
- <% if post && post.content != post_moderation_record.post_content %>
21
+ <% if post && post.content != record.post_content %>
18
22
  <p class="thredded--post-moderation-record--content-changed-notice">
19
23
  <%= t('thredded.moderation.posts_content_changed_since_moderation_html', post_url: post_permalink_path(post)) %>
20
24
  </p>
@@ -22,21 +26,20 @@
22
26
  <%= t 'thredded.moderation.post_deleted_notice' unless post %>
23
27
  </header>
24
28
  <article class="thredded--post">
25
- <header>
26
- <% if post_moderation_record.post_user %>
27
- <%= image_tag Thredded.avatar_url.call(post_moderation_record.post_user), class: 'thredded--post--avatar' %>
28
- <h2 class="thredded--post--user"><%= user_link post_moderation_record.post_user %></h2>
29
+ <% post_user_link = capture do %>
30
+ <% if post.user %>
31
+ <%= link_to post.user, user_moderation_path(post.user.id) %>
29
32
  <% else %>
30
- <h2 class="thredded--post--user">
31
- <%= post_moderation_record.post_user_name %>, <em><%= t 'thredded.null_user_name' %></em>
32
- </h2>
33
+ <%= safe_join [record.post_user_name, content_tag(:em, t('thredded.null_user_name'))].compact, ', ' %>
33
34
  <% end %>
34
- <% if post %>
35
- <p class="thredded--post--created-at"><%= time_ago post.created_at %></p>
36
- <% end %>
37
- </header>
35
+ <% end %>
36
+ <% if post %>
37
+ <%= render 'thredded/posts_common/header_with_user_and_topic', post: post_view, post_user_link: post_user_link %>
38
+ <% else %>
39
+ <header><h2 class="thredded--post--user"><%= post_user_link %></h2></header>
40
+ <% end %>
38
41
  <div class="thredded--post--content">
39
- <%= Thredded::ContentFormatter.new(self).format_content(post_moderation_record.post_content) %>
42
+ <%= Thredded::ContentFormatter.new(self).format_content(record.post_content) %>
40
43
  </div>
41
44
  </article>
42
45
  <%= render 'post_moderation_actions', post: post if post %>
@@ -0,0 +1,3 @@
1
+ <% user_detail = user.thredded_user_detail %>
2
+ <%= user_detail.moderation_state.to_s.humanize %>
3
+ <%= time_ago user_detail.moderation_state_changed_at, default: '' %>
@@ -0,0 +1,7 @@
1
+ <%# @param post [Thredded::PostView] %>
2
+ <%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
3
+ <%= render 'thredded/posts_common/header_with_topic', post: post %>
4
+ <%= render 'thredded/posts_common/content', post: post %>
5
+ <%= render 'thredded/posts_common/actions', post: post %>
6
+ <%= render 'post_moderation_actions', post: post %>
7
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%= form_tag users_moderation_path, class: 'thredded--form thredded--navigation--search', method: 'get' do %>
2
+ <%= label_tag :q, t('thredded.moderation.search_users.form_label') %>
3
+ <%= text_field_tag :q, @query,
4
+ type: 'search',
5
+ required: true,
6
+ # If there are no results the user will likely want to change the query, so auto-focus.
7
+ autofocus: @query.presence && !@users.presence,
8
+ placeholder: t('thredded.moderation.search_users.form_placeholder') %>
9
+ <button type="submit"><%= t 'thredded.search.form.btn_submit' %></button>
10
+ <% end %>
@@ -1,13 +1,7 @@
1
1
  <% content_for :thredded_page_title,
2
2
  safe_join([t('thredded.nav.moderation'), t('thredded.nav.moderation_history')], ': ') %>
3
- <% content_for :thredded_page_id, 'thredded--pending-moderation' %>
4
- <% content_for :thredded_breadcrumbs do %>
5
- <ul class="thredded--navigation-breadcrumbs">
6
- <li><%= link_to t('thredded.nav.all_messageboards'), messageboards_path %></li>
7
- <li><%= link_to t('thredded.nav.moderation'), pending_moderation_path %></li>
8
- <li><%= link_to t('thredded.nav.moderation_history'), moderation_history_path %></li>
9
- </ul>
10
- <% end %>
3
+ <% content_for :thredded_page_id, 'thredded--moderation-history' %>
4
+ <%= render 'nav' %>
11
5
 
12
6
  <%= thredded_page do %>
13
7
  <%= content_tag :section, class: 'thredded--main-section' do %>