decidim-comments 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/javascripts/decidim/comments/bundle.js +28 -23
- data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
- data/app/commands/decidim/comments/create_comment.rb +10 -0
- data/app/forms/decidim/comments/comment_form.rb +1 -1
- data/app/frontend/comments/add_comment_form.component.jsx +171 -87
- data/app/frontend/comments/add_comment_form.component.test.jsx +41 -22
- data/app/frontend/comments/add_comment_form.mutation.graphql +5 -3
- data/app/frontend/comments/add_comment_form_commentable.fragment.graphql +4 -0
- data/app/frontend/comments/add_comment_form_session.fragment.graphql +6 -0
- data/app/frontend/comments/comment.component.jsx +18 -18
- data/app/frontend/comments/comment.component.test.jsx +22 -21
- data/app/frontend/comments/comment.fragment.graphql +4 -4
- data/app/frontend/comments/comment_data.fragment.graphql +4 -3
- data/app/frontend/comments/comment_order_selector.component.jsx +3 -3
- data/app/frontend/comments/comment_thread.component.jsx +8 -3
- data/app/frontend/comments/comment_thread.component.test.jsx +3 -3
- data/app/frontend/comments/comment_thread.fragment.graphql +1 -1
- data/app/frontend/comments/comments.component.jsx +52 -33
- data/app/frontend/comments/comments.component.test.jsx +51 -38
- data/app/frontend/comments/comments.query.graphql +10 -4
- data/app/frontend/comments/down_vote_button.component.jsx +6 -3
- data/app/frontend/comments/up_vote_button.component.jsx +7 -4
- data/app/frontend/comments/vote_button.component.jsx +5 -0
- data/app/frontend/comments/vote_button_component.test.jsx +1 -1
- data/app/frontend/entry.test.js +2 -0
- data/app/frontend/support/generate_comments_data.js +4 -4
- data/app/mailers/decidim/comments/comment_notification_mailer.rb +31 -0
- data/app/models/decidim/comments/comment.rb +13 -9
- data/app/queries/decidim/comments/{comments_with_replies.rb → sorted_comments.rb} +3 -8
- data/app/types/decidim/comments/commentable_interface.rb +44 -0
- data/app/types/decidim/comments/commentable_mutation_type.rb +29 -0
- data/app/types/decidim/comments/commentable_type.rb +14 -0
- data/app/views/decidim/comments/comment_notification_mailer/comment_created.html.erb +18 -0
- data/app/views/decidim/comments/comment_notification_mailer/reply_created.html.erb +18 -0
- data/config/locales/ca.yml +20 -4
- data/config/locales/en.yml +22 -5
- data/config/locales/es.yml +20 -4
- data/config/locales/eu.yml +5 -0
- data/lib/decidim/comments.rb +5 -0
- data/{app/types/decidim/comments → lib/decidim/comments/api}/add_comment_type.rb +0 -0
- data/{app/types/decidim/comments → lib/decidim/comments/api}/comment_mutation_type.rb +0 -0
- data/{app/types/decidim/comments → lib/decidim/comments/api}/comment_type.rb +11 -17
- data/lib/decidim/comments/commentable.rb +45 -0
- data/{app/helpers → lib}/decidim/comments/comments_helper.rb +15 -10
- data/lib/decidim/comments/mutation_extensions.rb +8 -16
- data/lib/decidim/comments/query_extensions.rb +5 -8
- data/lib/decidim/comments/test/factories.rb +3 -3
- metadata +21 -12
- data/app/frontend/comments/add_comment_form.fragment.graphql +0 -6
@@ -12,7 +12,7 @@ describe("<VoteButton />", () => {
|
|
12
12
|
|
13
13
|
it("should render the number of votes passed as a prop", () => {
|
14
14
|
const wrapper = shallow(<VoteButton votes={10} buttonClassName="vote-button" iconName="vote-icon" voteAction={voteAction} />);
|
15
|
-
expect(wrapper.find('button')).to.
|
15
|
+
expect(wrapper.find('button').text()).to.match(/10/);
|
16
16
|
});
|
17
17
|
|
18
18
|
it("should render a button with the given buttonClassName", () => {
|
data/app/frontend/entry.test.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { random, name, date, image } from 'faker/locale/en';
|
2
2
|
|
3
|
-
/**
|
3
|
+
/**
|
4
4
|
* Generate random comment data to emulate a database real content
|
5
5
|
* @param {number} num - The number of comments to generate random data
|
6
6
|
* @returns {Object[]} - An array of objects representing comments data
|
@@ -17,9 +17,9 @@ const generateCommentsData = (num = 1) => {
|
|
17
17
|
name: name.findName(),
|
18
18
|
avatarUrl: image.imageUrl()
|
19
19
|
},
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
hasComments: false,
|
21
|
+
comments: [],
|
22
|
+
acceptsNewComments: true,
|
23
23
|
alignment: 0,
|
24
24
|
upVotes: random.number(),
|
25
25
|
upVoted: false,
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Comments
|
4
|
+
# A custom mailer for sending notifications to users when
|
5
|
+
# a comment is created.
|
6
|
+
class CommentNotificationMailer < Decidim::ApplicationMailer
|
7
|
+
helper Decidim::ResourceHelper
|
8
|
+
|
9
|
+
def comment_created(user, comment, commentable)
|
10
|
+
with_user(user) do
|
11
|
+
@comment = comment
|
12
|
+
@commentable = commentable
|
13
|
+
@organization = commentable.organization
|
14
|
+
subject = I18n.t("comment_created.subject", scope: "decidim.comments.mailer.comment_notification")
|
15
|
+
mail(to: commentable.author.email, subject: subject)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def reply_created(user, reply, comment, commentable)
|
20
|
+
with_user(user) do
|
21
|
+
@reply = reply
|
22
|
+
@comment = comment
|
23
|
+
@commentable = commentable
|
24
|
+
@organization = commentable.organization
|
25
|
+
subject = I18n.t("reply_created.subject", scope: "decidim.comments.mailer.comment_notification")
|
26
|
+
mail(to: comment.author.email, subject: subject)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -6,6 +6,7 @@ module Decidim
|
|
6
6
|
# to discuss or share their thoughts about the resource.
|
7
7
|
class Comment < ApplicationRecord
|
8
8
|
include Decidim::Authorable
|
9
|
+
include Decidim::Comments::Commentable
|
9
10
|
|
10
11
|
# Limit the max depth of a comment tree. If C is a comment and R is a reply:
|
11
12
|
# C (depth 0)
|
@@ -16,7 +17,6 @@ module Decidim
|
|
16
17
|
MAX_DEPTH = 3
|
17
18
|
|
18
19
|
belongs_to :commentable, foreign_key: "decidim_commentable_id", foreign_type: "decidim_commentable_type", polymorphic: true
|
19
|
-
has_many :replies, as: :commentable, foreign_key: "decidim_commentable_id", foreign_type: "decidim_commentable_type", class_name: Comment
|
20
20
|
has_many :up_votes, -> { where(weight: 1) }, foreign_key: "decidim_comment_id", class_name: CommentVote, dependent: :destroy
|
21
21
|
has_many :down_votes, -> { where(weight: -1) }, foreign_key: "decidim_comment_id", class_name: CommentVote, dependent: :destroy
|
22
22
|
|
@@ -24,16 +24,14 @@ module Decidim
|
|
24
24
|
validates :depth, numericality: { greater_than_or_equal_to: 0 }
|
25
25
|
validates :alignment, inclusion: { in: [0, 1, -1] }
|
26
26
|
|
27
|
-
validate :
|
27
|
+
validate :commentable_can_have_comments
|
28
28
|
|
29
29
|
before_save :compute_depth
|
30
30
|
|
31
31
|
delegate :organization, to: :commentable
|
32
32
|
|
33
|
-
# Public:
|
34
|
-
|
35
|
-
# Returns a bool value to indicate if comment can have replies
|
36
|
-
def can_have_replies?
|
33
|
+
# Public: Override Commentable concern method `accepts_new_comments?`
|
34
|
+
def accepts_new_comments?
|
37
35
|
depth < MAX_DEPTH
|
38
36
|
end
|
39
37
|
|
@@ -51,12 +49,18 @@ module Decidim
|
|
51
49
|
down_votes.any? { |vote| vote.author == user }
|
52
50
|
end
|
53
51
|
|
52
|
+
# Public: Returns the commentable object of the parent comment
|
53
|
+
def root_commentable
|
54
|
+
return commentable if depth == 0
|
55
|
+
commentable.root_commentable
|
56
|
+
end
|
57
|
+
|
54
58
|
private
|
55
59
|
|
56
|
-
# Private: Check if commentable can have
|
60
|
+
# Private: Check if commentable can have comments and if not adds
|
57
61
|
# a validation error to the model
|
58
|
-
def
|
59
|
-
errors.add(:commentable, :
|
62
|
+
def commentable_can_have_comments
|
63
|
+
errors.add(:commentable, :cannot_have_comments) unless commentable.accepts_new_comments?
|
60
64
|
end
|
61
65
|
|
62
66
|
# Private: Compute comment depth inside the current comment tree
|
@@ -2,7 +2,7 @@
|
|
2
2
|
module Decidim
|
3
3
|
module Comments
|
4
4
|
# A class used to find comments for a commentable resource
|
5
|
-
class
|
5
|
+
class SortedComments < Rectify::Query
|
6
6
|
attr_reader :commentable
|
7
7
|
|
8
8
|
# Syntactic sugar to initialize the class and return the queried objects.
|
@@ -32,11 +32,6 @@ module Decidim
|
|
32
32
|
scope = Comment
|
33
33
|
.where(commentable: commentable)
|
34
34
|
.includes(:author, :up_votes, :down_votes)
|
35
|
-
.includes(
|
36
|
-
replies: [:author, :up_votes, :down_votes,
|
37
|
-
replies: [:author, :up_votes, :down_votes,
|
38
|
-
replies: [:author, :up_votes, :down_votes]]]
|
39
|
-
)
|
40
35
|
|
41
36
|
scope = case @options[:order_by]
|
42
37
|
when "older"
|
@@ -77,8 +72,8 @@ module Decidim
|
|
77
72
|
end
|
78
73
|
|
79
74
|
def count_replies(comment)
|
80
|
-
if comment.
|
81
|
-
comment.
|
75
|
+
if comment.comments.size.positive?
|
76
|
+
comment.comments.size + comment.comments.sum { |reply| count_replies(reply) }
|
82
77
|
else
|
83
78
|
0
|
84
79
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Comments
|
4
|
+
# This interface represents a commentable object.
|
5
|
+
CommentableInterface = GraphQL::InterfaceType.define do
|
6
|
+
name "CommentableInterface"
|
7
|
+
description "A commentable interface"
|
8
|
+
|
9
|
+
field :id, !types.ID, "The commentable's ID"
|
10
|
+
|
11
|
+
field :type, !types.String, "The commentable's class name. i.e. `Decidim::ParticipatoryProcess`" do
|
12
|
+
property :commentable_type
|
13
|
+
end
|
14
|
+
|
15
|
+
field :acceptsNewComments, !types.Boolean, "Wether the object can have new comments or not" do
|
16
|
+
property :accepts_new_comments?
|
17
|
+
end
|
18
|
+
|
19
|
+
field :commentsHaveAlignment, !types.Boolean, "Wether the object comments have alignment or not" do
|
20
|
+
property :comments_have_alignment?
|
21
|
+
end
|
22
|
+
|
23
|
+
field :commentsHaveVotes, !types.Boolean, "Wether the object comments have votes or not" do
|
24
|
+
property :comments_have_votes?
|
25
|
+
end
|
26
|
+
|
27
|
+
field :comments do
|
28
|
+
type !types[CommentType]
|
29
|
+
|
30
|
+
argument :orderBy, types.String, "Order the comments"
|
31
|
+
|
32
|
+
resolve lambda { |obj, args, _ctx|
|
33
|
+
SortedComments.for(obj, order_by: args[:orderBy])
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
field :hasComments, !types.Boolean, "Check if the commentable has comments" do
|
38
|
+
resolve lambda { |obj, _args, _ctx|
|
39
|
+
obj.comments.size.positive?
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Comments
|
4
|
+
CommentableMutationType = GraphQL::ObjectType.define do
|
5
|
+
name "CommentableMutation"
|
6
|
+
description "A commentable which includes its available mutations"
|
7
|
+
|
8
|
+
field :id, !types.ID, "The Commentable's unique ID"
|
9
|
+
|
10
|
+
field :addComment, Decidim::Comments::CommentType do
|
11
|
+
description "Add a new comment to a commentable"
|
12
|
+
|
13
|
+
argument :body, !types.String, "The comments's body"
|
14
|
+
argument :alignment, types.Int, "The comment's alignment. Can be 0 (neutral), 1 (in favor) or -1 (against)'", default_value: 0
|
15
|
+
argument :userGroupId, types.ID, "The comment's user group id. Replaces the author."
|
16
|
+
|
17
|
+
resolve lambda { |obj, args, ctx|
|
18
|
+
params = { "comment" => { "body" => args[:body], "alignment" => args[:alignment], "user_group_id" => args[:userGroupId] } }
|
19
|
+
form = Decidim::Comments::CommentForm.from_params(params)
|
20
|
+
Decidim::Comments::CreateComment.call(form, ctx[:current_user], obj) do
|
21
|
+
on(:ok) do |comment|
|
22
|
+
return comment
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Comments
|
4
|
+
# This type represents a commentable object.
|
5
|
+
CommentableType = GraphQL::ObjectType.define do
|
6
|
+
name "Commentable"
|
7
|
+
description "A commentable object"
|
8
|
+
|
9
|
+
interfaces [
|
10
|
+
Decidim::Comments::CommentableInterface
|
11
|
+
]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<p><%= t("decidim.comments.comment_notification_mailer.hello", name: @commentable.author.name) %></p>
|
2
|
+
|
3
|
+
<p>
|
4
|
+
<%=
|
5
|
+
t(".new_comment_html", {
|
6
|
+
commenter: @comment.author.name,
|
7
|
+
commentable_link: link_to(@commentable.title, decidim_resource_url(@commentable))
|
8
|
+
})
|
9
|
+
%>
|
10
|
+
</p>
|
11
|
+
|
12
|
+
<blockquote>
|
13
|
+
<%= @comment.body %>
|
14
|
+
</blockquote>
|
15
|
+
|
16
|
+
<p>
|
17
|
+
<%= t("decidim.comments.comment_notification_mailer.manage_email_subscriptions_html", link: link_to(t("decidim.comments.comment_notification_mailer.notifications_settings_link"), decidim.notifications_settings_url(host: @organization.host))) %>
|
18
|
+
</p>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<p><%= t("decidim.comments.comment_notification_mailer.hello", name: @comment.author.name) %></p>
|
2
|
+
|
3
|
+
<p>
|
4
|
+
<%=
|
5
|
+
t(".new_reply_html", {
|
6
|
+
commenter: @reply.author.name,
|
7
|
+
commentable_link: link_to(@commentable.title, decidim_resource_url(@commentable))
|
8
|
+
})
|
9
|
+
%>
|
10
|
+
</p>
|
11
|
+
|
12
|
+
<blockquote>
|
13
|
+
<%= @reply.body %>
|
14
|
+
</blockquote>
|
15
|
+
|
16
|
+
<p>
|
17
|
+
<%= t("decidim.comments.comment_notification_mailer.manage_email_subscriptions_html", link: link_to(t("decidim.comments.comment_notification_mailer.notifications_settings_link"), decidim.notifications_settings_url(host: @organization.host))) %>
|
18
|
+
</p>
|
data/config/locales/ca.yml
CHANGED
@@ -2,21 +2,36 @@ ca:
|
|
2
2
|
activerecord:
|
3
3
|
errors:
|
4
4
|
messages:
|
5
|
-
|
5
|
+
cannot_have_comments: no pot tenir comentaris
|
6
6
|
decidim:
|
7
|
+
comments:
|
8
|
+
comment_notification_mailer:
|
9
|
+
comment_created:
|
10
|
+
new_comment_html: Hi ha un nou comentari d'<b>%{commenter}</b> a <b>%{commentable_link}</b>
|
11
|
+
hello: Hola %{name},
|
12
|
+
manage_email_subscriptions_html: Pots deixar de rebre aquests correus electrònics canviant la configuració a %{link}.
|
13
|
+
notifications_settings_link: la pàgina de configuració de les notificacions
|
14
|
+
reply_created:
|
15
|
+
new_reply_html: Hi ha una nova resposta del teu comentari de l'<b>%{commenter}</b> a <b>%{commentable_link}</b>
|
16
|
+
mailer:
|
17
|
+
comment_notification:
|
18
|
+
comment_created:
|
19
|
+
subject: Tens un nou comentari
|
20
|
+
reply_created:
|
21
|
+
subject: Tens una nova resposta del teu comentari
|
7
22
|
components:
|
8
23
|
add_comment_form:
|
24
|
+
account_message: "<a href=\"%{sign_in_url}\">Entra amb el teu compte</a> o <a href=\"%{sign_up_url}\">registra't</a> per a deixar un comentari.\n"
|
9
25
|
form:
|
10
26
|
body:
|
11
27
|
label: Comentari
|
12
28
|
placeholder: Què en penses d'això?
|
29
|
+
form_error: El text és necessari i no pot ser més llarg de %{length}
|
13
30
|
submit: Envia
|
14
31
|
user_group_id:
|
15
32
|
label: Comentar com a
|
16
33
|
opinion:
|
17
|
-
|
18
|
-
in_favor: Hi estic a favor
|
19
|
-
neutral: Sóc neutral
|
34
|
+
neutral: Neutral
|
20
35
|
title: Deixa el teu comentari
|
21
36
|
comment:
|
22
37
|
alignment:
|
@@ -33,6 +48,7 @@ ca:
|
|
33
48
|
comment_thread:
|
34
49
|
title: Conversa amb %{authorName}
|
35
50
|
comments:
|
51
|
+
blocked_comments_warning: Els comentaris estan desactivats a la fase actual però pots llegir els comentaris de les fases anteriors.
|
36
52
|
loading: Carregant els comentaris ...
|
37
53
|
title: "%{count} comentaris"
|
38
54
|
featured_comment:
|
data/config/locales/en.yml
CHANGED
@@ -3,22 +3,38 @@ en:
|
|
3
3
|
activerecord:
|
4
4
|
errors:
|
5
5
|
messages:
|
6
|
-
|
6
|
+
cannot_have_comments: can't have comments
|
7
7
|
decidim:
|
8
|
+
comments:
|
9
|
+
comment_notification_mailer:
|
10
|
+
comment_created:
|
11
|
+
new_comment_html: There is a new comment from <b>%{commenter}</b> in <b>%{commentable_link}</b>
|
12
|
+
hello: Hello %{name},
|
13
|
+
manage_email_subscriptions_html: You can stop receiving these emails by changing your settings in %{link}.
|
14
|
+
notifications_settings_link: the notifications settings page
|
15
|
+
reply_created:
|
16
|
+
new_reply_html: There is a new reply of your comment from <b>%{commenter}</b> in <b>%{commentable_link}</b>
|
17
|
+
mailer:
|
18
|
+
comment_notification:
|
19
|
+
comment_created:
|
20
|
+
subject: You have a new comment
|
21
|
+
reply_created:
|
22
|
+
subject: You have a new reply of your comment
|
8
23
|
components:
|
9
24
|
add_comment_form:
|
25
|
+
account_message: "<a href=\"%{sign_in_url}\">Sign in with your account</a> or <a href=\"%{sign_up_url}\">sign up</a> to add your comment.
|
26
|
+
"
|
10
27
|
form:
|
11
28
|
body:
|
12
29
|
label: Comment
|
13
30
|
placeholder: What do you think about this?
|
31
|
+
form_error: The text is required and it can't be longer than %{length} characters.
|
14
32
|
submit: Send
|
15
33
|
user_group_id:
|
16
34
|
label: Comment as
|
17
35
|
opinion:
|
18
|
-
|
19
|
-
|
20
|
-
neutral: I am neutral
|
21
|
-
title: Leave your comment
|
36
|
+
neutral: Neutral
|
37
|
+
title: Add your comment
|
22
38
|
comment:
|
23
39
|
alignment:
|
24
40
|
against: Against
|
@@ -34,6 +50,7 @@ en:
|
|
34
50
|
comment_thread:
|
35
51
|
title: Conversation with %{authorName}
|
36
52
|
comments:
|
53
|
+
blocked_comments_warning: Comments are disabled in the current step, but you can read the comments from previous steps.
|
37
54
|
loading: Loading comments ...
|
38
55
|
title: "%{count} comments"
|
39
56
|
featured_comment:
|
data/config/locales/es.yml
CHANGED
@@ -2,21 +2,36 @@ es:
|
|
2
2
|
activerecord:
|
3
3
|
errors:
|
4
4
|
messages:
|
5
|
-
|
5
|
+
cannot_have_comments: no puede tener comentarios
|
6
6
|
decidim:
|
7
|
+
comments:
|
8
|
+
comment_notification_mailer:
|
9
|
+
comment_created:
|
10
|
+
new_comment_html: Hay un nuevo comentario de <b>%{commenter}</b> en <b>%{commentable_link}</b>
|
11
|
+
hello: Hola %{name},
|
12
|
+
manage_email_subscriptions_html: Puedes dejar de recibir estos correos electrónicos cambiando la configuración en %{link}.
|
13
|
+
notifications_settings_link: la página de configuración de las notificaciones
|
14
|
+
reply_created:
|
15
|
+
new_reply_html: Hay una nueva respuesta de tu comentario de <b>%{commenter}</b> en <b>%{commentable_link}</b>
|
16
|
+
mailer:
|
17
|
+
comment_notification:
|
18
|
+
comment_created:
|
19
|
+
subject: Tienes un nuevo comentario
|
20
|
+
reply_created:
|
21
|
+
subject: Uno de tus comentarios ha recibido respuesta
|
7
22
|
components:
|
8
23
|
add_comment_form:
|
24
|
+
account_message: "<a href=\"%{sign_in_url}\">Entra con tu cuenta</a> o <a href=\"%{sign_up_url}\">regístrate</a> para dejar tu comentario.\n"
|
9
25
|
form:
|
10
26
|
body:
|
11
27
|
label: Comentario
|
12
28
|
placeholder: '¿Qué piensas sobre esto?'
|
29
|
+
form_error: El texto es necesario y no puede ser más de caracteres %{length}.
|
13
30
|
submit: Enviar
|
14
31
|
user_group_id:
|
15
32
|
label: Comentar como
|
16
33
|
opinion:
|
17
|
-
|
18
|
-
in_favor: Estoy a favor
|
19
|
-
neutral: Soy neutral
|
34
|
+
neutral: Neutral
|
20
35
|
title: Deje su comentario
|
21
36
|
comment:
|
22
37
|
alignment:
|
@@ -33,6 +48,7 @@ es:
|
|
33
48
|
comment_thread:
|
34
49
|
title: Conversación con %{authorName}
|
35
50
|
comments:
|
51
|
+
blocked_comments_warning: Los comentarios estan desactivados en la fase actual pero puedes leer los comentarios de las fases anteriores.
|
36
52
|
loading: Cargando los comentarios ...
|
37
53
|
title: "%{count} comentarios"
|
38
54
|
featured_comment:
|