Merge branch 'fixes' into 'master'

Fixes

See merge request framasoft/mobilizon!580
This commit is contained in:
Thomas Citharel 2020-10-05 17:59:21 +02:00
commit ffa310a138
7 changed files with 135 additions and 87 deletions

View File

@ -2,7 +2,7 @@
<div> <div>
<form <form
class="new-comment" class="new-comment"
v-if="currentActor.id && event.options.commentModeration !== CommentModeration.CLOSED" v-if="isAbleToComment"
@submit.prevent="createCommentForEvent(newComment)" @submit.prevent="createCommentForEvent(newComment)"
@keyup.ctrl.enter="createCommentForEvent(newComment)" @keyup.ctrl.enter="createCommentForEvent(newComment)"
> >
@ -22,11 +22,7 @@
</div> </div>
</article> </article>
</form> </form>
<b-notification <b-notification v-else :closable="false">{{ $t("Comments have been closed.") }}</b-notification>
v-else-if="event.options.commentModeration === CommentModeration.CLOSED"
:closable="false"
>{{ $t("Comments have been closed.") }}</b-notification
>
<transition name="comment-empty-list" mode="out-in"> <transition name="comment-empty-list" mode="out-in">
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul"> <transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
<comment <comment
@ -51,7 +47,6 @@
import { Prop, Vue, Component, Watch } from "vue-property-decorator"; import { Prop, Vue, Component, Watch } from "vue-property-decorator";
import Comment from "@/components/Comment/Comment.vue"; import Comment from "@/components/Comment/Comment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue"; import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";
import { import {
CREATE_COMMENT_FROM_EVENT, CREATE_COMMENT_FROM_EVENT,
@ -190,8 +185,11 @@ export default class CommentTree extends Vue {
// and reset the new comment field // and reset the new comment field
this.newComment = new CommentModel(); this.newComment = new CommentModel();
} catch (e) { } catch (error) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
} }
} }
@ -260,8 +258,11 @@ export default class CommentTree extends Vue {
}, },
}); });
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id); // this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
} catch (e) { } catch (error) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
} }
} }
@ -279,6 +280,18 @@ export default class CommentTree extends Vue {
get filteredOrderedComments(): IComment[] { get filteredOrderedComments(): IComment[] {
return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0); return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0);
} }
get isAbleToComment(): boolean {
if (this.currentActor.id) {
if (
this.event.options.commentModeration !== CommentModeration.CLOSED ||
(this.event.organizerActor && this.currentActor.id === this.event.organizerActor.id)
) {
return true;
}
}
return false;
}
} }
</script> </script>

View File

@ -20,7 +20,6 @@
"Admin": "Admin", "Admin": "Admin",
"Administration": "Administration", "Administration": "Administration",
"All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available", "All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available",
"Allow all comments": "Allow all comments",
"Allow registrations": "Allow registrations", "Allow registrations": "Allow registrations",
"Anonymous participant": "Anonymous participant", "Anonymous participant": "Anonymous participant",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.", "Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.",
@ -785,5 +784,6 @@
"Accessible only to members": "Accessible only to members", "Accessible only to members": "Accessible only to members",
"Created by {name}": "Created by {name}", "Created by {name}": "Created by {name}",
"View all posts": "View all posts", "View all posts": "View all posts",
"Didn't receive the instructions?": "Didn't receive the instructions?" "Didn't receive the instructions?": "Didn't receive the instructions?",
"Allow all comments from users with accounts": "Allow all comments from logged-in users"
} }

View File

@ -823,5 +823,6 @@
"{number} posts": "Aucun billet|Un billet|{number} billets", "{number} posts": "Aucun billet|Un billet|{number} billets",
"{profile} (by default)": "{profile} (par défault)", "{profile} (by default)": "{profile} (par défault)",
"{title} ({count} todos)": "{title} ({count} todos)", "{title} ({count} todos)": "{title} ({count} todos)",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"Allow all comments from users with accounts": "Autoriser tous les commentaires d'utilisateur·ices avec des comptes"
} }

View File

@ -169,7 +169,7 @@
v-model="event.options.commentModeration" v-model="event.options.commentModeration"
name="commentModeration" name="commentModeration"
:native-value="CommentModeration.ALLOW_ALL" :native-value="CommentModeration.ALLOW_ALL"
>{{ $t("Allow all comments") }}</b-radio >{{ $t("Allow all comments from users with accounts") }}</b-radio
> >
</div> </div>

View File

