Add ability to add message for participation and improve participation
management interface Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
130a3cf23f
commit
c732ec7f87
|
@ -16,8 +16,9 @@ A minimal file template [is available](https://framagit.org/framasoft/mobilizon/
|
|||
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/master/support/systemd/mobilizon.service).
|
||||
|
||||
### Added
|
||||
- Possibility to participate anonymously to an event
|
||||
- Possibility to participate to an event without an account
|
||||
- Possibility to participate to a remote event (being redirected by providing federated identity)
|
||||
- Possibility to add a note as a participant when event participation is manually validated (required when participating without an account)
|
||||
- Possibility to change email address for the account
|
||||
- Possibility to delete your account
|
||||
|
||||
|
@ -26,6 +27,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
|
|||
- Signature validation also now checks if `Date` header has acceptable values
|
||||
- Actor profiles are now stale after two days and have to be refetched
|
||||
- Actor keys are rotated some time after sending a `Delete` activity
|
||||
- Improved event participations managing interface
|
||||
|
||||
### Fixed
|
||||
- Fixed URL search
|
||||
|
|
|
@ -22,6 +22,7 @@ Supported Activity | Supported Object
|
|||
`Delete` | `Object`
|
||||
`Flag` | `Object`
|
||||
`Follow` | `Object`
|
||||
`Join` | `Event`
|
||||
`Reject` | `Follow`, `Join`
|
||||
`Remove` | `Note`, `Event`
|
||||
`Undo` | `Announce`, `Follow`
|
||||
|
@ -156,3 +157,27 @@ We add [an `address` property](https://schema.org/address), which we assume to b
|
|||
"type": "Event"
|
||||
}
|
||||
```
|
||||
|
||||
### Join
|
||||
|
||||
#### participationMessage
|
||||
|
||||
We add a `participationMessage` property on a `Join` activity so that participants may transmit a note to event organizers, to motivate their participation when event participations are manually approved. This field is restricted to plain text.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Join",
|
||||
"object": "http://mobilizon.test/events/some-uuid",
|
||||
"id": "http://mobilizon2.test/@admin/join/event/1",
|
||||
"actor": "http://mobilizon2.test/@admin",
|
||||
"participationMessage": "I want to join !",
|
||||
"@context": [
|
||||
{
|
||||
"participationMessage": {
|
||||
"@id": "mz:participationMessage",
|
||||
"@type": "sc:Text"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
|
@ -6,7 +6,6 @@
|
|||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {EventJoinOptions} from "@/types/event.model";
|
||||
<docs>
|
||||
A button to set your participation
|
||||
|
||||
|
@ -80,7 +81,7 @@ A button to set your participation
|
|||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.name || `@${currentActor.preferredUsername}` }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
|
@ -96,14 +97,13 @@ A button to set your participation
|
|||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventJoinOptions, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { FETCH_EVENT } from '@/graphql/event';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
|
@ -132,8 +132,12 @@ export default class ParticipationButton extends Vue {
|
|||
RouteName = RouteName;
|
||||
|
||||
joinEvent(actor: IPerson) {
|
||||
if (this.event.joinOptions === EventJoinOptions.RESTRICTED) {
|
||||
this.$emit('joinEventWithConfirmation', actor);
|
||||
} else {
|
||||
this.$emit('joinEvent', actor);
|
||||
}
|
||||
}
|
||||
|
||||
joinModal() {
|
||||
this.$emit('joinModal');
|
||||
|
|
164
js/src/components/Event/ParticipationTable.vue
Normal file
164
js/src/components/Event/ParticipationTable.vue
Normal file
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<b-table
|
||||
:data="data"
|
||||
ref="queueTable"
|
||||
detailed
|
||||
detail-key="id"
|
||||
:checked-rows.sync="checkedRows"
|
||||
checkable
|
||||
:is-row-checkable="row => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
default-sort="insertedAt"
|
||||
default-sort-direction="asc"
|
||||
:show-detail-icon="false"
|
||||
:loading="this.$apollo.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="total"
|
||||
:per-page="perPage"
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="page => $emit('page-change', page)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" sortable>
|
||||
<b-tag type="is-success" class="has-text-centered">{{ props.row.insertedAt | formatDateString }}<br>{{ props.row.insertedAt | formatTimeString }}</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" sortable v-if="showRole">
|
||||
<span v-if="props.row.role === ParticipantRole.CREATOR">
|
||||
{{ $t('Organizer') }}
|
||||
</span>
|
||||
<span v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ $t('Participant') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<b-table-column field="actor.preferredUsername" :label="$t('Participant')" sortable>
|
||||
<article class="media">
|
||||
<figure class="media-left" v-if="props.row.actor.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img :src="props.row.actor.avatar.url" alt="">
|
||||
</p>
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else-if="props.row.actor.preferredUsername === 'anonymous'" size="is-large" icon="incognito" />
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span><br />
|
||||
<span class="is-size-7 has-text-grey">@{{ props.row.actor.preferredUsername }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('Anonymous participant') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="metadata.message" :label="$t('Message')">
|
||||
<span @click="toggleQueueDetails(props.row)" :class="{ 'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH }" v-if="props.row.metadata && props.row.metadata.message">
|
||||
{{ props.row.metadata.message | ellipsize }}
|
||||
</span>
|
||||
<span v-else class="has-text-grey">
|
||||
{{ $t('No message') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article v-html="nl2br(props.row.metadata.message)" />
|
||||
</template>
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button @click="acceptParticipants(checkedRows)" type="is-success" v-if="canAcceptParticipants">
|
||||
{{ $tc('No participant to approve|Approve participant|Approve {number} participants', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
<b-button @click="refuseParticipants(checkedRows)" type="is-danger" v-if="canRefuseParticipants">
|
||||
{{ $tc('No participant to reject|Reject participant|Reject {number} participants', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { Refs } from '@/shims-vue';
|
||||
import { nl2br } from '@/utils/html';
|
||||
import { asyncForEach } from '@/utils/asyncForEach';
|
||||
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||
},
|
||||
})
|
||||
export default class ParticipationTable extends Vue {
|
||||
@Prop({ required: true, type: Array }) data!: IParticipant[];
|
||||
@Prop({ required: true, type: Number }) total!: number;
|
||||
@Prop({ required: true, type: Function }) acceptParticipant;
|
||||
@Prop({ required: true, type: Function }) refuseParticipant;
|
||||
@Prop({ required: false, type: Boolean, default: false }) showRole;
|
||||
@Prop({ required: false, type: Number, default: 20 }) perPage;
|
||||
checkedRows: IParticipant[] = [];
|
||||
|
||||
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
|
||||
nl2br = nl2br;
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
$refs!: Refs<{
|
||||
queueTable: any,
|
||||
}>;
|
||||
|
||||
toggleQueueDetails(row: IParticipant) {
|
||||
if (row.metadata.message && row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH) return;
|
||||
this.$refs.queueTable.toggleDetails(row);
|
||||
}
|
||||
|
||||
async acceptParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.acceptParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async refuseParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.refuseParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* We can accept participants if at least one of them is not approved
|
||||
*/
|
||||
get canAcceptParticipants(): boolean {
|
||||
return this.checkedRows.some(
|
||||
(participant: IParticipant) => [ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* We can refuse participants if at least one of them is something different than not approved
|
||||
*/
|
||||
get canRefuseParticipants(): boolean {
|
||||
return this.checkedRows.some((participant: IParticipant) => participant.role !== ParticipantRole.REJECTED);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ellipsed-message {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table {
|
||||
span.tag {
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -29,7 +29,7 @@
|
|||
<figure class="image is-32x32" v-if="identity.avatar">
|
||||
<img class="is-rounded" :src="identity.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else icon="account-circle" />
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</div>
|
||||
|
||||
<div class="media-content">
|
||||
|
@ -180,6 +180,10 @@ nav {
|
|||
background: $secondary;
|
||||
}
|
||||
|
||||
span.icon.is-medium {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 2.5em;
|
||||
}
|
||||
|
|
|
@ -7,18 +7,24 @@
|
|||
<b-message type="is-info">{{ $t("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.") }}</b-message>
|
||||
<b-message type="is-danger" v-if="error">{{ error }}</b-message>
|
||||
<b-field :label="$t('Email')">
|
||||
<b-field>
|
||||
<b-input
|
||||
type="email"
|
||||
v-model="anonymousParticipation.email"
|
||||
placeholder="Your email"
|
||||
required>
|
||||
</b-input>
|
||||
<p class="control">
|
||||
</b-field>
|
||||
<p v-if="event.joinOptions === EventJoinOptions.RESTRICTED">{{ $t("The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.") }}</p>
|
||||
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p>
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
v-model="anonymousParticipation.message"
|
||||
minlength="10"
|
||||
:required="event.joinOptions === EventJoinOptions.RESTRICTED">
|
||||
</b-input>
|
||||
</b-field>
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Send email') }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
<div class="has-text-centered">
|
||||
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
|
||||
{{ $t('Back to previous page') }}
|
||||
|
@ -31,7 +37,7 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole, EventJoinOptions } from '@/types/event.model';
|
||||
import { FETCH_EVENT, JOIN_EVENT } from '@/graphql/event';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
|
@ -55,10 +61,11 @@ import { RouteName } from '@/router';
|
|||
})
|
||||
export default class ParticipationWithoutAccount extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
anonymousParticipation: { email: String } = { email: '' };
|
||||
anonymousParticipation: { email: String, message: String } = { email: '', message: '' };
|
||||
event!: IEvent;
|
||||
config!: IConfig;
|
||||
error: String|boolean = false;
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
|
||||
async joinEvent() {
|
||||
this.error = false;
|
||||
|
@ -69,6 +76,7 @@ export default class ParticipationWithoutAccount extends Vue {
|
|||
eventId: this.event.id,
|
||||
actorId: this.config.anonymous.actorId,
|
||||
email: this.anonymousParticipation.email,
|
||||
message: this.anonymousParticipation.message,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
|
|
|
@ -2,6 +2,8 @@ import gql from 'graphql-tag';
|
|||
import { COMMENT_FIELDS_FRAGMENT } from '@/graphql/comment';
|
||||
|
||||
const participantQuery = `
|
||||
total,
|
||||
elements {
|
||||
role,
|
||||
id,
|
||||
actor {
|
||||
|
@ -18,7 +20,10 @@ const participantQuery = `
|
|||
uuid
|
||||
},
|
||||
metadata {
|
||||
cancellationToken
|
||||
cancellationToken,
|
||||
message
|
||||
},
|
||||
insertedAt
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -371,11 +376,12 @@ export const EDIT_EVENT = gql`
|
|||
`;
|
||||
|
||||
export const JOIN_EVENT = gql`
|
||||
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String) {
|
||||
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String, $message: String) {
|
||||
joinEvent(
|
||||
eventId: $eventId,
|
||||
actorId: $actorId,
|
||||
email: $email
|
||||
email: $email,
|
||||
message: $message
|
||||
) {
|
||||
${participantQuery}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
"Allow all comments": "Allow all comments",
|
||||
"Allow registrations": "Allow registrations",
|
||||
"An error has occurred.": "An error has occurred.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "And no anonymous participations|And one anonymous participation|And {count} anonymous participations",
|
||||
"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 participations": "Anonymous participations",
|
||||
"Approve": "Approve",
|
||||
|
@ -467,5 +466,17 @@
|
|||
"{count} requests waiting": "{count} requests waiting",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.",
|
||||
"If you want, you may send a message to the event organizer here.": "If you want, you may send a message to the event organizer here.",
|
||||
"Message": "Message",
|
||||
"Anonymous participant": "Anonymous participant",
|
||||
"No message": "No message",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "No participant to approve|Approve participant|Approve {number} participants",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "No participant to reject|Reject participant|Reject {number} participants",
|
||||
"Role": "Role",
|
||||
"Participant": "Participant",
|
||||
"Participation confirmation": "Participation confirmation",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?",
|
||||
"Confirm my participation": "Confirm my participation"
|
||||
}
|
|
@ -23,7 +23,6 @@
|
|||
"Allow all comments": "Permitir todos los comentarios",
|
||||
"Allow registrations": "Permitir registros",
|
||||
"An error has occurred.": "Se ha producido un error.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Y sin participaciones anónimas|Y una participación anónima|Y {count} participaciones anónimas",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Los participantes anónimos deberán confirmar su participación por correo electrónico.",
|
||||
"Anonymous participations": "Participaciones anónimas",
|
||||
"Approve": "Aprobar",
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"Allow all comments": "Salli kaikki kommentit",
|
||||
"Allow registrations": "Salli rekisteröityminen",
|
||||
"An error has occurred.": "Tapahtui virhe.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Ei anonyymejä osallistujia|Myös yksi anonyymi osallistuja|Myös {count} anonyymiä osallistujaa",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonyymejä osallistujia pyydetään vahvistamaan osallistumisensa sähköpostitse.",
|
||||
"Anonymous participations": "Anonyymit osallistujat",
|
||||
"Approve": "Hyväksy",
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
"Allow all comments": "Autoriser tous les commentaires",
|
||||
"Allow registrations": "Autoriser les inscriptions",
|
||||
"An error has occurred.": "Une erreur est survenue.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Et aucune participation anonyme|Et une participation anonyme|Et {count} participations anonymes",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.",
|
||||
"Anonymous participations": "Participations anonymes",
|
||||
"Approve": "Approuver",
|
||||
|
@ -252,14 +251,14 @@
|
|||
"Or": "Ou",
|
||||
"Organized": "Organisés",
|
||||
"Organized by {name}": "Organisé par {name}",
|
||||
"Organizer": "Organisateur",
|
||||
"Organizer": "Organisateur⋅ice",
|
||||
"Other software may also support this.": "D'autres logiciels peuvent également supporter cette fonctionnalité.",
|
||||
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
|
||||
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
||||
"Page not found": "Page non trouvée",
|
||||
"Participant already was rejected.": "Le participant a déjà été refusé.",
|
||||
"Participant has already been approved as participant.": "Le participant a déjà été approuvé en tant que participant.",
|
||||
"Participants": "Participants",
|
||||
"Participants": "Participant⋅e⋅s",
|
||||
"Participate": "Participer",
|
||||
"Participate using your email address": "Participer en utilisant votre adresse email",
|
||||
"Participation approval": "Validation des participations",
|
||||
|
@ -473,5 +472,17 @@
|
|||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "L'organisateur⋅ice de l'événement valide les participations manuellement. Comme vous avez choisi de participer sans compte, merci d'expliquer pourquoi vous voulez participer à cet événement.",
|
||||
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur⋅ice de l'événement ci-dessous.",
|
||||
"Message": "Message",
|
||||
"Anonymous participant": "Participant⋅e anonyme",
|
||||
"No message": "Pas de message",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "Aucun⋅e participant⋅e à valider|Valider le ou la participant⋅e|Valider {number} participant⋅es",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "Aucun⋅e participant⋅e à refuser|Refuser le ou la participant⋅e|Refuser {number} participant⋅es",
|
||||
"Role": "Rôle",
|
||||
"Participant": "Participant⋅e",
|
||||
"Participation confirmation": "Confirmation de votre participation",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "L'organisateur⋅ice de l'événement a choisi de valider manuellement les demandes de participation. Voulez-vous ajouter un petit message pour expliquer pourquoi vous souhaitez participer à cet événement ?",
|
||||
"Confirm my participation": "Confirmer ma participation"
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
"Allow all comments": "Autorizar totes los comentaris",
|
||||
"Allow registrations": "Permetre las inscripcions",
|
||||
"An error has occurred.": "Una error s’es producha.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "E cap de participacion anonima|E una participacion anonima|E {count} participacions anonima",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Òm demandarà als participants anonims de confirmar lor venguda via un corrièl.",
|
||||
"Anonymous participations": "Participacions anonimas",
|
||||
"Approve": "Aprovar",
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"Allow all comments": "Permitir todos comentários",
|
||||
"Allow registrations": "Permitir inscrições",
|
||||
"An error has occurred.": "Ocorreu um erro.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "E nenhuma participação anônima|E uma participação anônima|E {count} participações anônimas",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Os participantes anônimos deverão confirmar sua participação por email.",
|
||||
"Anonymous participations": "Participações anônimas",
|
||||
"Approve": "Aprovar",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import { ColorModifiers } from 'buefy/types/helpers';
|
||||
import { Route, RawLocation } from 'vue-router';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
|
@ -8,6 +9,23 @@ declare module 'vue/types/vue' {
|
|||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
};
|
||||
beforeRouteEnter?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
|
||||
beforeRouteLeave?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
|
||||
beforeRouteUpdate?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ export const eventRoutes: RouteConfig[] = [
|
|||
props: { isUpdate: true },
|
||||
},
|
||||
{
|
||||
path: '/events/participations/:eventId',
|
||||
path: '/events/:eventId/participations',
|
||||
name: EventRouteName.PARTICIPATIONS,
|
||||
component: participations,
|
||||
meta: { requiredAuth: true },
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Address, IAddress } from '@/types/address.model';
|
|||
import { ITag } from '@/types/tag.model';
|
||||
import { IPicture } from '@/types/picture.model';
|
||||
import { IComment } from '@/types/comment.model';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
|
||||
export enum EventStatus {
|
||||
TENTATIVE = 'TENTATIVE',
|
||||
|
@ -59,7 +60,8 @@ export interface IParticipant {
|
|||
role: ParticipantRole;
|
||||
actor: IActor;
|
||||
event: IEvent;
|
||||
metadata: { cancellationToken?: string };
|
||||
metadata: { cancellationToken?: string, message?: string };
|
||||
insertedAt?: Date;
|
||||
}
|
||||
|
||||
export class Participant implements IParticipant {
|
||||
|
@ -68,6 +70,7 @@ export class Participant implements IParticipant {
|
|||
actor!: IActor;
|
||||
role: ParticipantRole = ParticipantRole.NOT_APPROVED;
|
||||
metadata = {};
|
||||
insertedAt?: Date;
|
||||
|
||||
constructor(hash?: IParticipant) {
|
||||
if (!hash) return;
|
||||
|
@ -77,6 +80,7 @@ export class Participant implements IParticipant {
|
|||
this.actor = new Actor(hash.actor);
|
||||
this.role = hash.role;
|
||||
this.metadata = hash.metadata;
|
||||
this.insertedAt = hash.insertedAt;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,7 +136,7 @@ export interface IEvent {
|
|||
organizerActor?: IActor;
|
||||
attributedTo: IActor;
|
||||
participantStats: IEventParticipantStats;
|
||||
participants: IParticipant[];
|
||||
participants: Paginate<IParticipant>;
|
||||
|
||||
relatedEvents: IEvent[];
|
||||
comments: IComment[];
|
||||
|
@ -205,7 +209,7 @@ export class EventModel implements IEvent {
|
|||
publishAt = new Date();
|
||||
|
||||
participantStats = { notApproved: 0, notConfirmed: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
|
||||
participants: IParticipant[] = [];
|
||||
participants!: Paginate<IParticipant>;
|
||||
|
||||
relatedEvents: IEvent[] = [];
|
||||
comments: IComment[] = [];
|
||||
|
|
7
js/src/utils/asyncForEach.ts
Normal file
7
js/src/utils/asyncForEach.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index += 1) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
export { asyncForEach };
|
|
@ -45,6 +45,7 @@
|
|||
:current-actor="currentActor"
|
||||
@joinEvent="joinEvent"
|
||||
@joinModal="isJoinModalActive = true"
|
||||
@joinEventWithConfirmation="joinEventWithConfirmation"
|
||||
@confirmLeave="confirmLeave"
|
||||
/>
|
||||
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
|
||||
|
@ -263,13 +264,44 @@
|
|||
<button
|
||||
class="button is-primary"
|
||||
ref="confirmButton"
|
||||
@click="joinEvent(identity)">
|
||||
@click="event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity)">
|
||||
{{ $t('Confirm my particpation') }}
|
||||
</button>
|
||||
</footer>
|
||||
</template>
|
||||
</identity-picker>
|
||||
</b-modal>
|
||||
<b-modal :active.sync="isJoinConfirmationModalActive" has-modal-card ref="joinConfirmationModal">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t('Participation confirmation')}}</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p>{{ $t('The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?') }}</p>
|
||||
<form @submit.prevent="joinEvent(actorForConfirmation, messageForConfirmation)">
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
size="is-medium"
|
||||
v-model="messageForConfirmation"
|
||||
minlength="10">
|
||||
</b-input>
|
||||
</b-field>
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
native-type="button"
|
||||
class="button"
|
||||
ref="cancelButton"
|
||||
@click="isJoinConfirmationModalActive = false">
|
||||
{{ $t('Cancel') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Confirm my participation') }}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
@ -281,11 +313,10 @@ import {
|
|||
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
||||
FETCH_EVENT,
|
||||
JOIN_EVENT,
|
||||
LEAVE_EVENT,
|
||||
} from '@/graphql/event';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole, EventJoinOptions } from '@/types/event.model';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
|
||||
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
||||
|
@ -398,12 +429,16 @@ export default class Event extends EventMixin {
|
|||
showMap: boolean = false;
|
||||
isReportModalActive: boolean = false;
|
||||
isJoinModalActive: boolean = false;
|
||||
isJoinConfirmationModalActive: boolean = false;
|
||||
EventVisibility = EventVisibility;
|
||||
EventStatus = EventStatus;
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
RouteName = RouteName;
|
||||
observer!: IntersectionObserver;
|
||||
loadComments: boolean = false;
|
||||
anonymousParticipation: boolean|null = null;
|
||||
actorForConfirmation!: IPerson;
|
||||
messageForConfirmation: string = '';
|
||||
|
||||
get eventTitle() {
|
||||
if (!this.event) return undefined;
|
||||
|
@ -506,7 +541,13 @@ export default class Event extends EventMixin {
|
|||
}
|
||||
}
|
||||
|
||||
async joinEvent(identity: IPerson) {
|
||||
joinEventWithConfirmation(actor: IPerson) {
|
||||
this.isJoinConfirmationModalActive = true;
|
||||
this.actorForConfirmation = actor;
|
||||
}
|
||||
|
||||
async joinEvent(identity: IPerson, message: string|null = null) {
|
||||
this.isJoinConfirmationModalActive = false;
|
||||
this.isJoinModalActive = false;
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
||||
|
@ -514,6 +555,7 @@ export default class Event extends EventMixin {
|
|||
variables: {
|
||||
eventId: this.event.id,
|
||||
actorId: identity.id,
|
||||
message,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {ParticipantRole} from "@/types/event.model";
|
||||
<template>
|
||||
<main class="container">
|
||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
||||
|
@ -7,22 +8,17 @@
|
|||
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="participantsAndCreators.length > 0">
|
||||
<section v-if="participants && participants.total > 0">
|
||||
<h2 class="title">{{ $t('Participants') }}</h2>
|
||||
<p v-if="confirmedAnonymousParticipantsCountCount > 1">
|
||||
{{ $tc('And no anonymous participations|And one anonymous participation|And {count} anonymous participations', confirmedAnonymousParticipantsCountCount, { count: confirmedAnonymousParticipantsCountCount}) }}
|
||||
</p>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in participantsAndCreators" :key="participant.actor.id">
|
||||
<participant-card
|
||||
v-if="participant.actor.id !== config.anonymous.actorId"
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
<ParticipationTable
|
||||
:data="participants.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:showRole="true"
|
||||
:total="participants.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => participantPage = page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
|
@ -32,18 +28,16 @@
|
|||
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="queue.length > 0">
|
||||
<section v-if="queue && queue.total > 0">
|
||||
<h2 class="title">{{ $t('Waiting list') }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in queue" :key="participant.actor.id">
|
||||
<participant-card
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
<ParticipationTable
|
||||
:data="queue.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="queue.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => queuePage = page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
|
@ -53,18 +47,16 @@
|
|||
<span>{{ $t('Rejected')}} <b-tag rounded> {{ participantStats.rejected }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="rejected.length > 0">
|
||||
<section v-if="rejected && rejected.total > 0">
|
||||
<h2 class="title">{{ $t('Rejected participations') }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in rejected" :key="participant.actor.id">
|
||||
<participant-card
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
<ParticipationTable
|
||||
:data="rejected.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="rejected.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => rejectedPage = page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
|
@ -81,9 +73,15 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
|||
import { IPerson } from '@/types/actor';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import ParticipationTable from '@/components/Event/ParticipationTable.vue';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 20;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ParticipationTable,
|
||||
ParticipantCard,
|
||||
},
|
||||
apollo: {
|
||||
|
@ -97,7 +95,7 @@ import { IConfig } from '@/types/config.model';
|
|||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.PARTICIPANT].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
|
@ -106,18 +104,18 @@ import { IConfig } from '@/types/config.model';
|
|||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
organizers: {
|
||||
participants: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
roles: [ParticipantRole.CREATOR].join(),
|
||||
page: this.participantPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.CREATOR, ParticipantRole.PARTICIPANT].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
|
@ -127,13 +125,13 @@ import { IConfig } from '@/types/config.model';
|
|||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
page: this.queuePage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.NOT_APPROVED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
|
@ -143,27 +141,36 @@ import { IConfig } from '@/types/config.model';
|
|||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
page: this.rejectedPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.REJECTED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||
},
|
||||
})
|
||||
export default class Participants extends Vue {
|
||||
@Prop({ required: true }) eventId!: string;
|
||||
page: number = 1;
|
||||
limit: number = 10;
|
||||
|
||||
organizers: IParticipant[] = [];
|
||||
queue: IParticipant[] = [];
|
||||
rejected: IParticipant[] = [];
|
||||
participants!: Paginate<IParticipant>;
|
||||
participantPage: number = 1;
|
||||
|
||||
queue!: Paginate<IParticipant>;
|
||||
queuePage: number = 1;
|
||||
|
||||
rejected!: Paginate<IParticipant>;
|
||||
rejectedPage: number = 1;
|
||||
|
||||
event!: IEvent;
|
||||
config!: IConfig;
|
||||
|
||||
|
@ -173,23 +180,20 @@ export default class Participants extends Vue {
|
|||
hasMoreParticipants: boolean = false;
|
||||
activeTab: number = 0;
|
||||
|
||||
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
dataTransform(data): Paginate<Participant> {
|
||||
return {
|
||||
total: data.event.participants.total,
|
||||
elements: data.event.participants.elements.map(participation => new Participant(participation)),
|
||||
};
|
||||
}
|
||||
|
||||
get participantStats(): IEventParticipantStats | null {
|
||||
if (!this.event) return null;
|
||||
return this.event.participantStats;
|
||||
}
|
||||
|
||||
get participantsAndCreators(): IParticipant[] {
|
||||
if (this.event) {
|
||||
return [...this.organizers, ...this.event.participants]
|
||||
.filter(participant => [ParticipantRole.PARTICIPANT, ParticipantRole.CREATOR].includes(participant.role));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
get confirmedAnonymousParticipantsCountCount(): number {
|
||||
return this.participantsAndCreators.filter(({ actor: { id } }) => id === this.config.anonymous.actorId).length;
|
||||
}
|
||||
|
||||
@Watch('participantStats', { deep: true })
|
||||
watchParticipantStats(stats: IEventParticipantStats) {
|
||||
if (!stats) return;
|
||||
|
@ -232,8 +236,8 @@ export default class Participants extends Vue {
|
|||
},
|
||||
});
|
||||
if (data) {
|
||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.rejected = this.rejected.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.rejected.elements = this.rejected.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participantStats.going += 1;
|
||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
|
@ -242,7 +246,7 @@ export default class Participants extends Vue {
|
|||
this.event.participantStats.rejected -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.PARTICIPANT;
|
||||
this.event.participants.push(participant);
|
||||
this.event.participants.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@ -260,8 +264,10 @@ export default class Participants extends Vue {
|
|||
},
|
||||
});
|
||||
if (data) {
|
||||
this.event.participants = this.event.participants.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participants.elements = this.event.participants.elements.filter(
|
||||
participant => participant.id !== data.updateParticipation.id,
|
||||
);
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participantStats.rejected += 1;
|
||||
if (participant.role === ParticipantRole.PARTICIPANT) {
|
||||
this.event.participantStats.participant -= 1;
|
||||
|
@ -271,8 +277,8 @@ export default class Participants extends Vue {
|
|||
this.event.participantStats.notApproved -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.REJECTED;
|
||||
this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id);
|
||||
this.rejected.push(participant);
|
||||
this.rejected.elements = this.rejected.elements.filter(participantIn => participantIn.id !== participant.id);
|
||||
this.rejected.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -439,7 +439,10 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata: Map.get(additional, :metadata)
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HtmlSanitizeEx.strip_tags(&1)))
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
|
|
|
@ -306,13 +306,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data
|
||||
%{
|
||||
"type" => "Join",
|
||||
"object" => object,
|
||||
"actor" => _actor,
|
||||
"id" => id,
|
||||
"participationMessage" => note
|
||||
} = data
|
||||
) do
|
||||
with actor <- Utils.get_actor(data),
|
||||
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.join(object, actor, false, %{url: id}) do
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.join(object, actor, false, %{url: id, metadata: %{message: note}}) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
|
|
|
@ -77,6 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
"anonymousParticipationEnabled" => %{
|
||||
"@id" => "mz:anonymousParticipationEnabled",
|
||||
"@type" => "sc:Boolean"
|
||||
},
|
||||
"participationMessage" => %{
|
||||
"@id" => "mz:participationMessage",
|
||||
"@type" => "sc:Text"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -25,7 +25,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Participant do
|
|||
"type" => "Join",
|
||||
"id" => participant.url,
|
||||
"actor" => participant.actor.url,
|
||||
"object" => participant.event.url
|
||||
"object" => participant.event.url,
|
||||
"participationMessage" => Map.get(participant.metadata, :message)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,7 +102,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
end
|
||||
|
||||
def list_participants_for_event(_, _args, _resolution) do
|
||||
{:ok, []}
|
||||
{:ok, %{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
|
||||
|
|
|
@ -17,12 +17,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
"""
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{actor_id: actor_id, event_id: event_id} = args,
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
case User.owns_actor(user, actor_id) do
|
||||
{:is_owned, %Actor{} = actor} ->
|
||||
do_actor_join_event(actor, event_id)
|
||||
do_actor_join_event(actor, event_id, args)
|
||||
|
||||
_ ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
@ -136,7 +136,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id, token: token},
|
||||
_resolution
|
||||
) do
|
||||
)
|
||||
when not is_nil(token) do
|
||||
with {:anonymous_participation_enabled, true} <-
|
||||
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
||||
{:anonymous_actor_id, true} <-
|
||||
|
|
|
@ -65,7 +65,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
|||
|
||||
field(:participant_stats, :participant_stats)
|
||||
|
||||
field(:participants, list_of(:participant), description: "The event's participants") do
|
||||
field(:participants, :paginated_participant_list, description: "The event's participants") do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
arg(:roles, :string, default_value: "")
|
||||
|
|
|
@ -33,12 +33,21 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
|||
field(:metadata, :participant_metadata,
|
||||
description: "The metadata associated to this participant"
|
||||
)
|
||||
|
||||
field(:inserted_at, :datetime, description: "The datetime this participant was created")
|
||||
end
|
||||
|
||||
object :participant_metadata do
|
||||
field(:cancellation_token, :string,
|
||||
description: "The eventual token to leave an event when user is anonymous"
|
||||
)
|
||||
|
||||
field(:message, :string, description: "The eventual message the participant left")
|
||||
end
|
||||
|
||||
object :paginated_participant_list do
|
||||
field(:elements, list_of(:participant), description: "A list of participants")
|
||||
field(:total, :integer, description: "The total number of participants in the list")
|
||||
end
|
||||
|
||||
enum :participant_role_enum do
|
||||
|
@ -64,6 +73,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
|||
arg(:event_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:email, :string)
|
||||
arg(:message, :string)
|
||||
|
||||
resolve(&Participant.actor_join_event/3)
|
||||
end
|
||||
|
|
|
@ -759,7 +759,7 @@ defmodule Mobilizon.Events do
|
|||
Default behaviour is to not return :not_approved or :not_confirmed participants
|
||||
"""
|
||||
@spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
|
||||
[Participant.t()]
|
||||
Page.t()
|
||||
def list_participants_for_event(
|
||||
id,
|
||||
roles \\ @default_participant_roles,
|
||||
|
@ -769,8 +769,7 @@ defmodule Mobilizon.Events do
|
|||
id
|
||||
|> list_participants_for_event_query()
|
||||
|> filter_role(roles)
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
||||
|
|
|
@ -18,11 +18,15 @@ defmodule Mobilizon.Events.Participant do
|
|||
role: ParticipantRole.t(),
|
||||
url: String.t(),
|
||||
event: Event.t(),
|
||||
actor: Actor.t()
|
||||
actor: Actor.t(),
|
||||
metadata: Map.t()
|
||||
}
|
||||
|
||||
@required_attrs [:url, :role, :event_id, :actor_id]
|
||||
@attrs @required_attrs
|
||||
@metadata_attrs [:email, :confirmation_token, :cancellation_token, :message]
|
||||
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "participants" do
|
||||
|
@ -33,6 +37,7 @@ defmodule Mobilizon.Events.Participant do
|
|||
field(:email, :string)
|
||||
field(:confirmation_token, :string)
|
||||
field(:cancellation_token, :string)
|
||||
field(:message, :string)
|
||||
end
|
||||
|
||||
belongs_to(:event, Event, primary_key: true)
|
||||
|
@ -70,7 +75,7 @@ defmodule Mobilizon.Events.Participant do
|
|||
|
||||
defp metadata_changeset(schema, params) do
|
||||
schema
|
||||
|> cast(params, [:email, :confirmation_token, :cancellation_token])
|
||||
|> cast(params, @metadata_attrs)
|
||||
|> Checker.validate_changeset()
|
||||
end
|
||||
|
||||
|
|
405
schema.graphql
405
schema.graphql
|
@ -1,5 +1,5 @@
|
|||
# source: http://localhost:4000/api
|
||||
# timestamp: Thu Feb 13 2020 11:32:20 GMT+0100 (GMT+01:00)
|
||||
# timestamp: Wed Mar 04 2020 10:26:53 GMT+0100 (GMT+01:00)
|
||||
|
||||
schema {
|
||||
query: RootQueryType
|
||||
|
@ -839,6 +839,9 @@ type Participant {
|
|||
"""The participation ID"""
|
||||
id: ID
|
||||
|
||||
"""The datetime this participant was created"""
|
||||
insertedAt: DateTime
|
||||
|
||||
"""The metadata associated to this participant"""
|
||||
metadata: ParticipantMetadata
|
||||
|
||||
|
@ -849,6 +852,9 @@ type Participant {
|
|||
type ParticipantMetadata {
|
||||
"""The eventual token to leave an event when user is anonymous"""
|
||||
cancellationToken: String
|
||||
|
||||
"""The eventual message the participant left"""
|
||||
message: String
|
||||
}
|
||||
|
||||
enum ParticipantRoleEnum {
|
||||
|
@ -1082,203 +1088,9 @@ enum ReportStatus {
|
|||
}
|
||||
|
||||
type RootMutationType {
|
||||
saveAdminSettings(instanceDescription: String, instanceName: String, instanceTerms: String, instanceTermsType: InstanceTermsType, instanceTermsUrl: String, registrationsOpen: Boolean): AdminSettings
|
||||
changeEmail(email: String!, password: String!): User
|
||||
|
||||
"""Create a comment"""
|
||||
createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment
|
||||
|
||||
"""Create an user"""
|
||||
createUser(email: String!, locale: String, password: String!): User
|
||||
|
||||
"""Update an identity"""
|
||||
updatePerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
id: ID!
|
||||
|
||||
"""The displayed name for this profile"""
|
||||
name: String
|
||||
|
||||
"""The summary for this profile"""
|
||||
summary: String
|
||||
): Person
|
||||
|
||||
"""Create an event"""
|
||||
createEvent(
|
||||
beginsOn: DateTime!
|
||||
category: String = "meeting"
|
||||
description: String!
|
||||
draft: Boolean = false
|
||||
endsOn: DateTime
|
||||
joinOptions: EventJoinOptions = FREE
|
||||
onlineAddress: String
|
||||
options: EventOptionsInput
|
||||
organizerActorId: ID!
|
||||
phoneAddress: String
|
||||
physicalAddress: AddressInput
|
||||
|
||||
"""
|
||||
The picture for the event, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
picture: PictureInput
|
||||
publishAt: DateTime
|
||||
status: EventStatus
|
||||
|
||||
"""The list of tags associated to the event"""
|
||||
tags: [String] = [""]
|
||||
title: String!
|
||||
visibility: EventVisibility = PUBLIC
|
||||
): Event
|
||||
validateEmail(token: String!): User
|
||||
|
||||
"""Delete an event"""
|
||||
deleteEvent(actorId: ID!, eventId: ID!): DeletedObject
|
||||
|
||||
"""Accept a participation"""
|
||||
updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant
|
||||
|
||||
"""Leave an event"""
|
||||
leaveEvent(actorId: ID!, eventId: ID!, token: String): DeletedParticipant
|
||||
|
||||
"""Delete an identity"""
|
||||
deletePerson(id: ID!): Person
|
||||
|
||||
"""Refresh a token"""
|
||||
refreshToken(refreshToken: String!): RefreshedToken
|
||||
|
||||
"""Validate an user after registration"""
|
||||
validateUser(token: String!): Login
|
||||
|
||||
"""Upload a picture"""
|
||||
uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture
|
||||
|
||||
"""Delete a feed token"""
|
||||
deleteFeedToken(token: String!): DeletedFeedToken
|
||||
|
||||
"""Create a note on a report"""
|
||||
createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote
|
||||
|
||||
"""Leave an event"""
|
||||
leaveGroup(actorId: ID!, groupId: ID!): DeletedMember
|
||||
|
||||
"""Create a Feed Token"""
|
||||
createFeedToken(actorId: ID): FeedToken
|
||||
|
||||
"""Send a link through email to reset user password"""
|
||||
sendResetPassword(email: String!, locale: String): String
|
||||
|
||||
"""Delete a relay subscription"""
|
||||
removeRelay(address: String!): Follower
|
||||
|
||||
"""Change default actor for user"""
|
||||
changeDefaultActor(preferredUsername: String!): User
|
||||
deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject
|
||||
|
||||
"""Create a report"""
|
||||
createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report
|
||||
|
||||
"""Register a first profile on registration"""
|
||||
registerPerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The email from the user previously created"""
|
||||
email: String!
|
||||
|
||||
"""The displayed name for the new profile"""
|
||||
name: String = ""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the new profile"""
|
||||
summary: String = ""
|
||||
): Person
|
||||
|
||||
"""Delete a group"""
|
||||
deleteGroup(actorId: ID!, groupId: ID!): DeletedObject
|
||||
deleteAccount(password: String!): DeletedObject
|
||||
|
||||
"""Add a relay subscription"""
|
||||
addRelay(address: String!): Follower
|
||||
|
||||
"""Reset user password"""
|
||||
resetPassword(locale: String = "en", password: String!, token: String!): Login
|
||||
|
||||
"""Create a group"""
|
||||
createGroup(
|
||||
"""
|
||||
The avatar for the group, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the group, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The identity that creates the group"""
|
||||
creatorActorId: ID!
|
||||
|
||||
"""The displayed name for the group"""
|
||||
name: String
|
||||
|
||||
"""The name for the group"""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the group"""
|
||||
summary: String = ""
|
||||
): Group
|
||||
|
||||
"""Confirm a participation"""
|
||||
confirmParticipation(confirmationToken: String!): Participant
|
||||
deleteComment(actorId: ID!, commentId: ID!): Comment
|
||||
|
||||
"""Join an event"""
|
||||
joinEvent(actorId: ID!, email: String, eventId: ID!): Participant
|
||||
|
||||
"""Accept a relay subscription"""
|
||||
acceptRelay(address: String!): Follower
|
||||
|
||||
"""Join a group"""
|
||||
joinGroup(actorId: ID!, groupId: ID!): Member
|
||||
|
||||
"""Reject a relay subscription"""
|
||||
rejectRelay(address: String!): Follower
|
||||
|
||||
"""Create a new person for user"""
|
||||
createPerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The displayed name for the new profile"""
|
||||
name: String = ""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the new profile"""
|
||||
summary: String = ""
|
||||
): Person
|
||||
|
||||
"""Update an event"""
|
||||
updateEvent(
|
||||
beginsOn: DateTime
|
||||
|
@ -1305,18 +1117,211 @@ type RootMutationType {
|
|||
title: String
|
||||
visibility: EventVisibility = PUBLIC
|
||||
): Event
|
||||
validateEmail(token: String!): User
|
||||
|
||||
"""Change default actor for user"""
|
||||
changeDefaultActor(preferredUsername: String!): User
|
||||
|
||||
"""Leave an event"""
|
||||
leaveEvent(actorId: ID!, eventId: ID!, token: String): DeletedParticipant
|
||||
deleteAccount(password: String!): DeletedObject
|
||||
|
||||
"""Delete a group"""
|
||||
deleteGroup(actorId: ID!, groupId: ID!): DeletedObject
|
||||
|
||||
"""Create an user"""
|
||||
createUser(email: String!, locale: String, password: String!): User
|
||||
|
||||
"""Add a relay subscription"""
|
||||
addRelay(address: String!): Follower
|
||||
|
||||
"""Delete an identity"""
|
||||
deletePerson(id: ID!): Person
|
||||
|
||||
"""Accept a relay subscription"""
|
||||
acceptRelay(address: String!): Follower
|
||||
|
||||
"""Refresh a token"""
|
||||
refreshToken(refreshToken: String!): RefreshedToken
|
||||
|
||||
"""Update an identity"""
|
||||
updatePerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
id: ID!
|
||||
|
||||
"""The displayed name for this profile"""
|
||||
name: String
|
||||
|
||||
"""The summary for this profile"""
|
||||
summary: String
|
||||
): Person
|
||||
|
||||
"""Create a report"""
|
||||
createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report
|
||||
|
||||
"""Delete a feed token"""
|
||||
deleteFeedToken(token: String!): DeletedFeedToken
|
||||
|
||||
"""Reset user password"""
|
||||
resetPassword(locale: String = "en", password: String!, token: String!): Login
|
||||
|
||||
"""Login an user"""
|
||||
login(email: String!, password: String!): Login
|
||||
|
||||
"""Leave an event"""
|
||||
leaveGroup(actorId: ID!, groupId: ID!): DeletedMember
|
||||
|
||||
"""Register a first profile on registration"""
|
||||
registerPerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The email from the user previously created"""
|
||||
email: String!
|
||||
|
||||
"""The displayed name for the new profile"""
|
||||
name: String = ""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the new profile"""
|
||||
summary: String = ""
|
||||
): Person
|
||||
|
||||
"""Delete a relay subscription"""
|
||||
removeRelay(address: String!): Follower
|
||||
|
||||
"""Change an user password"""
|
||||
changePassword(newPassword: String!, oldPassword: String!): User
|
||||
|
||||
"""Update a report"""
|
||||
updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report
|
||||
|
||||
"""Resend registration confirmation token"""
|
||||
resendConfirmationEmail(email: String!, locale: String): String
|
||||
|
||||
"""Login an user"""
|
||||
login(email: String!, password: String!): Login
|
||||
"""Confirm a participation"""
|
||||
confirmParticipation(confirmationToken: String!): Participant
|
||||
|
||||
"""Delete an event"""
|
||||
deleteEvent(actorId: ID!, eventId: ID!): DeletedObject
|
||||
|
||||
"""Update a report"""
|
||||
updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report
|
||||
|
||||
"""Create a group"""
|
||||
createGroup(
|
||||
"""
|
||||
The avatar for the group, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the group, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The identity that creates the group"""
|
||||
creatorActorId: ID!
|
||||
|
||||
"""The displayed name for the group"""
|
||||
name: String
|
||||
|
||||
"""The name for the group"""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the group"""
|
||||
summary: String = ""
|
||||
): Group
|
||||
|
||||
"""Validate an user after registration"""
|
||||
validateUser(token: String!): Login
|
||||
|
||||
"""Join an event"""
|
||||
joinEvent(actorId: ID!, email: String, eventId: ID!, message: String): Participant
|
||||
deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject
|
||||
deleteComment(actorId: ID!, commentId: ID!): Comment
|
||||
|
||||
"""Reject a relay subscription"""
|
||||
rejectRelay(address: String!): Follower
|
||||
|
||||
"""Create a comment"""
|
||||
createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment
|
||||
|
||||
"""Create a note on a report"""
|
||||
createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote
|
||||
|
||||
"""Accept a participation"""
|
||||
updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant
|
||||
|
||||
"""Create a Feed Token"""
|
||||
createFeedToken(actorId: ID): FeedToken
|
||||
|
||||
"""Join a group"""
|
||||
joinGroup(actorId: ID!, groupId: ID!): Member
|
||||
|
||||
"""Create a new person for user"""
|
||||
createPerson(
|
||||
"""
|
||||
The avatar for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
avatar: PictureInput
|
||||
|
||||
"""
|
||||
The banner for the profile, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
banner: PictureInput
|
||||
|
||||
"""The displayed name for the new profile"""
|
||||
name: String = ""
|
||||
preferredUsername: String!
|
||||
|
||||
"""The summary for the new profile"""
|
||||
summary: String = ""
|
||||
): Person
|
||||
|
||||
"""Upload a picture"""
|
||||
uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture
|
||||
|
||||
"""Create an event"""
|
||||
createEvent(
|
||||
beginsOn: DateTime!
|
||||
category: String = "meeting"
|
||||
description: String!
|
||||
draft: Boolean = false
|
||||
endsOn: DateTime
|
||||
joinOptions: EventJoinOptions = FREE
|
||||
onlineAddress: String
|
||||
options: EventOptionsInput
|
||||
organizerActorId: ID!
|
||||
phoneAddress: String
|
||||
physicalAddress: AddressInput
|
||||
|
||||
"""
|
||||
The picture for the event, either as an object or directly the ID of an existing Picture
|
||||
"""
|
||||
picture: PictureInput
|
||||
publishAt: DateTime
|
||||
status: EventStatus
|
||||
|
||||
"""The list of tags associated to the event"""
|
||||
tags: [String] = [""]
|
||||
title: String!
|
||||
visibility: EventVisibility = PUBLIC
|
||||
): Event
|
||||
saveAdminSettings(instanceDescription: String, instanceName: String, instanceTerms: String, instanceTermsType: InstanceTermsType, instanceTermsUrl: String, registrationsOpen: Boolean): AdminSettings
|
||||
changeEmail(email: String!, password: String!): User
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
|
@ -818,6 +818,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
|||
assert activity.data["cc"] == []
|
||||
end
|
||||
|
||||
@join_message "I want to get in!"
|
||||
test "it accepts Join activities" do
|
||||
%Actor{url: organizer_url} = organizer = insert(:actor)
|
||||
%Actor{url: participant_url} = _participant = insert(:actor)
|
||||
|
@ -829,13 +830,19 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
|||
|> Jason.decode!()
|
||||
|> Map.put("actor", participant_url)
|
||||
|> Map.put("object", event_url)
|
||||
|> Map.put("participationMessage", @join_message)
|
||||
|
||||
assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data)
|
||||
assert {:ok, activity, %Participant{} = participant} =
|
||||
Transmogrifier.handle_incoming(join_data)
|
||||
|
||||
assert participant.metadata.message == @join_message
|
||||
assert participant.role == :participant
|
||||
|
||||
assert activity.data["type"] == "Accept"
|
||||
assert activity.data["object"]["object"] == event_url
|
||||
assert activity.data["object"]["id"] =~ "/join/event/"
|
||||
assert activity.data["object"]["type"] =~ "Join"
|
||||
assert activity.data["object"]["participationMessage"] == @join_message
|
||||
assert activity.data["actor"] == organizer_url
|
||||
assert activity.data["id"] =~ "/accept/join/"
|
||||
end
|
||||
|
@ -894,6 +901,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
|||
# Organiser is not present since we use factories directly
|
||||
assert event.id
|
||||
|> Events.list_participants_for_event()
|
||||
|> Map.get(:elements)
|
||||
|> Enum.map(& &1.id) ==
|
||||
[]
|
||||
end
|
||||
|
@ -924,6 +932,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
|||
# The only participant left is the organizer
|
||||
assert event.id
|
||||
|> Events.list_participants_for_event()
|
||||
|> Map.get(:elements)
|
||||
|> Enum.map(& &1.id) ==
|
||||
[organizer_participation.id]
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
|
||||
alias Mobilizon.GraphQL.AbsintheHelpers
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Web.Email
|
||||
|
||||
import Mobilizon.Factory
|
||||
|
@ -446,6 +447,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
participants(roles: "participant,moderator,administrator,creator", actor_id: "#{
|
||||
actor.id
|
||||
}") {
|
||||
elements {
|
||||
role,
|
||||
actor {
|
||||
preferredUsername
|
||||
|
@ -453,6 +455,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
|
@ -462,7 +465,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
|
||||
assert json_response(res, 200)["errors"] == nil
|
||||
|
||||
assert json_response(res, 200)["data"]["event"]["participants"] == [
|
||||
assert json_response(res, 200)["data"]["event"]["participants"]["elements"] == [
|
||||
%{
|
||||
"actor" => %{
|
||||
"preferredUsername" => actor.preferred_username
|
||||
|
@ -485,6 +488,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
||||
actor.id
|
||||
}") {
|
||||
elements {
|
||||
role,
|
||||
actor {
|
||||
preferredUsername
|
||||
|
@ -492,6 +496,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
|
@ -500,7 +505,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
||||
|
||||
sorted_participants =
|
||||
json_response(res, 200)["data"]["event"]["participants"]
|
||||
json_response(res, 200)["data"]["event"]["participants"]["elements"]
|
||||
|> Enum.filter(&(&1["role"] == "PARTICIPANT"))
|
||||
|
||||
assert sorted_participants == [
|
||||
|
@ -518,6 +523,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
||||
actor.id
|
||||
}") {
|
||||
elements {
|
||||
role,
|
||||
actor {
|
||||
preferredUsername
|
||||
|
@ -525,6 +531,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
|
@ -533,7 +540,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
||||
|
||||
sorted_participants =
|
||||
json_response(res, 200)["data"]["event"]["participants"]
|
||||
json_response(res, 200)["data"]["event"]["participants"]["elements"]
|
||||
|> Enum.sort_by(
|
||||
&(&1
|
||||
|> Map.get("actor")
|
||||
|
@ -1053,7 +1060,8 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert res["data"]["joinEvent"]["event"]["id"] == to_string(event.id)
|
||||
assert res["data"]["joinEvent"]["actor"]["id"] == to_string(actor_id)
|
||||
|
||||
%Participant{} = participant = event.id |> Events.list_participants_for_event() |> hd
|
||||
%Participant{} =
|
||||
participant = event.id |> Events.list_participants_for_event() |> Map.get(:elements) |> hd
|
||||
|
||||
assert participant.metadata.email == @email
|
||||
|
||||
|
@ -1093,7 +1101,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert %Participant{
|
||||
metadata: %{confirmation_token: confirmation_token},
|
||||
role: :not_confirmed
|
||||
} = participant = event.id |> Events.list_participants_for_event([]) |> hd()
|
||||
} =
|
||||
participant =
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd()
|
||||
|
||||
# hack to avoid preloading event in participant
|
||||
participant = Map.put(participant, :event, event)
|
||||
|
@ -1118,7 +1128,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
)
|
||||
|
||||
assert %Participant{role: :participant} =
|
||||
event.id |> Events.list_participants_for_event() |> hd()
|
||||
event.id |> Events.list_participants_for_event() |> Map.get(:elements) |> hd()
|
||||
end
|
||||
|
||||
test "I can participate anonymously and and confirm my participation with bad token",
|
||||
|
@ -1140,7 +1150,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert res["data"]["joinEvent"]["event"]["id"] == to_string(event.id)
|
||||
assert res["data"]["joinEvent"]["actor"]["id"] == to_string(actor_id)
|
||||
|
||||
%Participant{} = participant = event.id |> Events.list_participants_for_event([]) |> hd
|
||||
%Participant{} =
|
||||
participant =
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd
|
||||
|
||||
assert participant.metadata.email == @email
|
||||
|
||||
|
@ -1157,7 +1169,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert hd(res["errors"])["message"] == "This token is invalid"
|
||||
|
||||
assert %Participant{role: :not_confirmed} =
|
||||
event.id |> Events.list_participants_for_event([]) |> hd()
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd()
|
||||
end
|
||||
|
||||
test "I can participate anonymously but change my mind and cancel my participation",
|
||||
|
@ -1181,7 +1193,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
|
||||
{:ok, %Event{participant_stats: %{not_confirmed: 1}}} = Events.get_event(event.id)
|
||||
|
||||
%Participant{} = participant = event.id |> Events.list_participants_for_event([]) |> hd
|
||||
%Participant{} =
|
||||
participant =
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd
|
||||
|
||||
assert participant.metadata.email == @email
|
||||
|
||||
|
@ -1205,7 +1219,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
id: participant_id,
|
||||
role: :not_confirmed,
|
||||
metadata: %{cancellation_token: cancellation_token}
|
||||
} = event.id |> Events.list_participants_for_event([]) |> hd()
|
||||
} = event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd()
|
||||
|
||||
res =
|
||||
conn
|
||||
|
@ -1221,7 +1235,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert res["data"]["leaveEvent"]["id"] == participant_id
|
||||
|
||||
{:ok, %Event{participant_stats: %{not_confirmed: 0}}} = Events.get_event(event.id)
|
||||
assert Events.list_participants_for_event(event.id, []) == []
|
||||
assert Events.list_participants_for_event(event.id, []) == %Page{elements: [], total: 0}
|
||||
end
|
||||
|
||||
test "I can participate anonymously, confirm my participation and then be confirmed by the organizer",
|
||||
|
@ -1274,7 +1288,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert %Participant{
|
||||
role: :not_confirmed,
|
||||
metadata: %{confirmation_token: confirmation_token, email: @email}
|
||||
} = event.id |> Events.list_participants_for_event([]) |> hd
|
||||
} = event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd
|
||||
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
|
@ -1293,7 +1307,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
}} = Events.get_event(event.id)
|
||||
|
||||
assert %Participant{role: :not_approved, id: participant_id} =
|
||||
event.id |> Events.list_participants_for_event([]) |> hd
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd
|
||||
|
||||
update_participation_mutation = """
|
||||
mutation UpdateParticipation($participantId: ID!, $role: String!, $moderatorActorId: ID!) {
|
||||
|
@ -1325,7 +1339,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
|||
assert res["errors"] == nil
|
||||
|
||||
assert %Participant{role: :participant} =
|
||||
event.id |> Events.list_participants_for_event([]) |> hd
|
||||
event.id |> Events.list_participants_for_event([]) |> Map.get(:elements) |> hd
|
||||
|
||||
assert {:ok,
|
||||
%Event{
|
||||
|
|
|
@ -110,8 +110,11 @@ defmodule Mobilizon.EventsTest do
|
|||
assert event.ends_on == DateTime.from_naive!(~N[2010-04-17 14:00:00Z], "Etc/UTC")
|
||||
assert event.title == "some title"
|
||||
|
||||
assert hd(Events.list_participants_for_event(event.id)).actor.id == actor.id
|
||||
assert hd(Events.list_participants_for_event(event.id)).role == :creator
|
||||
assert %Participant{} =
|
||||
participant = hd(Events.list_participants_for_event(event.id).elements)
|
||||
|
||||
assert participant.actor.id == actor.id
|
||||
assert participant.role == :creator
|
||||
end
|
||||
|
||||
test "create_event/1 with invalid data returns error changeset" do
|
||||
|
|
Loading…
Reference in a new issue