Merge branch 'more-fixes' into 'master'

Even more fixes

See merge request framasoft/mobilizon!472
This commit is contained in:
Thomas Citharel 2020-06-15 20:04:03 +02:00
commit 061f51447e
17 changed files with 213 additions and 67 deletions

View file

@ -7,31 +7,11 @@
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }">
<h2 class="title">{{ participation.event.title }}</h2>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<span>
<span>
{{
$t("Organized by {name}", {
name: participation.event.organizerActor.displayName(),
})
}}
</span>
<span v-if="participation.role === ParticipantRole.PARTICIPANT">
{{ $t("Going as {name}", { name: participation.actor.displayName() }) }}
</span>
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" />
<b-icon
icon="link"
@ -42,7 +22,42 @@
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
/>
</span>
<span class="column is-narrow participant-stats">
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<span>
<i18n tag="span" path="Organized by {name}">
<popover-actor-card
slot="name"
:actor="participation.event.organizerActor"
:inline="true"
>
{{ participation.event.organizerActor.displayName() }}
</popover-actor-card>
</i18n>
<i18n
v-if="participation.role === ParticipantRole.PARTICIPANT"
path="Going as {name}"
tag="span"
>
<popover-actor-card slot="name" :actor="participation.actor" :inline="true">
{{ participation.actor.displayName() }}
</popover-actor-card>
</i18n>
</span>
</div>
<div>
<span
class="participant-stats"
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
"
>
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
{{
$t("{approved} / {total} seats", {
@ -176,6 +191,7 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import EventMixin from "../../mixins/event";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/auth";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
const defaultOptions: IEventCardOptions = {
hideDate: true,
@ -187,6 +203,7 @@ const defaultOptions: IEventCardOptions = {
@Component({
components: {
DateCalendarIcon,
PopoverActorCard,
},
apollo: {
currentActor: {

View file

@ -206,6 +206,7 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
preferredUsername
name
domain
summary
avatar {
url
}
@ -226,6 +227,7 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
preferredUsername
name
domain
summary
avatar {
url
}

View file

@ -184,6 +184,11 @@ export const LOGS = gql`
domain
name
}
... on User {
id
email
confirmedAt
}
}
insertedAt
}

View file

@ -256,7 +256,7 @@
"On {date} from {startTime} to {endTime}": "On {date} from {startTime} to {endTime}",
"On {date} starting at {startTime}": "On {date} starting at {startTime}",
"On {date}": "On {date}",
"Only accessible through link and search (private)": "Only accessible through link and search (private)",
"Only accessible through link (private)": "Only accessible through link (private)",
"Only alphanumeric characters and underscores are supported.": "Only alphanumeric characters and underscores are supported.",
"Open": "Open",
"Opened reports": "Opened reports",
@ -581,12 +581,12 @@
"Every hour": "Every hour",
"Every day": "Every day",
"report #{report_number}": "report #{report_number}",
"{actor} closed {report}": "{actor} closed {report}",
"{moderator} closed {report}": "{moderator} closed {report}",
"a non-existent report": "a non-existent report",
"{actor} reopened {report}": "{actor} reopened {report}",
"{actor} marked {report} as resolved": "{actor} marked {report} as resolved",
"{actor} added a note on {report}": "{actor} added a note on {report}",
"{actor} deleted an event named \"{title}\"": "{actor} deleted an event named \"{title}\"",
"{moderator} reopened {report}": "{moderator} reopened {report}",
"{moderator} marked {report} as resolved": "{moderator} marked {report} as resolved",
"{moderator} added a note on {report}": "{moderator} added a note on {report}",
"{moderator} deleted an event named \"{title}\"": "{moderator} deleted an event named \"{title}\"",
"If the direction given by the development team does not suit you, you have the legal right to create your own version of the software, with your own governance choices.": "If the direction given by the development team does not suit you, you have the legal right to create your own version of the software, with your own governance choices.",
"change the world, one byte at a time": "change the world, one byte at a time",
"Concieved with care for humans": "Concieved with care for humans",
@ -623,7 +623,7 @@
"Participations": "Participations",
"Nothing to see here": "Nothing to see here",
"Not confirmed": "Not confirmed",
"{actor} suspended profile {profile}": "{actor} suspended profile {profile}",
"{moderator} suspended profile {profile}": "{moderator} suspended profile {profile}",
"Suspend": "Suspend",
"Unsuspend": "Unsuspend",
"None": "None",
@ -642,5 +642,7 @@
"I agree to the {instanceRules} and {termsOfService}": "I agree to the {instanceRules} and {termsOfService}",
"This email is already used.": "This email is already used.",
"Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.",
"more than 1360 contributors": "more than 1360 contributors"
"more than 1360 contributors": "more than 1360 contributors",
"{moderator} has unsuspended profile {profile}": "{moderator} has unsuspended profile {profile}",
"{moderator} has deleted user {user}": "{moderator} has deleted user {user}"
}

View file

@ -321,7 +321,7 @@
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Ongoing tasks": "Tâches en cours",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Only accessible through link (private)": "Uniquement accessible par lien (privé)",
"Only alphanumeric characters and underscores are supported.": "Seuls les caractères alphanumériques et les tirets bas sont acceptés.",
"Open": "Ouvert",
"Opened reports": "Signalements ouverts",
@ -603,12 +603,12 @@
"Every hour": "À chaque heure",
"Every day": "Chaque jour",
"report #{report_number}": "le signalement #{report_number}",
"{actor} closed {report}": "{actor} a fermé {report}",
"{moderator} closed {report}": "{moderator} a fermé {report}",
"a non-existent report": "un signalement non-existant",
"{actor} reopened {report}": "{actor} a réouvert {report}",
"{actor} marked {report} as resolved": "{actor} a marqué {report} comme résolu",
"{actor} added a note on {report}": "{actor} a ajouté une note sur {report}",
"{actor} deleted an event named \"{title}\"": "{actor} a supprimé un événement nommé \"{title}\"",
"{moderator} reopened {report}": "{moderator} a réouvert {report}",
"{moderator} marked {report} as resolved": "{moderator} a marqué {report} comme résolu",
"{moderator} added a note on {report}": "{moderator} a ajouté une note sur {report}",
"{moderator} deleted an event named \"{title}\"": "{moderator} a supprimé un événement nommé \"{title}\"",
"If the direction given by the development team does not suit you, you have the legal right to create your own version of the software, with your own governance choices.": "Si la direction donnée par léquipe de développement ne vous convient pas, vous avez légalement le droit de créer votre version du logiciel avec vos propres choix de gouvernance.",
"change the world, one byte at a time": "changer le monde, un octet à la fois",
"Concieved with care for humans": "Conçu avec soin pour les humains",
@ -621,7 +621,7 @@
"Mobilizon is under development, we will add new features to this site during regular updates, until the release of <b>version 1 of the software in the fall of 2020</b>.": "Mobilizon est en cours de développement, nous ajouterons de nouvelles fonctionnalités à ce site lors de mises à jour régulières, jusqu'à la publication de <b>la version 1 du logiciel à l'automne 2020</b>.",
"To activate more notifications, head over to the notification settings.": "Pour activer plus de notifications, rendez-vous dans vos paramètres de notification.",
"Manage my notifications": "Gérer mes notifications",
"We use your timezone to make sure you get notifications for an event at the correct time.": "Nous utilisons votre fuseau hoaire pour nous assurer que vous recevez les notifications pour un événement au bon moment.",
"We use your timezone to make sure you get notifications for an event at the correct time.": "Nous utilisons votre fuseau horaire pour nous assurer que vous recevez les notifications pour un événement au bon moment.",
"Your timezone was detected as {timezone}.": "Votre fuseau horaire a été détecté en tant que {timezone}.",
"Manage my settings": "Gérer mes paramètres",
"Let's define a few settings": "Définissons quelques paramètres",
@ -646,7 +646,7 @@
"Participations": "Participations",
"Nothing to see here": "Il n'y a rien à voir ici",
"Not confirmed": "Non confirmé·e",
"{actor} suspended profile {profile}": "{actor} a suspendu le profil {profile}",
"{moderator} suspended profile {profile}": "{moderator} a suspendu le profil {profile}",
"Suspend": "Suspendre",
"Unsuspend": "Annuler la suspension",
"None": "Aucun",
@ -665,5 +665,7 @@
"I agree to the {instanceRules} and {termsOfService}": "J'accepte les {instanceRules} et les {termsOfService}",
"This email is already used.": "Cette adresse email est déjà utilisée.",
"Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Propulsé par {mobilizon}. © 2018 - {date} Les contributeur·ices Mobilizon - Fait avec le soutien financier de {contributors}.",
"more than 1360 contributors": "plus de 1360 contributeur·ices"
"more than 1360 contributors": "plus de 1360 contributeur·ices",
"{moderator} has unsuspended profile {profile}": "{moderator} a annulé la suspension de {profile}",
"{moderator} has deleted user {user}": "{moderator} a supprimé l'utilisateur·ice {user}"
}

View file

@ -41,6 +41,7 @@ export enum ActionLogAction {
COMMENT_DELETION = "COMMENT_DELETION",
ACTOR_SUSPENSION = "ACTOR_SUSPENSION",
ACTOR_UNSUSPENSION = "ACTOR_UNSUSPENSION",
USER_DELETION = "USER_DELETION",
}
export interface IActionLog {

View file

@ -35,7 +35,12 @@
</router-link>
</b-table-column>
<b-table-column field="confirmedAt" :label="$t('Confirmed at')" :centered="true">
{{ props.row.confirmedAt | formatDateTimeString }}
<template v-if="props.row.confirmedAt">
{{ props.row.confirmedAt | formatDateTimeString }}
</template>
<template v-else>
{{ $t("Not confirmed") }}
</template>
</b-table-column>
<b-table-column field="locale" :label="$t('Language')" :centered="true">
{{ props.row.locale }}

View file

@ -92,7 +92,7 @@
v-model="event.visibility"
name="eventVisibility"
:native-value="EventVisibility.UNLISTED"
>{{ $t("Only accessible through link and search (private)") }}</b-radio
>{{ $t("Only accessible through link (private)") }}</b-radio
>
</div>
<!-- <div class="field">

View file

@ -7,10 +7,10 @@
<i18n
v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED"
tag="span"
path="{actor} closed {report}"
path="{moderator} closed {report}"
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -23,10 +23,10 @@
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED"
tag="span"
path="{actor} reopened {report}"
path="{moderator} reopened {report}"
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -39,10 +39,10 @@
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED"
tag="span"
path="{actor} marked {report} as resolved"
path="{moderator} marked {report} as resolved"
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -55,10 +55,10 @@
<i18n
v-else-if="log.action === ActionLogAction.NOTE_CREATION"
tag="span"
path="{actor} added a note on {report}"
path="{moderator} added a note on {report}"
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -73,10 +73,10 @@
<i18n
v-else-if="log.action === ActionLogAction.EVENT_DELETION"
tag="span"
path='{actor} deleted an event named "{title}"'
path='{moderator} deleted an event named "{title}"'
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -85,10 +85,10 @@
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_SUSPENSION"
tag="span"
path="{actor} suspended profile {profile}"
path="{moderator} suspended profile {profile}"
>
<router-link
slot="actor"
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
@ -98,6 +98,40 @@
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_UNSUSPENSION"
tag="span"
path="{moderator} has unsuspended profile {profile}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.object.id } }"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.USER_DELETION"
tag="span"
path="{moderator} has deleted user {user}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
v-if="log.object.confirmedAt"
slot="user"
:to="{ name: RouteName.ADMIN_USER_PROFILE, params: { id: log.object.id } }"
>{{ log.object.email }}
</router-link>
<b v-else slot="user">{{ log.object.email }}</b>
</i18n>
<br />
<small>{{ log.insertedAt | formatDateTimeString }}</small>
</div>

View file

@ -111,6 +111,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
}
end
defp transform_action_log(User, :delete, %ActionLog{changes: changes}) do
%{
action: :user_deletion,
object: convert_changes_to_struct(User, changes)
}
end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),

View file

@ -110,6 +110,29 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, stats.participant + stats.moderator + stats.administrator + stats.creator}
end
def stats_participants(
%Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event,
_args,
%{context: %{current_user: %User{id: user_id} = _user}} = _resolution
) do
if Events.is_user_moderator_for_event?(user_id, event_id) do
stats =
Map.put(
stats,
:going,
stats.participant + stats.moderator + stats.administrator + stats.creator
)
{:ok, stats}
else
{:ok, %EventParticipantStats{}}
end
end
def stats_participants(_event, _args, _resolution) do
{:ok, %EventParticipantStats{}}
end
@doc """
List related events
"""

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Config, Events, Users}
alias Mobilizon.{Actors, Admin, Config, Events, Users}
alias Mobilizon.Actors.Actor
alias Mobilizon.Crypto
alias Mobilizon.Federation.ActivityPub
@ -390,11 +390,20 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end
def delete_account(_parent, %{user_id: user_id}, %{
context: %{current_user: %User{role: role}}
context: %{current_user: %User{role: role} = moderator_user}
})
when is_moderator(role) do
with %User{} = user <- Users.get_user(user_id) do
do_delete_account(%User{} = user)
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(moderator_user)},
%User{disabled: false} = user <- Users.get_user(user_id),
{:ok, %User{}} <- do_delete_account(%User{} = user) do
Admin.log_action(moderator_actor, "delete", user)
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
%User{disabled: true} ->
{:error, "User already disabled"}
end
end

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.Resolvers.Admin
@ -32,6 +33,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
value(:event_update)
value(:actor_suspension)
value(:actor_unsuspension)
value(:user_deletion)
end
@desc "The objects that can be in an action log"
@ -54,6 +56,9 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
%Actor{type: "Person"}, _ ->
:person
%User{}, _ ->
:user
_, _ ->
nil
end)

View file

@ -63,7 +63,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
field(:draft, :boolean, description: "Whether or not the event is a draft")
field(:participant_stats, :participant_stats)
field(:participant_stats, :participant_stats,
description: "Statistics on the event",
resolve: &Event.stats_participants/3
)
field(:participants, :paginated_participant_list, description: "The event's participants") do
arg(:page, :integer, default_value: 1)
@ -121,11 +124,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
end
object :participant_stats do
field(:going, :integer,
description: "The number of approved participants",
resolve: &Event.stats_participants_going/3
)
field(:going, :integer, description: "The number of approved participants")
field(:not_approved, :integer, description: "The number of not approved participants")
field(:not_confirmed, :integer, description: "The number of not confirmed participants")
field(:rejected, :integer, description: "The number of rejected participants")

View file

@ -15,7 +15,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "A local user of Mobilizon"
object :user do
field(:id, non_null(:id), description: "The user's ID")
interfaces([:action_log_object])
field(:id, :id, description: "The user's ID")
field(:email, non_null(:string), description: "The user's email")
field(:actors, non_null(list_of(:person)),

View file

@ -425,6 +425,16 @@ defmodule Mobilizon.Events do
|> Repo.all()
end
@spec is_user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
def is_user_moderator_for_event?(user_id, event_id) do
Participant
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|> where([p, _a], p.event_id == ^event_id)
|> where([_p, a], a.user_id == ^user_id)
|> where([p, _a], p.role == ^:creator)
|> Repo.exists?()
end
@doc """
Finds close events to coordinates.
Radius is in meters and defaults to 50km.
@ -741,7 +751,8 @@ defmodule Mobilizon.Events do
|> Repo.one()
end
@default_participant_roles [:participant, :moderator, :administrator, :creator]
@moderator_roles [:moderator, :administrator, :creator]
@default_participant_roles [:participant] ++ @moderator_roles
@doc """
Returns the list of participants for an event.
@ -810,7 +821,7 @@ defmodule Mobilizon.Events do
where:
p.event_id == ^event_id and
p.actor_id ==
^actor_id and p.role in ^[:moderator, :administrator, :creator]
^actor_id and p.role in ^@moderator_roles
)
) == nil)
end

View file

@ -554,7 +554,8 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
test "stats_participants_for_event/3 give the number of (un)approved participants", %{
conn: conn,
actor: actor
actor: actor,
user: user
} do
event =
@event
@ -577,6 +578,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid)
@ -623,12 +625,33 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid)
assert json_response(res, 200)["data"]["event"]["participantStats"]["going"] == 2
assert json_response(res, 200)["data"]["event"]["participantStats"]["notApproved"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 1
query = """
{
event(uuid: "#{event.uuid}") {
uuid,
participantStats {
going,
notApproved,
rejected
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert is_nil(json_response(res, 200)["errors"])
assert json_response(res, 200)["data"]["event"]["going"] == nil
end
end