@ -3,9 +3,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
Handles the comment-related GraphQL calls. Handles the comment-related GraphQL calls.
""" """
alias Mobilizon.{Actors, Admin, Discussions} alias Mobilizon.{Actors, Admin, Discussions, Events}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment, as: CommentModel alias Mobilizon.Discussions.Comment, as: CommentModel
alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
@ -20,7 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
def create_comment( def create_comment(
_parent, _parent,
%{actor_id: actor_id} = args, %{actor_id: actor_id, event_id: event_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_user: %User{} = user
@ -28,10 +29,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
} }
) do ) do
with {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id), with {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
{:find_event,
{:ok,
%Event{
options: %EventOptions{comment_moderation: comment_moderation},
organizer_actor_id: organizer_actor_id
}}} <-
{:find_event, Events.get_event(event_id)},
{actor_id, ""} <- Integer.parse(actor_id),
{:allowed, true} <-
{:allowed, comment_moderation != :closed || actor_id == organizer_actor_id},
{:ok, _, %CommentModel{} = comment} <- {:ok, _, %CommentModel{} = comment} <-
Comments.create_comment(args) do Comments.create_comment(args) do
{:ok, comment} {:ok, comment}
else else
{:allowed, false} ->
{:error, :unauthorized}
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
end end

View File

@ -66,7 +66,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "Create a comment" @desc "Create a comment"
field :create_comment, type: :comment do field :create_comment, type: :comment do
arg(:text, non_null(:string)) arg(:text, non_null(:string))
arg(:event_id, :id) arg(:event_id, non_null(:id))
arg(:in_reply_to_comment_id, :id) arg(:in_reply_to_comment_id, :id)
arg(:actor_id, non_null(:id)) arg(:actor_id, non_null(:id))

View File

@ -3,9 +3,11 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Events
alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
@comment %{text: "I love this event"} @comment_text "I love this event"
setup %{conn: conn} do setup %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -16,75 +18,104 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
end end
describe "Comment Resolver" do describe "Comment Resolver" do
@create_comment_mutation """
mutation CreateComment($text: String!, $actorId: ID, $eventId: ID, $inReplyToCommentId: ID) {
createComment(text: $text, actorId: $actorId, eventId: $eventId, inReplyToCommentId: $inReplyToCommentId) {
id,
text,
uuid,
inReplyToComment {
id,
text
}
}
}
"""
test "create_comment/3 creates a comment", %{ test "create_comment/3 creates a comment", %{
conn: conn, conn: conn,
actor: actor, actor: actor,
user: user, user: user,
event: event event: event
} do } do
mutation = """
mutation {
createComment(
text: "#{@comment.text}",
actor_id: "#{actor.id}",
event_id: "#{event.id}"
) {
text,
uuid
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: mutation, variables: %{}) |> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{text: @comment_text, actorId: actor.id, eventId: event.id}
)
assert res["data"]["createComment"]["text"] == @comment.text assert res["data"]["createComment"]["text"] == @comment_text
end end
test "create_comment/3 checks that user owns actor", %{conn: conn, user: user} do test "create_comment/3 checks that user owns actor", %{conn: conn, user: user, event: event} do
actor = insert(:actor) actor = insert(:actor)
mutation = """
mutation {
createComment(
text: "#{@comment.text}",
actor_id: "#{actor.id}"
) {
text,
uuid
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: mutation, variables: %{}) |> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{text: @comment_text, actorId: actor.id, eventId: event.id}
)
assert hd(res["errors"])["message"] == assert hd(res["errors"])["message"] ==
"Profile is not owned by authenticated user" "Profile is not owned by authenticated user"
end end
test "create_comment/3 requires that the user needs to be authenticated", %{conn: conn} do test "create_comment/3 doesn't allow creating events if it's disabled", %{
actor = insert(:actor) conn: conn,
actor: actor,
mutation = """ user: user,
mutation { event: event
createComment( } do
text: "#{@comment.text}", {:ok, %Event{options: %EventOptions{comment_moderation: :closed}}} =
actor_id: "#{actor.id}" Events.update_event(event, %{options: %{comment_moderation: :closed}})
) {
text,
uuid
}
}
"""
res = res =
conn conn
|> AbsintheHelpers.graphql_query(query: mutation, variables: %{}) |> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{text: @comment_text, actorId: actor.id, eventId: event.id}
)
assert hd(res["errors"])["message"] ==
"You don't have permission to do this"
end
test "create_comment/3 allows creating events if it's disabled but we're the organizer", %{
conn: conn,
actor: actor,
user: user
} do
event = insert(:event, organizer_actor: actor, options: %{comment_moderation: :closed})
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{text: @comment_text, actorId: actor.id, eventId: event.id}
)
assert is_nil(res["errors"])
assert res["data"]["createComment"]["text"] == @comment_text
end
test "create_comment/3 requires that the user needs to be authenticated", %{
conn: conn,
event: event
} do
actor = insert(:actor)
res =
conn
|> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{text: @comment_text, actorId: actor.id, eventId: event.id}
)
assert hd(res["errors"])["message"] == assert hd(res["errors"])["message"] ==
"You are not allowed to create a comment if not connected" "You are not allowed to create a comment if not connected"
@ -98,35 +129,24 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
} do } do
comment = insert(:comment) comment = insert(:comment)
mutation = """
mutation {
createComment(
text: "#{@comment.text}",
actor_id: "#{actor.id}",
event_id: "#{event.id}",
in_reply_to_comment_id: "#{comment.id}"
) {
id,
text,
uuid,
in_reply_to_comment {
id,
text
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: mutation, variables: %{}) |> AbsintheHelpers.graphql_query(
query: @create_comment_mutation,
variables: %{
text: @comment_text,
actorId: actor.id,
eventId: event.id,
inReplyToCommentId: comment.id
}
)
assert res["errors"] == nil assert is_nil(res["errors"])
assert res["data"]["createComment"]["text"] == @comment.text assert res["data"]["createComment"]["text"] == @comment_text
uuid = res["data"]["createComment"]["uuid"] uuid = res["data"]["createComment"]["uuid"]
assert res["data"]["createComment"]["in_reply_to_comment"]["id"] == assert res["data"]["createComment"]["inReplyToComment"]["id"] ==
to_string(comment.id) to_string(comment.id)
query = """ query = """
@ -144,7 +164,7 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
|> AbsintheHelpers.graphql_query(query: query, variables: %{}) |> AbsintheHelpers.graphql_query(query: query, variables: %{})
assert res["errors"] == nil assert res["errors"] == nil
assert res["data"]["thread"] == [%{"uuid" => uuid, "text" => @comment.text}] assert res["data"]["thread"] == [%{"uuid" => uuid, "text" => @comment_text}]
end end
@delete_comment """ @delete_comment """