From 482369d5fed221e22cad2ccafb167d50e6a29c80 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Tue, 29 Aug 2023 12:57:52 +0000 Subject: [PATCH 01/10] Translated using Weblate (Croatian) Currently translated at 87.9% (1393 of 1584 strings) Translation: Mobilizon/Frontend Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/hr/ --- js/src/i18n/hr.json | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/js/src/i18n/hr.json b/js/src/i18n/hr.json index 80e9fb2bb..bac0aad31 100644 --- a/js/src/i18n/hr.json +++ b/js/src/i18n/hr.json @@ -81,6 +81,7 @@ "Admin settings successfully saved.": "Administratorske postavke su uspješno spremljene.", "Administration": "Administrator", "Administrator": "Administrator", + "All": "Sve", "All activities": "Sve aktivnosti", "All good, let's continue!": "Sve je u redu. Nastavimo!", "All the places have already been taken": "Sva mjesta su već zauzeta", @@ -133,12 +134,12 @@ "Atom feed for events and posts": "Atom feed za događaje i objave", "Attending": "Prisustvovanje", "Avatar": "Avatar", - "Back to group list": "", + "Back to group list": "Natrag na popis grupa", "Back to homepage": "Natrag na početnu web-stranicu", "Back to previous page": "Natrag na prethodnu stranicu", - "Back to profile list": "", + "Back to profile list": "Natrag na popis profila", "Back to top": "Natrag na vrh stranice", - "Back to user list": "", + "Back to user list": "Natrag na popis korisnika", "Banner": "Natpis", "Before you can login, you need to click on the link inside it to validate your account.": "Morate ovjeriti svoj račun na dobivenoj poveznici kako biste se mogli prijaviti.", "Begins on": "Započinje na", @@ -387,6 +388,9 @@ "Find or add an element": "Pronađi ili dodaj jedan element", "First steps": "Prvi koraci", "Follow": "Prati", + "Follow a new instance": "Prati novu instancu", + "Follow instance": "Prati instancu", + "Follow status": "Prati stanje", "Follower": "Pratilac", "Followers": "Pratioci", "Followers will receive new public events and posts.": "Pratioci će primati obavijesti o novim javnim događajima i objave.", @@ -472,9 +476,9 @@ "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Ako je odabrano ručno odobrenje sudionika, Mobilizon će ti poslati e-mail o novim sudionicima koji se moraju obraditi. Dolje možeš odabrati učestalost ovih obavijesti.", "If you want, you may send a message to the event organizer here.": "Ako želite, ovdje možete poslati poruku organizatoru događaja.", "Ignore": "Zanemari", - "In person": "", + "In person": "Osobno", "In the following context, an application is a software, either provided by the Mobilizon team or by a 3rd-party, used to interact with your instance.": "U sljedećem kontekstu, aplikacija je softver pružen od Mobilizon tima ili 3. partije, koji vam omogućuje povezivanje sa instancom.", - "In the past": "", + "In the past": "U prošlosti", "In this instance's network": "U mreži ove instance", "Increase": "Povećaj", "Instance": "Instanca", @@ -624,11 +628,12 @@ "No follower matches the filters": "Nijedan pratilac ne odgovara filtrima", "No group found": "Bez rezultata", "No group matches the filters": "Nijedna grupa ne odgovara filterima", - "No group member found": "", + "No group member found": "Nije pronađen nijedan član grupe", "No groups found": "Bez rezultata", "No groups found for {search}": "Nema grupa za {search}", "No information": "Nema informacija", "No instance follows your instance yet.": "Nijedna instanca još ne prati vašu instancu.", + "No instance found.": "Nije pronađena nijedna instanca.", "No instance to approve|Approve instance|Approve {number} instances": "Nema instance za odobriti|Odobri instancu|Odobri {number} instanca", "No instance to reject|Reject instance|Reject {number} instances": "Nema instance za odbiti|Odbij instancu|Odbij {number} instanca", "No instance to remove|Remove instance|Remove {number} instances": "Nema instanca za ukloniti|Ukolni instancu|Ukloni {number} instanca", @@ -913,6 +918,7 @@ "Starts on…": "Započinje…", "Status": "Stanje", "Statuses": "Stanja", + "Stop following instance": "Prekini pratiti instancu", "Street": "Ulica", "Submit": "Podnesi", "Submit to Akismet": "Pošalji Akismetu", @@ -975,9 +981,9 @@ "The group's physical address was changed.": "Promijenjena je fizička adresa grupe.", "The group's short description was changed.": "Promijenjen je kratki opis grupe.", "The instance administrator is the person or entity that runs this Mobilizon instance.": "Administrator instance je osoba ili oni koji vode ovu Mobilizon instancu.", - "The member was approved": "", + "The member was approved": "Član je odobren", "The member was removed from the group {group}": "Član je izbačen iz grupe {group}", - "The membership request from {profile} was rejected": "", + "The membership request from {profile} was rejected": "Zahtjev za članstvo od {profile} je odbijen", "The only way for your group to get new members is if an admininistrator invites them.": "Jedini način da vaša grupa dobije nove članove je da ih pozove adminitrator grupe.", "The organiser has chosen to close comments.": "Organizatori su odlučili zatvoriti komentare.", "The page you're looking for doesn't exist.": "Stranica koju tražite ne postoji.", @@ -1008,7 +1014,7 @@ "This group doesn't have a description yet.": "Ova grupa nema opis.", "This group is accessible only through it's link. Be careful where you post this link.": "Ova je grupa dostupna samo putem njene poveznice. Pazi gdje objavljuješ ovu poveznicu.", "This group is invite-only": "Ovoj grupi se ne možete pridružiti bez pozivnice", - "This group was not found": "", + "This group was not found": "Ova grupa nije pronađena", "This identifier is unique to your profile. It allows others to find you.": "Ovaj identifikator jer jedinstven vašem profilu. Omogućuje drugima da vas nađu.", "This information is saved only on your computer. Click for details": "Ove informacije su spremljene na vaše računalo. Kliknite za detalje", "This instance hasn't got push notifications enabled.": "Ova instanca nema aktivirane automatske obavijesti.", @@ -1020,10 +1026,10 @@ "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ova objava je dostupna samo članovima. Imaš pristup u svrhu moderiranja budući da si moderator instance.", "This post is accessible only through it's link. Be careful where you post this link.": "Ova je objava dostupna samo putem njene poveznice. Pazi gdje objavljuješ ovu poveznicu.", "This profile is from another instance, the informations shown here may be incomplete.": "Ovaj profil je iz jedne druge instance, ovdje prikazane informacije mogu biti nepotpune.", - "This profile was not found": "", + "This profile was not found": "Ovaj profil nije pronađen", "This setting will be used to display the website and send you emails in the correct language.": "Ova postavka će se koristiti za prikazivanje web-stranice i slanje e-maila u ispravnom jeziku.", "This user doesn't have any profiles": "Korisnik nema nijedan profil", - "This user was not found": "", + "This user was not found": "Ovaj korisnik nije pronađen", "This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone).": "Ova web-stranica nije moderirana te će se svi podatci koje upišete automatski izbirisati svaki dan u 00:01 (Pariška vremenska zona).", "This week": "Ovaj tjedan", "This weekend": "Ovaj vikend", @@ -1160,7 +1166,7 @@ "Yesterday": "Jučer", "You accepted the invitation to join the group.": "Prihvatili ste poziv u grupu.", "You added the member {member}.": "Dodali ste člana {member}.", - "You approved {member}'s membership.": "", + "You approved {member}'s membership.": "Odobrio/la si članstvo za {member}.", "You archived the discussion {discussion}.": "Arhivirali ste razgovor {discussion}.", "You are not an administrator for this group.": "Niste administrator ove grupe.", "You are not part of any group.": "Niste član nijedne grupe.", @@ -1192,7 +1198,7 @@ "You don't follow any instances yet.": "Ne pratite nijednu instancu.", "You don't have any upcoming events. Maybe try another filter?": "Nemaš nijedan nadolazeći događaj. Želiš li probati jedan drugi filtar?", "You excluded member {member}.": "Isključili ste člana {member}.", - "You have attended {count} events in the past.": "", + "You have attended {count} events in the past.": "Broj događaja kojima si prisustvovao/la u prošlosti: 0.|Broj događaja kojima si prisustvovao/la u prošlosti: 1.|Broj događaja kojima si prisustvovao/la u prošlosti: {count}.", "You have been invited by {invitedBy} to the following group:": "{invitedBy} su vas pozvali u grupu:", "You have been removed from this group's members.": "Izbačeni ste iz ove grupe.", "You have cancelled your participation": "Otkazali ste svoje sudjelovanje", @@ -1212,7 +1218,7 @@ "You promoted the member {member} to an unknown role.": "Promovirali ste člana {member} u nepoznatu ulogu.", "You promoted {member} to administrator.": "Promovirali ste {member} u administratora.", "You promoted {member} to moderator.": "Promovirali ste člana {member} u moderatora.", - "You rejected {member}'s membership request.": "", + "You rejected {member}'s membership request.": "Odbio/la si zahtjev za članstvo za {member}.", "You renamed the discussion from {old_discussion} to {discussion}.": "Preimenovali ste razgovor iz {old_discussion} u {discussion}.", "You renamed the folder from {old_resource_title} to {resource}.": "Preimenovali ste mapu iz {old_resource_title} u {resource}.", "You renamed the resource from {old_resource_title} to {resource}.": "preimenovali ste resurs iz {old_resource_title} u {resource}.", @@ -1252,7 +1258,7 @@ "Your email is being changed": "Vaša e-mail adresa se mijenja", "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Vaša e-mail adresa će se koristiti samo za potvrđivanje da ste stvarna osoba i za slanje eventualnih novosti za ovaj događaj. NEĆE SE proslijediti drugim instancama ili organizatorima događaja.", "Your federated identity": "Vaš federalizirani identitet", - "Your membership was approved by {profile}.": "", + "Your membership was approved by {profile}.": "Tvoje članstvo je odobrio/la {profile}.", "Your participation has been confirmed": "Vaše sudjelovanje je potvrđeno", "Your participation has been rejected": "Vaše sudjelovanje je odbijeno", "Your participation has been requested": "Poslan je zahtjev za vaše sudjelovanje", @@ -1276,6 +1282,7 @@ "[This comment has been deleted]": "[Ovaj je komentar izbrisan]", "[deleted]": "[izbrisano]", "a non-existent report": "nepostojeći izvještaj", + "access the corresponding account": "pristupi odgovarajućem računu", "access to the group's private content as well": "", "and {number} groups": "dodaj {number} grupa", "any distance": "bilo koja udaljenost", @@ -1358,7 +1365,7 @@ "{old_group_name} was renamed to {group}.": "{old_group_name} je preimenovano u {group}.", "{profile} (by default)": "{profile} (po zadanom)", "{profile} added the member {member}.": "{profile} su dodali člana {member}.", - "{profile} approved {member}'s membership.": "", + "{profile} approved {member}'s membership.": "{profile} je odobrio/la članstvo za {member}.", "{profile} archived the discussion {discussion}.": "{profile} su arhivirali razgovor {discussion}.", "{profile} created the discussion {discussion}.": "{profile} su stvorili razgovor {discussion}.", "{profile} created the folder {resource}.": "{profile} su stvorili mapu {resource}.", @@ -1380,7 +1387,7 @@ "{profile} promoted {member} to an unknown role.": "{profile} su promovirali {member} u nepoznatu ulogu.", "{profile} promoted {member} to moderator.": "{profile} su promovirali {member} u moderatora.", "{profile} quit the group.": "{profile} su izašli iz grupe.", - "{profile} rejected {member}'s membership request.": "", + "{profile} rejected {member}'s membership request.": "{profile} je odbio/la zahtjev za članstvo za {member}.", "{profile} renamed the discussion from {old_discussion} to {discussion}.": "{profile} su preimenovali razgovor iz {old_discussion} u {discussion}.", "{profile} renamed the folder from {old_resource_title} to {resource}.": "{profile} su preimenovali mapu iz {old_resource_title} u {resource}.", "{profile} renamed the resource from {old_resource_title} to {resource}.": "{profile} su preimenovali resurs iz {old_resource_title} u {resource}.", From f2ac3e2e5d28f4257a5e2d4870d339fecf3a5f1b Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 5 Jun 2023 18:32:29 +0200 Subject: [PATCH 02/10] feat(reports): allow reports to hold multiple events Signed-off-by: Thomas Citharel --- js/src/components/Comment/EventComment.vue | 2 +- .../components/Event/EventActionSection.vue | 2 +- js/src/composition/apollo/report.ts | 2 +- js/src/graphql/report.ts | 8 +-- js/src/types/report.model.ts | 2 +- js/src/views/Moderation/ReportView.vue | 50 +++++++++++-------- lib/federation/activity_pub/types/reports.ex | 22 ++++---- .../activity_stream/converter/flag.ex | 43 +++++++--------- lib/graphql/resolvers/report.ex | 2 +- lib/graphql/schema/report.ex | 10 ++-- lib/mobilizon/reports/report.ex | 15 ++++-- lib/mobilizon/reports/reports.ex | 6 +-- lib/service/anti_spam/akismet.ex | 2 +- lib/web/templates/email/report.html.heex | 25 ++++++---- lib/web/templates/email/report.text.eex | 8 +-- ...4_allow_multiple_events_to_be_reported.exs | 14 ++++++ ...backfill_report_events_with_old_events.exs | 31 ++++++++++++ ...06_remove_obsolete_event_id_on_reports.exs | 15 ++++++ test/graphql/api/report_test.exs | 10 ++-- test/graphql/resolvers/report_test.exs | 18 ++++--- test/support/factory.ex | 2 +- 21 files changed, 185 insertions(+), 104 deletions(-) create mode 100644 priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs create mode 100644 priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs create mode 100644 priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs diff --git a/js/src/components/Comment/EventComment.vue b/js/src/components/Comment/EventComment.vue index d0069c250..7219fb2f1 100644 --- a/js/src/components/Comment/EventComment.vue +++ b/js/src/components/Comment/EventComment.vue @@ -352,7 +352,7 @@ const reportComment = async ( ): Promise => { if (!props.comment.actor) return; createReportMutation({ - eventId: props.event.id, + eventsIds: [props.event.id ?? ""], reportedId: props.comment.actor?.id ?? "", commentsIds: [props.comment.id ?? ""], content, diff --git a/js/src/components/Event/EventActionSection.vue b/js/src/components/Event/EventActionSection.vue index 4330ec750..7a5dff456 100644 --- a/js/src/components/Event/EventActionSection.vue +++ b/js/src/components/Event/EventActionSection.vue @@ -648,7 +648,7 @@ const reportEvent = async ( if (!organizer.value) return; createReportMutation({ - eventId: event.value?.id ?? "", + eventsIds: [event.value?.id ?? ""], reportedId: organizer.value?.id ?? "", content, forward, diff --git a/js/src/composition/apollo/report.ts b/js/src/composition/apollo/report.ts index 37ecaece6..adfdf8a48 100644 --- a/js/src/composition/apollo/report.ts +++ b/js/src/composition/apollo/report.ts @@ -5,7 +5,7 @@ export function useCreateReport() { return useMutation< { createReport: { id: string } }, { - eventId?: string; + eventsIds?: string[]; reportedId: string; content?: string; commentsIds?: string[]; diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 0c4c0c3b6..d623b6393 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -20,7 +20,7 @@ export const REPORTS = gql` ...ActorFragment suspended } - event { + events { id uuid title @@ -46,7 +46,7 @@ const REPORT_FRAGMENT = gql` reporter { ...ActorFragment } - event { + events { id uuid title @@ -97,14 +97,14 @@ export const REPORT = gql` export const CREATE_REPORT = gql` mutation CreateReport( - $eventId: ID + $eventsIds: [ID] $reportedId: ID! $content: String $commentsIds: [ID] $forward: Boolean ) { createReport( - eventId: $eventId + eventsIds: $eventsIds reportedId: $reportedId content: $content commentsIds: $commentsIds diff --git a/js/src/types/report.model.ts b/js/src/types/report.model.ts index dd54158a3..ff79b36af 100644 --- a/js/src/types/report.model.ts +++ b/js/src/types/report.model.ts @@ -16,7 +16,7 @@ export interface IReport extends IActionLogObject { id: string; reported: IActor; reporter: IPerson; - event?: IEvent; + events?: IEvent[]; comments: IComment[]; content: string; notes: IReportNote[]; diff --git a/js/src/views/Moderation/ReportView.vue b/js/src/views/Moderation/ReportView.vue index 221d88824..efcd8bed0 100644 --- a/js/src/views/Moderation/ReportView.vue +++ b/js/src/views/Moderation/ReportView.vue @@ -150,14 +150,14 @@ {{ t("Unknown") }} - - {{ t("Event") }} + @@ -206,17 +206,23 @@

{{ t("Reported content") }}

- - {{ t("Delete") }} +
+ + {{ t("Delete") }} +
{ const dialog = inject("dialog"); -const confirmEventDelete = (): void => { +const confirmEventDelete = (event: IEvent): void => { dialog?.confirm({ title: t("Deleting event"), message: t( @@ -428,7 +435,7 @@ const confirmEventDelete = (): void => { confirmText: t("Delete Event"), variant: "danger", hasIcon: true, - onConfirm: () => deleteEvent(), + onConfirm: () => deleteEvent(event), }); }; @@ -451,8 +458,8 @@ const { onError: deleteEventMutationError, } = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT); -deleteEventMutationDone(() => { - const eventTitle = report.value?.event?.title; +deleteEventMutationDone((result) => { + const eventTitle = result?.context?.eventTitle; notifier?.success( t("Event {eventTitle} deleted", { eventTitle, @@ -464,10 +471,13 @@ deleteEventMutationError((error) => { console.error(error); }); -const deleteEvent = async (): Promise => { - if (!report.value?.event?.id) return; +const deleteEvent = async (event: IEvent): Promise => { + if (!event?.id) return; - deleteEventMutation({ eventId: report.value.event.id }); + deleteEventMutation( + { eventId: event.id }, + { context: { eventTitle: event.title } } + ); }; const { diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex index ddcfd34ac..9f6c94ac9 100644 --- a/lib/federation/activity_pub/types/reports.ex +++ b/lib/federation/activity_pub/types/reports.ex @@ -2,7 +2,6 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do @moduledoc false alias Mobilizon.{Actors, Discussions, Events, Reports} alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Reports.Report @@ -26,15 +25,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do %Actor{} = reported_actor = Actors.get_actor!(args.reported_id) content = HTML.strip_tags(args.content) - event_id = Map.get(args, :event_id) - - event = - if is_nil(event_id) do - nil - else - {:ok, %Event{} = event} = Events.get_event(event_id) - event - end + events = + args + |> Map.get(:events_ids, []) + |> Enum.map(fn event_id -> + case Events.get_event(event_id) do + {:ok, event} -> event + {:error, :event_not_found} -> nil + end + end) + |> Enum.filter(& &1) comments = Discussions.list_comments_by_actor_and_ids( @@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do reporter: reporter_actor, reported: reported_actor, content: content, - event: event, + events: events, comments: comments }) end diff --git a/lib/federation/activity_stream/converter/flag.ex b/lib/federation/activity_stream/converter/flag.ex index 4ff34f678..347ee8d00 100644 --- a/lib/federation/activity_stream/converter/flag.ex +++ b/lib/federation/activity_stream/converter/flag.ex @@ -9,11 +9,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Discussions - alias Mobilizon.Events + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Reports.Report + alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} @@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do "uri" => params["uri"], "content" => params["content"], "reported_id" => params["reported"].id, - "event_id" => (!is_nil(params["event"]) && params["event"].id) || nil, + "events" => params["events"], "comments" => params["comments"] } end @@ -50,9 +50,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do @impl Converter @spec model_to_as(Report.t()) :: map def model_to_as(%Report{} = report) do - object = [report.reported.url] ++ Enum.map(report.comments, fn comment -> comment.url end) - - object = if report.event, do: object ++ [report.event.url], else: object + object = + [report.reported.url] ++ + Enum.map(report.comments, fn comment -> comment.url end) ++ + Enum.map(report.events, & &1.url) %{ "type" => "Flag", @@ -68,14 +69,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do with {:ok, %Actor{} = reporter} <- ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]), %Actor{} = reported <- find_reported(objects), - event <- find_event(objects), - comments <- find_comments(objects, reported, event) do + %{events: events, comments: comments} <- find_events_and_comments(objects) do %{ "reporter" => reporter, "uri" => object["id"], "content" => object["content"], "reported" => reported, - "event" => event, + "events" => events, "comments" => comments } end @@ -94,26 +94,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do end) end - # Remove the reported actor and the event from the object list. - @spec find_comments(list(String.t()), Actor.t() | nil, Event.t() | nil) :: list(Comment.t()) - defp find_comments(objects, reported, event) do + defp find_events_and_comments(objects) do objects - |> Enum.filter(fn url -> - !((!is_nil(reported) && url == reported.url) || (!is_nil(event) && event.url == url)) - end) - |> Enum.map(&Discussions.get_comment_from_url/1) - |> Enum.filter(& &1) - end + |> Enum.map(&ActivityPub.fetch_object_from_url/1) + |> Enum.reduce(%{comments: [], events: []}, fn res, acc -> + case res do + {:ok, %Event{} = event} -> + Map.put(acc, :events, [event | acc.events]) - @spec find_event(list(String.t())) :: Event.t() | nil - defp find_event(objects) do - Enum.reduce_while(objects, nil, fn url, _ -> - case Events.get_event_by_url(url) do - %Event{} = event -> - {:halt, event} + {:ok, %Comment{} = comment} -> + Map.put(acc, :comments, [comment | acc.comments]) _ -> - {:cont, nil} + acc end end) end diff --git a/lib/graphql/resolvers/report.ex b/lib/graphql/resolvers/report.ex index e8c99453b..fc7112dd9 100644 --- a/lib/graphql/resolvers/report.ex +++ b/lib/graphql/resolvers/report.ex @@ -67,7 +67,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do {:ok, _, %Report{} = report} -> {:ok, report} - _error -> + error -> {:error, dgettext("errors", "Error while saving report")} end end diff --git a/lib/graphql/schema/report.ex b/lib/graphql/schema/report.ex index 43451776e..8d57c67c8 100644 --- a/lib/graphql/schema/report.ex +++ b/lib/graphql/schema/report.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do field(:uri, :string, description: "The URI of the report", meta: [private: true]) field(:reported, :actor, description: "The actor that is being reported") field(:reporter, :actor, description: "The actor that created the report") - field(:event, :event, description: "The event that is being reported") + field(:events, list_of(:event), description: "The event that is being reported") field(:comments, list_of(:comment), description: "The comments that are reported") field(:notes, list_of(:report_note), @@ -100,11 +100,15 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do field :create_report, type: :report do arg(:content, :string, description: "The message sent with the report") arg(:reported_id, non_null(:id), description: "The actor's ID that is being reported") - arg(:event_id, :id, default_value: nil, description: "The event ID that is being reported") + + arg(:events_ids, list_of(:id), + default_value: [], + description: "The list of event IDs that are being reported" + ) arg(:comments_ids, list_of(:id), default_value: [], - description: "The comment ID that is being reported" + description: "The comment IDs that are being reported" ) arg(:forward, :boolean, diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 11a9a3701..931898695 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -22,13 +22,13 @@ defmodule Mobilizon.Reports.Report do reported: Actor.t(), reporter: Actor.t(), manager: Actor.t(), - event: Event.t(), + events: [Event.t()], comments: [Comment.t()], notes: [Note.t()] } @required_attrs [:url, :reported_id, :reporter_id] - @optional_attrs [:content, :status, :manager_id, :event_id, :local] + @optional_attrs [:content, :status, :manager_id, :local] @attrs @required_attrs ++ @optional_attrs @timestamps_opts [type: :utc_datetime] @@ -46,8 +46,8 @@ defmodule Mobilizon.Reports.Report do belongs_to(:reporter, Actor) # The actor who last acted on this report belongs_to(:manager, Actor) - # The eventual Event inside the report - belongs_to(:event, Event) + # The eventual Events inside the report + many_to_many(:events, Event, join_through: "reports_events", on_replace: :delete) # The eventual Comments inside the report many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete) # The notes associated to the report @@ -62,6 +62,7 @@ defmodule Mobilizon.Reports.Report do report |> cast(attrs, @attrs) |> maybe_generate_url() + |> maybe_put_events(attrs) |> maybe_put_comments(attrs) |> validate_required(@required_attrs) end @@ -72,6 +73,12 @@ defmodule Mobilizon.Reports.Report do defp maybe_put_comments(%Ecto.Changeset{} = changeset, _), do: changeset + defp maybe_put_events(%Ecto.Changeset{} = changeset, %{events: events}) do + put_assoc(changeset, :events, events) + end + + defp maybe_put_events(%Ecto.Changeset{} = changeset, _), do: changeset + @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp maybe_generate_url(%Ecto.Changeset{} = changeset) do with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url), diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex index 36da1cb67..5e407b10c 100644 --- a/lib/mobilizon/reports/reports.ex +++ b/lib/mobilizon/reports/reports.ex @@ -21,7 +21,7 @@ defmodule Mobilizon.Reports do def get_report(id) do Report |> Repo.get(id) - |> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes]) + |> Repo.preload([:reported, :reporter, :manager, :events, :comments, :notes]) end @doc """ @@ -33,7 +33,7 @@ defmodule Mobilizon.Reports do %Report{} |> Report.changeset(attrs) |> Repo.insert() do - {:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])} + {:ok, Repo.preload(report, [:events, :reported, :reporter, :comments])} end end @@ -102,7 +102,7 @@ defmodule Mobilizon.Reports do @spec list_reports_query(atom()) :: Ecto.Query.t() defp list_reports_query(status) do Report - |> preload([:reported, :reporter, :manager, :event, :comments, :notes]) + |> preload([:reported, :reporter, :manager, :events, :comments, :notes]) |> where([r], r.status == ^status) end diff --git a/lib/service/anti_spam/akismet.ex b/lib/service/anti_spam/akismet.ex index f17946e6c..7c1bf98e0 100644 --- a/lib/service/anti_spam/akismet.ex +++ b/lib/service/anti_spam/akismet.ex @@ -139,7 +139,7 @@ defmodule Mobilizon.Service.AntiSpam.Akismet do end end - defp report_to_akismet_comment(%Report{event: %Event{id: event_id}}) do + defp report_to_akismet_comment(%Report{events: [%Event{id: event_id} | _]}) do with %Event{description: body, organizer_actor: %Actor{} = actor} <- Events.get_event_with_preload!(event_id), {email, preferred_username, ip} <- actor_details(actor) do diff --git a/lib/web/templates/email/report.html.heex b/lib/web/templates/email/report.html.heex index 48c5a62e6..9dedeeb8c 100644 --- a/lib/web/templates/email/report.html.heex +++ b/lib/web/templates/email/report.html.heex @@ -104,7 +104,7 @@ <% end %> - <%= if Map.has_key?(@report, :event) and @report.event do %> + <%= if Map.has_key?(@report, :events) and length(@report.events) > 0 do %>

-

<%= gettext("Event") %>

- - <%= gettext("%{title} by %{creator}", - title: @report.event.title, - creator: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reported) - ) %> - +

<%= gettext("Flagged events") %>

+ <%= for event <- @report.events do %> + + <%= gettext("%{title} by %{creator}", + title: event.title, + creator: + Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reported) + ) %> + + <% end %>

<% end %> <% end %> -<%= if Map.has_key?(@report, :event) and @report.event do %> - <%= gettext "Event" %> - <%= @report.event.title %> +<%= if Map.has_key?(@report, :event) && length(@report.events) > 0 do %> + <%= gettext "Events" %> + <%= for event <- @report.events do %> + <%= event.title %> + <% end %> <% end %> <%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %> <%= gettext "Comments" %> diff --git a/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs b/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs new file mode 100644 index 000000000..dc1c9ba08 --- /dev/null +++ b/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs @@ -0,0 +1,14 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AllowMultipleEventsToBeReported do + use Ecto.Migration + + def up do + create table(:reports_events, primary_key: false) do + add(:report_id, references(:reports, on_delete: :delete_all), null: false) + add(:event_id, references(:events, on_delete: :delete_all), null: false) + end + end + + def down do + drop table(:reports_events) + end +end diff --git a/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs b/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs new file mode 100644 index 000000000..a9bdfc995 --- /dev/null +++ b/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs @@ -0,0 +1,31 @@ +defmodule Mobilizon.Storage.Repo.Migrations.BackfillReportEventsWithOldEvents do + use Ecto.Migration + + def up do + process_reports_with_events() + end + + def down do + IO.puts("Doing nothing, migration can't be reverted") + end + + defp process_reports_with_events do + %Postgrex.Result{rows: rows} = + Ecto.Adapters.SQL.query!( + Mobilizon.Storage.Repo, + "SELECT id, event_id FROM reports WHERE event_id IS NOT NULL" + ) + + Enum.map(rows, &migrate_event_row/1) + end + + defp migrate_event_row([report_id, event_id]) when not is_nil(event_id) do + Ecto.Adapters.SQL.query!( + Mobilizon.Storage.Repo, + "INSERT INTO reports_events VALUES ($1, $2)", + [report_id, event_id] + ) + end + + defp migrate_event_row(_), do: :ok +end diff --git a/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs b/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs new file mode 100644 index 000000000..1f3294a4d --- /dev/null +++ b/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs @@ -0,0 +1,15 @@ +defmodule Mobilizon.Storage.Repo.Migrations.RemoveObsoleteEventIdOnReports do + use Ecto.Migration + + def up do + alter table(:reports) do + remove_if_exists :event_id, :integer + end + end + + def down do + alter table(:reports) do + add(:event_id, references(:events, on_delete: :delete_all), null: true) + end + end +end diff --git a/test/graphql/api/report_test.exs b/test/graphql/api/report_test.exs index 1960f09bf..85ddec7cd 100644 --- a/test/graphql/api/report_test.exs +++ b/test/graphql/api/report_test.exs @@ -30,7 +30,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: event_id, + events_ids: [event_id], comments_ids: [], forward: false }) @@ -64,7 +64,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: nil, + events_ids: [], comments_ids: [comment_1_id, comment_2_id] }) @@ -100,7 +100,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: nil, + events_ids: [], comments_ids: [comment_1_id, comment_2_id], forward: true }) @@ -131,7 +131,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: "This is not a nice thing", - event_id: nil, + events_ids: [], comments_ids: [comment_1_id], forward: true }) @@ -157,7 +157,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: "This is not a nice thing", - event_id: nil, + events_ids: [], comments_ids: [comment_1_id], forward: true }) diff --git a/test/graphql/resolvers/report_test.exs b/test/graphql/resolvers/report_test.exs index 30ab06996..5773bbcb0 100644 --- a/test/graphql/resolvers/report_test.exs +++ b/test/graphql/resolvers/report_test.exs @@ -15,17 +15,17 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do describe "Resolver: Report a content" do @create_report_mutation """ - mutation CreateReport($reportedId: ID!, $eventId: ID, $content: String) { + mutation CreateReport($reportedId: ID!, $eventsIds: [ID], $content: String) { createReport( reportedId: $reportedId, - eventId: $eventId, + eventsIds: $eventsIds, content: $content ) { content, reporter { id }, - event { + events { id }, status @@ -55,7 +55,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do query: @create_report_mutation, variables: %{ reportedId: reported.id, - eventId: event.id, + eventsIds: [event.id], content: "This is an issue" } ) @@ -63,7 +63,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do assert res["errors"] == nil assert res["data"]["createReport"]["content"] == "This is an issue" assert res["data"]["createReport"]["status"] == "OPEN" - assert res["data"]["createReport"]["event"]["id"] == to_string(event.id) + assert res["data"]["createReport"]["events"] |> hd |> Map.get("id") == to_string(event.id) assert res["data"]["createReport"]["reporter"]["id"] == to_string(reporter.id) @@ -122,7 +122,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter { id }, - event { + events { id }, status @@ -280,7 +280,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter { preferredUsername }, - event { + events { title }, comments { @@ -312,7 +312,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter.preferred_username assert json_response(res, 200)["data"]["report"]["content"] == report.content - assert json_response(res, 200)["data"]["report"]["event"]["title"] == report.event.title + + assert json_response(res, 200)["data"]["report"]["events"] |> hd |> Map.get("title") == + report.events |> hd |> Map.get(:title) assert json_response(res, 200)["data"]["report"]["comments"] |> hd |> Map.get("text") == report.comments |> hd |> Map.get(:text) diff --git a/test/support/factory.ex b/test/support/factory.ex index 699f834e8..7ca138b34 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -322,7 +322,7 @@ defmodule Mobilizon.Factory do url: "http://mobilizon.test/report/deae1020-54b8-47df-9eea-d8c0e943e57f/activity", reported: build(:actor), reporter: build(:actor), - event: build(:event), + events: build_list(1, :event), comments: build_list(1, :comment) } end From b105c508c03ce3cb96dd8342f96d3291aa197e22 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 31 Aug 2023 14:37:54 +0200 Subject: [PATCH 03/10] feat(reports): improve reportview and allow removing content + resolve report automatically Signed-off-by: Thomas Citharel --- js/src/components/Comment/EventComment.vue | 23 +- js/src/graphql/report.ts | 8 + js/src/i18n/en_US.json | 16 +- js/src/i18n/fr_FR.json | 14 +- js/src/views/Moderation/ReportView.vue | 256 +++++++++++++------ lib/federation/activity_pub/types/reports.ex | 5 +- lib/graphql/resolvers/report.ex | 2 +- 7 files changed, 230 insertions(+), 94 deletions(-) diff --git a/js/src/components/Comment/EventComment.vue b/js/src/components/Comment/EventComment.vue index 7219fb2f1..64d87c765 100644 --- a/js/src/components/Comment/EventComment.vue +++ b/js/src/components/Comment/EventComment.vue @@ -4,7 +4,7 @@ :class="{ reply: comment.inReplyToComment, 'bg-mbz-purple-50 dark:bg-mbz-purple-500': comment.isAnnouncement, - 'bg-mbz-bluegreen-50 dark:bg-mbz-bluegreen-600': commentSelected, + '!bg-mbz-bluegreen-50 dark:!bg-mbz-bluegreen-600': commentSelected, 'shadow-none': !rootComment, }" > @@ -62,6 +62,7 @@ class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1" v-if=" currentActor?.id && + !readOnly && event.options.commentModeration !== CommentModeration.CLOSED && !comment.deletedAt " @@ -70,7 +71,7 @@ {{ t("Reply") }} - +