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).
|
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
|
### 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 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 change email address for the account
|
||||||
- Possibility to delete your 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
|
- 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 profiles are now stale after two days and have to be refetched
|
||||||
- Actor keys are rotated some time after sending a `Delete` activity
|
- Actor keys are rotated some time after sending a `Delete` activity
|
||||||
|
- Improved event participations managing interface
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed URL search
|
- Fixed URL search
|
||||||
|
|
|
@ -22,6 +22,7 @@ Supported Activity | Supported Object
|
||||||
`Delete` | `Object`
|
`Delete` | `Object`
|
||||||
`Flag` | `Object`
|
`Flag` | `Object`
|
||||||
`Follow` | `Object`
|
`Follow` | `Object`
|
||||||
|
`Join` | `Event`
|
||||||
`Reject` | `Follow`, `Join`
|
`Reject` | `Follow`, `Join`
|
||||||
`Remove` | `Note`, `Event`
|
`Remove` | `Note`, `Event`
|
||||||
`Undo` | `Announce`, `Follow`
|
`Undo` | `Announce`, `Follow`
|
||||||
|
@ -156,3 +157,27 @@ We add [an `address` property](https://schema.org/address), which we assume to b
|
||||||
"type": "Event"
|
"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"
|
:loading="$apollo.queries.relayFollowers.loading"
|
||||||
ref="table"
|
ref="table"
|
||||||
:checked-rows.sync="checkedRows"
|
:checked-rows.sync="checkedRows"
|
||||||
:is-row-checkable="(row) => row.id !== 3"
|
|
||||||
detailed
|
detailed
|
||||||
:show-detail-icon="false"
|
:show-detail-icon="false"
|
||||||
paginated
|
paginated
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {EventJoinOptions} from "@/types/event.model";
|
||||||
<docs>
|
<docs>
|
||||||
A button to set your participation
|
A button to set your participation
|
||||||
|
|
||||||
|
@ -80,7 +81,7 @@ A button to set your participation
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
<span>{{ $t('as {identity}', {identity: currentActor.name || `@${currentActor.preferredUsername}` }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
|
@ -96,14 +97,13 @@ A button to set your participation
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
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 { IPerson, Person } from '@/types/actor';
|
||||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
|
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
|
||||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||||
import { CONFIG } from '@/graphql/config';
|
import { CONFIG } from '@/graphql/config';
|
||||||
import { IConfig } from '@/types/config.model';
|
import { IConfig } from '@/types/config.model';
|
||||||
import { RouteName } from '@/router';
|
import { RouteName } from '@/router';
|
||||||
import { FETCH_EVENT } from '@/graphql/event';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -132,7 +132,11 @@ export default class ParticipationButton extends Vue {
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
joinEvent(actor: IPerson) {
|
joinEvent(actor: IPerson) {
|
||||||
this.$emit('joinEvent', actor);
|
if (this.event.joinOptions === EventJoinOptions.RESTRICTED) {
|
||||||
|
this.$emit('joinEventWithConfirmation', actor);
|
||||||
|
} else {
|
||||||
|
this.$emit('joinEvent', actor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinModal() {
|
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">
|
<figure class="image is-32x32" v-if="identity.avatar">
|
||||||
<img class="is-rounded" :src="identity.avatar.url" alt="" />
|
<img class="is-rounded" :src="identity.avatar.url" alt="" />
|
||||||
</figure>
|
</figure>
|
||||||
<b-icon v-else icon="account-circle" />
|
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
|
@ -180,6 +180,10 @@ nav {
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.icon.is-medium {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-height: 2.5em;
|
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-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-message type="is-danger" v-if="error">{{ error }}</b-message>
|
||||||
<b-field :label="$t('Email')">
|
<b-field :label="$t('Email')">
|
||||||
<b-field>
|
<b-input
|
||||||
<b-input
|
type="email"
|
||||||
type="email"
|
v-model="anonymousParticipation.email"
|
||||||
v-model="anonymousParticipation.email"
|
placeholder="Your email"
|
||||||
placeholder="Your email"
|
required>
|
||||||
required>
|
</b-input>
|
||||||
</b-input>
|
|
||||||
<p class="control">
|
|
||||||
<b-button type="is-primary" native-type="submit">{{ $t('Send email') }}</b-button>
|
|
||||||
</p>
|
|
||||||
</b-field>
|
|
||||||
</b-field>
|
</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>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
|
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
|
||||||
{{ $t('Back to previous page') }}
|
{{ $t('Back to previous page') }}
|
||||||
|
@ -31,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
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 { FETCH_EVENT, JOIN_EVENT } from '@/graphql/event';
|
||||||
import { IConfig } from '@/types/config.model';
|
import { IConfig } from '@/types/config.model';
|
||||||
import { CONFIG } from '@/graphql/config';
|
import { CONFIG } from '@/graphql/config';
|
||||||
|
@ -55,10 +61,11 @@ import { RouteName } from '@/router';
|
||||||
})
|
})
|
||||||
export default class ParticipationWithoutAccount extends Vue {
|
export default class ParticipationWithoutAccount extends Vue {
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
anonymousParticipation: { email: String } = { email: '' };
|
anonymousParticipation: { email: String, message: String } = { email: '', message: '' };
|
||||||
event!: IEvent;
|
event!: IEvent;
|
||||||
config!: IConfig;
|
config!: IConfig;
|
||||||
error: String|boolean = false;
|
error: String|boolean = false;
|
||||||
|
EventJoinOptions = EventJoinOptions;
|
||||||
|
|
||||||
async joinEvent() {
|
async joinEvent() {
|
||||||
this.error = false;
|
this.error = false;
|
||||||
|
@ -69,6 +76,7 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||||
eventId: this.event.id,
|
eventId: this.event.id,
|
||||||
actorId: this.config.anonymous.actorId,
|
actorId: this.config.anonymous.actorId,
|
||||||
email: this.anonymousParticipation.email,
|
email: this.anonymousParticipation.email,
|
||||||
|
message: this.anonymousParticipation.message,
|
||||||
},
|
},
|
||||||
update: (store, { data }) => {
|
update: (store, { data }) => {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
|
@ -2,23 +2,28 @@ import gql from 'graphql-tag';
|
||||||
import { COMMENT_FIELDS_FRAGMENT } from '@/graphql/comment';
|
import { COMMENT_FIELDS_FRAGMENT } from '@/graphql/comment';
|
||||||
|
|
||||||
const participantQuery = `
|
const participantQuery = `
|
||||||
role,
|
total,
|
||||||
id,
|
elements {
|
||||||
actor {
|
role,
|
||||||
preferredUsername,
|
id,
|
||||||
avatar {
|
actor {
|
||||||
url
|
preferredUsername,
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
domain
|
||||||
},
|
},
|
||||||
name,
|
event {
|
||||||
id,
|
id,
|
||||||
domain
|
uuid
|
||||||
},
|
},
|
||||||
event {
|
metadata {
|
||||||
id,
|
cancellationToken,
|
||||||
uuid
|
message
|
||||||
},
|
},
|
||||||
metadata {
|
insertedAt
|
||||||
cancellationToken
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -371,11 +376,12 @@ export const EDIT_EVENT = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const JOIN_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(
|
joinEvent(
|
||||||
eventId: $eventId,
|
eventId: $eventId,
|
||||||
actorId: $actorId,
|
actorId: $actorId,
|
||||||
email: $email
|
email: $email,
|
||||||
|
message: $message
|
||||||
) {
|
) {
|
||||||
${participantQuery}
|
${participantQuery}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"Allow all comments": "Allow all comments",
|
"Allow all comments": "Allow all comments",
|
||||||
"Allow registrations": "Allow registrations",
|
"Allow registrations": "Allow registrations",
|
||||||
"An error has occurred.": "An error has occurred.",
|
"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 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",
|
"Anonymous participations": "Anonymous participations",
|
||||||
"Approve": "Approve",
|
"Approve": "Approve",
|
||||||
|
@ -467,5 +466,17 @@
|
||||||
"{count} requests waiting": "{count} requests waiting",
|
"{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.",
|
"{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 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 all comments": "Permitir todos los comentarios",
|
||||||
"Allow registrations": "Permitir registros",
|
"Allow registrations": "Permitir registros",
|
||||||
"An error has occurred.": "Se ha producido un error.",
|
"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 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",
|
"Anonymous participations": "Participaciones anónimas",
|
||||||
"Approve": "Aprobar",
|
"Approve": "Aprobar",
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"Allow all comments": "Salli kaikki kommentit",
|
"Allow all comments": "Salli kaikki kommentit",
|
||||||
"Allow registrations": "Salli rekisteröityminen",
|
"Allow registrations": "Salli rekisteröityminen",
|
||||||
"An error has occurred.": "Tapahtui virhe.",
|
"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 participants will be asked to confirm their participation through e-mail.": "Anonyymejä osallistujia pyydetään vahvistamaan osallistumisensa sähköpostitse.",
|
||||||
"Anonymous participations": "Anonyymit osallistujat",
|
"Anonymous participations": "Anonyymit osallistujat",
|
||||||
"Approve": "Hyväksy",
|
"Approve": "Hyväksy",
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"Allow all comments": "Autoriser tous les commentaires",
|
"Allow all comments": "Autoriser tous les commentaires",
|
||||||
"Allow registrations": "Autoriser les inscriptions",
|
"Allow registrations": "Autoriser les inscriptions",
|
||||||
"An error has occurred.": "Une erreur est survenue.",
|
"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 participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.",
|
||||||
"Anonymous participations": "Participations anonymes",
|
"Anonymous participations": "Participations anonymes",
|
||||||
"Approve": "Approuver",
|
"Approve": "Approuver",
|
||||||
|
@ -252,14 +251,14 @@
|
||||||
"Or": "Ou",
|
"Or": "Ou",
|
||||||
"Organized": "Organisés",
|
"Organized": "Organisés",
|
||||||
"Organized by {name}": "Organisé par {name}",
|
"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é.",
|
"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.",
|
"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 limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
||||||
"Page not found": "Page non trouvée",
|
"Page not found": "Page non trouvée",
|
||||||
"Participant already was rejected.": "Le participant a déjà été refusé.",
|
"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.",
|
"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": "Participer",
|
||||||
"Participate using your email address": "Participer en utilisant votre adresse email",
|
"Participate using your email address": "Participer en utilisant votre adresse email",
|
||||||
"Participation approval": "Validation des participations",
|
"Participation approval": "Validation des participations",
|
||||||
|
@ -473,5 +472,17 @@
|
||||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
"{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.",
|
"{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 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 all comments": "Autorizar totes los comentaris",
|
||||||
"Allow registrations": "Permetre las inscripcions",
|
"Allow registrations": "Permetre las inscripcions",
|
||||||
"An error has occurred.": "Una error s’es producha.",
|
"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 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",
|
"Anonymous participations": "Participacions anonimas",
|
||||||
"Approve": "Aprovar",
|
"Approve": "Aprovar",
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"Allow all comments": "Permitir todos comentários",
|
"Allow all comments": "Permitir todos comentários",
|
||||||
"Allow registrations": "Permitir inscrições",
|
"Allow registrations": "Permitir inscrições",
|
||||||
"An error has occurred.": "Ocorreu um erro.",
|
"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 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",
|
"Anonymous participations": "Participações anônimas",
|
||||||
"Approve": "Aprovar",
|
"Approve": "Aprovar",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { ColorModifiers } from 'buefy/types/helpers';
|
import { ColorModifiers } from 'buefy/types/helpers';
|
||||||
|
import { Route, RawLocation } from 'vue-router';
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
declare module 'vue/types/vue' {
|
||||||
interface Vue {
|
interface Vue {
|
||||||
|
@ -8,6 +9,23 @@ declare module 'vue/types/vue' {
|
||||||
error: (message: string) => void;
|
error: (message: string) => void;
|
||||||
info: (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 },
|
props: { isUpdate: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/events/participations/:eventId',
|
path: '/events/:eventId/participations',
|
||||||
name: EventRouteName.PARTICIPATIONS,
|
name: EventRouteName.PARTICIPATIONS,
|
||||||
component: participations,
|
component: participations,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true },
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Address, IAddress } from '@/types/address.model';
|
||||||
import { ITag } from '@/types/tag.model';
|
import { ITag } from '@/types/tag.model';
|
||||||
import { IPicture } from '@/types/picture.model';
|
import { IPicture } from '@/types/picture.model';
|
||||||
import { IComment } from '@/types/comment.model';
|
import { IComment } from '@/types/comment.model';
|
||||||
|
import { Paginate } from '@/types/paginate';
|
||||||
|
|
||||||
export enum EventStatus {
|
export enum EventStatus {
|
||||||
TENTATIVE = 'TENTATIVE',
|
TENTATIVE = 'TENTATIVE',
|
||||||
|
@ -59,7 +60,8 @@ export interface IParticipant {
|
||||||
role: ParticipantRole;
|
role: ParticipantRole;
|
||||||
actor: IActor;
|
actor: IActor;
|
||||||
event: IEvent;
|
event: IEvent;
|
||||||
metadata: { cancellationToken?: string };
|
metadata: { cancellationToken?: string, message?: string };
|
||||||
|
insertedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Participant implements IParticipant {
|
export class Participant implements IParticipant {
|
||||||
|
@ -68,6 +70,7 @@ export class Participant implements IParticipant {
|
||||||
actor!: IActor;
|
actor!: IActor;
|
||||||
role: ParticipantRole = ParticipantRole.NOT_APPROVED;
|
role: ParticipantRole = ParticipantRole.NOT_APPROVED;
|
||||||
metadata = {};
|
metadata = {};
|
||||||
|
insertedAt?: Date;
|
||||||
|
|
||||||
constructor(hash?: IParticipant) {
|
constructor(hash?: IParticipant) {
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
|
@ -77,6 +80,7 @@ export class Participant implements IParticipant {
|
||||||
this.actor = new Actor(hash.actor);
|
this.actor = new Actor(hash.actor);
|
||||||
this.role = hash.role;
|
this.role = hash.role;
|
||||||
this.metadata = hash.metadata;
|
this.metadata = hash.metadata;
|
||||||
|
this.insertedAt = hash.insertedAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +136,7 @@ export interface IEvent {
|
||||||
organizerActor?: IActor;
|
organizerActor?: IActor;
|
||||||
attributedTo: IActor;
|
attributedTo: IActor;
|
||||||
participantStats: IEventParticipantStats;
|
participantStats: IEventParticipantStats;
|
||||||
participants: IParticipant[];
|
participants: Paginate<IParticipant>;
|
||||||
|
|
||||||
relatedEvents: IEvent[];
|
relatedEvents: IEvent[];
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
|
@ -205,7 +209,7 @@ export class EventModel implements IEvent {
|
||||||
publishAt = new Date();
|
publishAt = new Date();
|
||||||
|
|
||||||
participantStats = { notApproved: 0, notConfirmed: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
|
participantStats = { notApproved: 0, notConfirmed: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
|
||||||
participants: IParticipant[] = [];
|
participants!: Paginate<IParticipant>;
|
||||||
|
|
||||||
relatedEvents: IEvent[] = [];
|
relatedEvents: IEvent[] = [];
|
||||||
comments: IComment[] = [];
|
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"
|
:current-actor="currentActor"
|
||||||
@joinEvent="joinEvent"
|
@joinEvent="joinEvent"
|
||||||
@joinModal="isJoinModalActive = true"
|
@joinModal="isJoinModalActive = true"
|
||||||
|
@joinEventWithConfirmation="joinEventWithConfirmation"
|
||||||
@confirmLeave="confirmLeave"
|
@confirmLeave="confirmLeave"
|
||||||
/>
|
/>
|
||||||
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
|
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
|
||||||
|
@ -263,13 +264,44 @@
|
||||||
<button
|
<button
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
ref="confirmButton"
|
ref="confirmButton"
|
||||||
@click="joinEvent(identity)">
|
@click="event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity)">
|
||||||
{{ $t('Confirm my particpation') }}
|
{{ $t('Confirm my particpation') }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
</identity-picker>
|
</identity-picker>
|
||||||
</b-modal>
|
</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>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -281,11 +313,10 @@ import {
|
||||||
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
||||||
FETCH_EVENT,
|
FETCH_EVENT,
|
||||||
JOIN_EVENT,
|
JOIN_EVENT,
|
||||||
LEAVE_EVENT,
|
|
||||||
} from '@/graphql/event';
|
} from '@/graphql/event';
|
||||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
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 { IPerson, Person } from '@/types/actor';
|
||||||
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
|
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
|
||||||
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
||||||
|
@ -398,12 +429,16 @@ export default class Event extends EventMixin {
|
||||||
showMap: boolean = false;
|
showMap: boolean = false;
|
||||||
isReportModalActive: boolean = false;
|
isReportModalActive: boolean = false;
|
||||||
isJoinModalActive: boolean = false;
|
isJoinModalActive: boolean = false;
|
||||||
|
isJoinConfirmationModalActive: boolean = false;
|
||||||
EventVisibility = EventVisibility;
|
EventVisibility = EventVisibility;
|
||||||
EventStatus = EventStatus;
|
EventStatus = EventStatus;
|
||||||
|
EventJoinOptions = EventJoinOptions;
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
observer!: IntersectionObserver;
|
observer!: IntersectionObserver;
|
||||||
loadComments: boolean = false;
|
loadComments: boolean = false;
|
||||||
anonymousParticipation: boolean|null = null;
|
anonymousParticipation: boolean|null = null;
|
||||||
|
actorForConfirmation!: IPerson;
|
||||||
|
messageForConfirmation: string = '';
|
||||||
|
|
||||||
get eventTitle() {
|
get eventTitle() {
|
||||||
if (!this.event) return undefined;
|
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;
|
this.isJoinModalActive = false;
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
||||||
|
@ -514,6 +555,7 @@ export default class Event extends EventMixin {
|
||||||
variables: {
|
variables: {
|
||||||
eventId: this.event.id,
|
eventId: this.event.id,
|
||||||
actorId: identity.id,
|
actorId: identity.id,
|
||||||
|
message,
|
||||||
},
|
},
|
||||||
update: (store, { data }) => {
|
update: (store, { data }) => {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {ParticipantRole} from "@/types/event.model";
|
||||||
<template>
|
<template>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
<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>
|
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
|
||||||
</template>
|
</template>
|
||||||
<template>
|
<template>
|
||||||
<section v-if="participantsAndCreators.length > 0">
|
<section v-if="participants && participants.total > 0">
|
||||||
<h2 class="title">{{ $t('Participants') }}</h2>
|
<h2 class="title">{{ $t('Participants') }}</h2>
|
||||||
<p v-if="confirmedAnonymousParticipantsCountCount > 1">
|
<ParticipationTable
|
||||||
{{ $tc('And no anonymous participations|And one anonymous participation|And {count} anonymous participations', confirmedAnonymousParticipantsCountCount, { count: confirmedAnonymousParticipantsCountCount}) }}
|
:data="participants.elements"
|
||||||
</p>
|
:accept-participant="acceptParticipant"
|
||||||
<div class="columns is-multiline">
|
:refuse-participant="refuseParticipant"
|
||||||
<div class="column is-one-quarter-desktop" v-for="participant in participantsAndCreators" :key="participant.actor.id">
|
:showRole="true"
|
||||||
<participant-card
|
:total="participants.total"
|
||||||
v-if="participant.actor.id !== config.anonymous.actorId"
|
:perPage="PARTICIPANTS_PER_PAGE"
|
||||||
:participant="participant"
|
@page-change="(page) => participantPage = page"
|
||||||
:accept="acceptParticipant"
|
/>
|
||||||
:reject="refuseParticipant"
|
|
||||||
:exclude="refuseParticipant"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-tab-item>
|
</b-tab-item>
|
||||||
|
@ -32,18 +28,16 @@
|
||||||
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
|
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
|
||||||
</template>
|
</template>
|
||||||
<template>
|
<template>
|
||||||
<section v-if="queue.length > 0">
|
<section v-if="queue && queue.total > 0">
|
||||||
<h2 class="title">{{ $t('Waiting list') }}</h2>
|
<h2 class="title">{{ $t('Waiting list') }}</h2>
|
||||||
<div class="columns">
|
<ParticipationTable
|
||||||
<div class="column is-one-quarter-desktop" v-for="participant in queue" :key="participant.actor.id">
|
:data="queue.elements"
|
||||||
<participant-card
|
:accept-participant="acceptParticipant"
|
||||||
:participant="participant"
|
:refuse-participant="refuseParticipant"
|
||||||
:accept="acceptParticipant"
|
:total="queue.total"
|
||||||
:reject="refuseParticipant"
|
:perPage="PARTICIPANTS_PER_PAGE"
|
||||||
:exclude="refuseParticipant"
|
@page-change="(page) => queuePage = page"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-tab-item>
|
</b-tab-item>
|
||||||
|
@ -53,18 +47,16 @@
|
||||||
<span>{{ $t('Rejected')}} <b-tag rounded> {{ participantStats.rejected }} </b-tag> </span>
|
<span>{{ $t('Rejected')}} <b-tag rounded> {{ participantStats.rejected }} </b-tag> </span>
|
||||||
</template>
|
</template>
|
||||||
<template>
|
<template>
|
||||||
<section v-if="rejected.length > 0">
|
<section v-if="rejected && rejected.total > 0">
|
||||||
<h2 class="title">{{ $t('Rejected participations') }}</h2>
|
<h2 class="title">{{ $t('Rejected participations') }}</h2>
|
||||||
<div class="columns">
|
<ParticipationTable
|
||||||
<div class="column is-one-quarter-desktop" v-for="participant in rejected" :key="participant.actor.id">
|
:data="rejected.elements"
|
||||||
<participant-card
|
:accept-participant="acceptParticipant"
|
||||||
:participant="participant"
|
:refuse-participant="refuseParticipant"
|
||||||
:accept="acceptParticipant"
|
:total="rejected.total"
|
||||||
:reject="refuseParticipant"
|
:perPage="PARTICIPANTS_PER_PAGE"
|
||||||
:exclude="refuseParticipant"
|
@page-change="(page) => rejectedPage = page"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-tab-item>
|
</b-tab-item>
|
||||||
|
@ -81,9 +73,15 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||||
import { IPerson } from '@/types/actor';
|
import { IPerson } from '@/types/actor';
|
||||||
import { CONFIG } from '@/graphql/config';
|
import { CONFIG } from '@/graphql/config';
|
||||||
import { IConfig } from '@/types/config.model';
|
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({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
ParticipationTable,
|
||||||
ParticipantCard,
|
ParticipantCard,
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -97,7 +95,7 @@ import { IConfig } from '@/types/config.model';
|
||||||
return {
|
return {
|
||||||
uuid: this.eventId,
|
uuid: this.eventId,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10,
|
limit: PARTICIPANTS_PER_PAGE,
|
||||||
roles: [ParticipantRole.PARTICIPANT].join(),
|
roles: [ParticipantRole.PARTICIPANT].join(),
|
||||||
actorId: this.currentActor.id,
|
actorId: this.currentActor.id,
|
||||||
};
|
};
|
||||||
|
@ -106,18 +104,18 @@ import { IConfig } from '@/types/config.model';
|
||||||
return !this.currentActor.id;
|
return !this.currentActor.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
organizers: {
|
participants: {
|
||||||
query: PARTICIPANTS,
|
query: PARTICIPANTS,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
uuid: this.eventId,
|
uuid: this.eventId,
|
||||||
page: 1,
|
page: this.participantPage,
|
||||||
limit: 20,
|
limit: PARTICIPANTS_PER_PAGE,
|
||||||
roles: [ParticipantRole.CREATOR].join(),
|
roles: [ParticipantRole.CREATOR, ParticipantRole.PARTICIPANT].join(),
|
||||||
actorId: this.currentActor.id,
|
actorId: this.currentActor.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
update(data) { return this.dataTransform(data); },
|
||||||
skip() {
|
skip() {
|
||||||
return !this.currentActor.id;
|
return !this.currentActor.id;
|
||||||
},
|
},
|
||||||
|
@ -127,13 +125,13 @@ import { IConfig } from '@/types/config.model';
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
uuid: this.eventId,
|
uuid: this.eventId,
|
||||||
page: 1,
|
page: this.queuePage,
|
||||||
limit: 20,
|
limit: PARTICIPANTS_PER_PAGE,
|
||||||
roles: [ParticipantRole.NOT_APPROVED].join(),
|
roles: [ParticipantRole.NOT_APPROVED].join(),
|
||||||
actorId: this.currentActor.id,
|
actorId: this.currentActor.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
update(data) { return this.dataTransform(data); },
|
||||||
skip() {
|
skip() {
|
||||||
return !this.currentActor.id;
|
return !this.currentActor.id;
|
||||||
},
|
},
|
||||||
|
@ -143,27 +141,36 @@ import { IConfig } from '@/types/config.model';
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
uuid: this.eventId,
|
uuid: this.eventId,
|
||||||
page: 1,
|
page: this.rejectedPage,
|
||||||
limit: 20,
|
limit: PARTICIPANTS_PER_PAGE,
|
||||||
roles: [ParticipantRole.REJECTED].join(),
|
roles: [ParticipantRole.REJECTED].join(),
|
||||||
actorId: this.currentActor.id,
|
actorId: this.currentActor.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
update(data) { return this.dataTransform(data); },
|
||||||
skip() {
|
skip() {
|
||||||
return !this.currentActor.id;
|
return !this.currentActor.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class Participants extends Vue {
|
export default class Participants extends Vue {
|
||||||
@Prop({ required: true }) eventId!: string;
|
@Prop({ required: true }) eventId!: string;
|
||||||
page: number = 1;
|
page: number = 1;
|
||||||
limit: number = 10;
|
limit: number = 10;
|
||||||
|
|
||||||
organizers: IParticipant[] = [];
|
participants!: Paginate<IParticipant>;
|
||||||
queue: IParticipant[] = [];
|
participantPage: number = 1;
|
||||||
rejected: IParticipant[] = [];
|
|
||||||
|
queue!: Paginate<IParticipant>;
|
||||||
|
queuePage: number = 1;
|
||||||
|
|
||||||
|
rejected!: Paginate<IParticipant>;
|
||||||
|
rejectedPage: number = 1;
|
||||||
|
|
||||||
event!: IEvent;
|
event!: IEvent;
|
||||||
config!: IConfig;
|
config!: IConfig;
|
||||||
|
|
||||||
|
@ -173,23 +180,20 @@ export default class Participants extends Vue {
|
||||||
hasMoreParticipants: boolean = false;
|
hasMoreParticipants: boolean = false;
|
||||||
activeTab: number = 0;
|
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 {
|
get participantStats(): IEventParticipantStats | null {
|
||||||
if (!this.event) return null;
|
if (!this.event) return null;
|
||||||
return this.event.participantStats;
|
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 })
|
@Watch('participantStats', { deep: true })
|
||||||
watchParticipantStats(stats: IEventParticipantStats) {
|
watchParticipantStats(stats: IEventParticipantStats) {
|
||||||
if (!stats) return;
|
if (!stats) return;
|
||||||
|
@ -232,8 +236,8 @@ export default class Participants extends Vue {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (data) {
|
if (data) {
|
||||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||||
this.rejected = this.rejected.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;
|
this.event.participantStats.going += 1;
|
||||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||||
this.event.participantStats.notApproved -= 1;
|
this.event.participantStats.notApproved -= 1;
|
||||||
|
@ -242,7 +246,7 @@ export default class Participants extends Vue {
|
||||||
this.event.participantStats.rejected -= 1;
|
this.event.participantStats.rejected -= 1;
|
||||||
}
|
}
|
||||||
participant.role = ParticipantRole.PARTICIPANT;
|
participant.role = ParticipantRole.PARTICIPANT;
|
||||||
this.event.participants.push(participant);
|
this.event.participants.elements.push(participant);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -260,8 +264,10 @@ export default class Participants extends Vue {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (data) {
|
if (data) {
|
||||||
this.event.participants = this.event.participants.filter(participant => participant.id !== data.updateParticipation.id);
|
this.event.participants.elements = this.event.participants.elements.filter(
|
||||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
participant => participant.id !== data.updateParticipation.id,
|
||||||
|
);
|
||||||
|
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||||
this.event.participantStats.rejected += 1;
|
this.event.participantStats.rejected += 1;
|
||||||
if (participant.role === ParticipantRole.PARTICIPANT) {
|
if (participant.role === ParticipantRole.PARTICIPANT) {
|
||||||
this.event.participantStats.participant -= 1;
|
this.event.participantStats.participant -= 1;
|
||||||
|
@ -271,8 +277,8 @@ export default class Participants extends Vue {
|
||||||
this.event.participantStats.notApproved -= 1;
|
this.event.participantStats.notApproved -= 1;
|
||||||
}
|
}
|
||||||
participant.role = ParticipantRole.REJECTED;
|
participant.role = ParticipantRole.REJECTED;
|
||||||
this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id);
|
this.rejected.elements = this.rejected.elements.filter(participantIn => participantIn.id !== participant.id);
|
||||||
this.rejected.push(participant);
|
this.rejected.elements.push(participant);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -439,7 +439,10 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||||
event_id: event.id,
|
event_id: event.id,
|
||||||
actor_id: actor.id,
|
actor_id: actor.id,
|
||||||
url: Map.get(additional, :url),
|
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),
|
join_data <- Convertible.model_to_as(participant),
|
||||||
audience <-
|
audience <-
|
||||||
|
|
|
@ -306,13 +306,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data
|
%{
|
||||||
|
"type" => "Join",
|
||||||
|
"object" => object,
|
||||||
|
"actor" => _actor,
|
||||||
|
"id" => id,
|
||||||
|
"participationMessage" => note
|
||||||
|
} = data
|
||||||
) do
|
) do
|
||||||
with actor <- Utils.get_actor(data),
|
with actor <- Utils.get_actor(data),
|
||||||
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
|
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
|
||||||
object <- Utils.get_url(object),
|
object <- Utils.get_url(object),
|
||||||
{:ok, object} <- ActivityPub.fetch_object_from_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}
|
{:ok, activity, object}
|
||||||
else
|
else
|
||||||
e ->
|
e ->
|
||||||
|
|
|
@ -77,6 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||||
"anonymousParticipationEnabled" => %{
|
"anonymousParticipationEnabled" => %{
|
||||||
"@id" => "mz:anonymousParticipationEnabled",
|
"@id" => "mz:anonymousParticipationEnabled",
|
||||||
"@type" => "sc:Boolean"
|
"@type" => "sc:Boolean"
|
||||||
|
},
|
||||||
|
"participationMessage" => %{
|
||||||
|
"@id" => "mz:participationMessage",
|
||||||
|
"@type" => "sc:Text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -25,7 +25,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Participant do
|
||||||
"type" => "Join",
|
"type" => "Join",
|
||||||
"id" => participant.url,
|
"id" => participant.url,
|
||||||
"actor" => participant.actor.url,
|
"actor" => participant.actor.url,
|
||||||
"object" => participant.event.url
|
"object" => participant.event.url,
|
||||||
|
"participationMessage" => Map.get(participant.metadata, :message)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,7 +102,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_participants_for_event(_, _args, _resolution) do
|
def list_participants_for_event(_, _args, _resolution) do
|
||||||
{:ok, []}
|
{:ok, %{total: 0, elements: []}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
|
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
|
||||||
|
|
|
@ -17,12 +17,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
"""
|
"""
|
||||||
def actor_join_event(
|
def actor_join_event(
|
||||||
_parent,
|
_parent,
|
||||||
%{actor_id: actor_id, event_id: event_id},
|
%{actor_id: actor_id, event_id: event_id} = args,
|
||||||
%{context: %{current_user: %User{} = user}}
|
%{context: %{current_user: %User{} = user}}
|
||||||
) do
|
) do
|
||||||
case User.owns_actor(user, actor_id) do
|
case User.owns_actor(user, actor_id) do
|
||||||
{:is_owned, %Actor{} = actor} ->
|
{: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"}
|
{:error, "Actor id is not owned by authenticated user"}
|
||||||
|
@ -136,7 +136,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
_parent,
|
_parent,
|
||||||
%{actor_id: actor_id, event_id: event_id, token: token},
|
%{actor_id: actor_id, event_id: event_id, token: token},
|
||||||
_resolution
|
_resolution
|
||||||
) do
|
)
|
||||||
|
when not is_nil(token) do
|
||||||
with {:anonymous_participation_enabled, true} <-
|
with {:anonymous_participation_enabled, true} <-
|
||||||
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
||||||
{:anonymous_actor_id, true} <-
|
{:anonymous_actor_id, true} <-
|
||||||
|
|
|
@ -65,7 +65,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
|
|
||||||
field(:participant_stats, :participant_stats)
|
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(:page, :integer, default_value: 1)
|
||||||
arg(:limit, :integer, default_value: 10)
|
arg(:limit, :integer, default_value: 10)
|
||||||
arg(:roles, :string, default_value: "")
|
arg(:roles, :string, default_value: "")
|
||||||
|
|
|
@ -33,12 +33,21 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||||
field(:metadata, :participant_metadata,
|
field(:metadata, :participant_metadata,
|
||||||
description: "The metadata associated to this participant"
|
description: "The metadata associated to this participant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
field(:inserted_at, :datetime, description: "The datetime this participant was created")
|
||||||
end
|
end
|
||||||
|
|
||||||
object :participant_metadata do
|
object :participant_metadata do
|
||||||
field(:cancellation_token, :string,
|
field(:cancellation_token, :string,
|
||||||
description: "The eventual token to leave an event when user is anonymous"
|
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
|
end
|
||||||
|
|
||||||
enum :participant_role_enum do
|
enum :participant_role_enum do
|
||||||
|
@ -64,6 +73,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||||
arg(:event_id, non_null(:id))
|
arg(:event_id, non_null(:id))
|
||||||
arg(:actor_id, non_null(:id))
|
arg(:actor_id, non_null(:id))
|
||||||
arg(:email, :string)
|
arg(:email, :string)
|
||||||
|
arg(:message, :string)
|
||||||
|
|
||||||
resolve(&Participant.actor_join_event/3)
|
resolve(&Participant.actor_join_event/3)
|
||||||
end
|
end
|
||||||
|
|
|
@ -759,7 +759,7 @@ defmodule Mobilizon.Events do
|
||||||
Default behaviour is to not return :not_approved or :not_confirmed participants
|
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) ::
|
@spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
|
||||||
[Participant.t()]
|
Page.t()
|
||||||
def list_participants_for_event(
|
def list_participants_for_event(
|
||||||
id,
|
id,
|
||||||
roles \\ @default_participant_roles,
|
roles \\ @default_participant_roles,
|
||||||
|
@ -769,8 +769,7 @@ defmodule Mobilizon.Events do
|
||||||
id
|
id
|
||||||
|> list_participants_for_event_query()
|
|> list_participants_for_event_query()
|
||||||
|> filter_role(roles)
|
|> filter_role(roles)
|
||||||
|> Page.paginate(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|> Repo.all()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
||||||
|
|
|
@ -18,11 +18,15 @@ defmodule Mobilizon.Events.Participant do
|
||||||
role: ParticipantRole.t(),
|
role: ParticipantRole.t(),
|
||||||
url: String.t(),
|
url: String.t(),
|
||||||
event: Event.t(),
|
event: Event.t(),
|
||||||
actor: Actor.t()
|
actor: Actor.t(),
|
||||||
|
metadata: Map.t()
|
||||||
}
|
}
|
||||||
|
|
||||||
@required_attrs [:url, :role, :event_id, :actor_id]
|
@required_attrs [:url, :role, :event_id, :actor_id]
|
||||||
@attrs @required_attrs
|
@attrs @required_attrs
|
||||||
|
@metadata_attrs [:email, :confirmation_token, :cancellation_token, :message]
|
||||||
|
|
||||||
|
@timestamps_opts [type: :utc_datetime]
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
schema "participants" do
|
schema "participants" do
|
||||||
|
@ -33,6 +37,7 @@ defmodule Mobilizon.Events.Participant do
|
||||||
field(:email, :string)
|
field(:email, :string)
|
||||||
field(:confirmation_token, :string)
|
field(:confirmation_token, :string)
|
||||||
field(:cancellation_token, :string)
|
field(:cancellation_token, :string)
|
||||||
|
field(:message, :string)
|
||||||
end
|
end
|
||||||
|
|
||||||
belongs_to(:event, Event, primary_key: true)
|
belongs_to(:event, Event, primary_key: true)
|
||||||
|
@ -70,7 +75,7 @@ defmodule Mobilizon.Events.Participant do
|
||||||
|
|
||||||
defp metadata_changeset(schema, params) do
|
defp metadata_changeset(schema, params) do
|
||||||
schema
|
schema
|
||||||
|> cast(params, [:email, :confirmation_token, :cancellation_token])
|
|> cast(params, @metadata_attrs)
|
||||||
|> Checker.validate_changeset()
|
|> Checker.validate_changeset()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
405
schema.graphql
405
schema.graphql
|
@ -1,5 +1,5 @@
|
||||||
# source: http://localhost:4000/api
|
# 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 {
|
schema {
|
||||||
query: RootQueryType
|
query: RootQueryType
|
||||||
|
@ -839,6 +839,9 @@ type Participant {
|
||||||
"""The participation ID"""
|
"""The participation ID"""
|
||||||
id: ID
|
id: ID
|
||||||
|
|
||||||
|
"""The datetime this participant was created"""
|
||||||
|
insertedAt: DateTime
|
||||||
|
|
||||||
"""The metadata associated to this participant"""
|
"""The metadata associated to this participant"""
|
||||||
metadata: ParticipantMetadata
|
metadata: ParticipantMetadata
|
||||||
|
|
||||||
|
@ -849,6 +852,9 @@ type Participant {
|
||||||
type ParticipantMetadata {
|
type ParticipantMetadata {
|
||||||
"""The eventual token to leave an event when user is anonymous"""
|
"""The eventual token to leave an event when user is anonymous"""
|
||||||
cancellationToken: String
|
cancellationToken: String
|
||||||
|
|
||||||
|
"""The eventual message the participant left"""
|
||||||
|
message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ParticipantRoleEnum {
|
enum ParticipantRoleEnum {
|
||||||
|
@ -1082,203 +1088,9 @@ enum ReportStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RootMutationType {
|
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"""
|
"""Send a link through email to reset user password"""
|
||||||
sendResetPassword(email: String!, locale: String): String
|
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"""
|
"""Update an event"""
|
||||||
updateEvent(
|
updateEvent(
|
||||||
beginsOn: DateTime
|
beginsOn: DateTime
|
||||||
|
@ -1305,18 +1117,211 @@ type RootMutationType {
|
||||||
title: String
|
title: String
|
||||||
visibility: EventVisibility = PUBLIC
|
visibility: EventVisibility = PUBLIC
|
||||||
): Event
|
): 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"""
|
"""Change an user password"""
|
||||||
changePassword(newPassword: String!, oldPassword: String!): User
|
changePassword(newPassword: String!, oldPassword: String!): User
|
||||||
|
|
||||||
"""Update a report"""
|
|
||||||
updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report
|
|
||||||
|
|
||||||
"""Resend registration confirmation token"""
|
"""Resend registration confirmation token"""
|
||||||
resendConfirmationEmail(email: String!, locale: String): String
|
resendConfirmationEmail(email: String!, locale: String): String
|
||||||
|
|
||||||
"""Login an user"""
|
"""Confirm a participation"""
|
||||||
login(email: String!, password: String!): Login
|
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"] == []
|
assert activity.data["cc"] == []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@join_message "I want to get in!"
|
||||||
test "it accepts Join activities" do
|
test "it accepts Join activities" do
|
||||||
%Actor{url: organizer_url} = organizer = insert(:actor)
|
%Actor{url: organizer_url} = organizer = insert(:actor)
|
||||||
%Actor{url: participant_url} = _participant = insert(:actor)
|
%Actor{url: participant_url} = _participant = insert(:actor)
|
||||||
|
@ -829,13 +830,19 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
|> Jason.decode!()
|
|> Jason.decode!()
|
||||||
|> Map.put("actor", participant_url)
|
|> Map.put("actor", participant_url)
|
||||||
|> Map.put("object", event_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["type"] == "Accept"
|
||||||
assert activity.data["object"]["object"] == event_url
|
assert activity.data["object"]["object"] == event_url
|
||||||
assert activity.data["object"]["id"] =~ "/join/event/"
|
assert activity.data["object"]["id"] =~ "/join/event/"
|
||||||
assert activity.data["object"]["type"] =~ "Join"
|
assert activity.data["object"]["type"] =~ "Join"
|
||||||
|
assert activity.data["object"]["participationMessage"] == @join_message
|
||||||
assert activity.data["actor"] == organizer_url
|
assert activity.data["actor"] == organizer_url
|
||||||
assert activity.data["id"] =~ "/accept/join/"
|
assert activity.data["id"] =~ "/accept/join/"
|
||||||
end
|
end
|
||||||
|
@ -894,6 +901,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
# Organiser is not present since we use factories directly
|
# Organiser is not present since we use factories directly
|
||||||
assert event.id
|
assert event.id
|
||||||
|> Events.list_participants_for_event()
|
|> Events.list_participants_for_event()
|
||||||
|
|> Map.get(:elements)
|
||||||
|> Enum.map(& &1.id) ==
|
|> Enum.map(& &1.id) ==
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
@ -924,6 +932,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
# The only participant left is the organizer
|
# The only participant left is the organizer
|
||||||
assert event.id
|
assert event.id
|
||||||
|> Events.list_participants_for_event()
|
|> Events.list_participants_for_event()
|
||||||
|
|> Map.get(:elements)
|
||||||
|> Enum.map(& &1.id) ==
|
|> Enum.map(& &1.id) ==
|
||||||
[organizer_participation.id]
|
[organizer_participation.id]
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
|
alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
|
||||||
alias Mobilizon.GraphQL.AbsintheHelpers
|
alias Mobilizon.GraphQL.AbsintheHelpers
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Web.Email
|
alias Mobilizon.Web.Email
|
||||||
|
|
||||||
import Mobilizon.Factory
|
import Mobilizon.Factory
|
||||||
|
@ -446,9 +447,11 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
participants(roles: "participant,moderator,administrator,creator", actor_id: "#{
|
participants(roles: "participant,moderator,administrator,creator", actor_id: "#{
|
||||||
actor.id
|
actor.id
|
||||||
}") {
|
}") {
|
||||||
role,
|
elements {
|
||||||
actor {
|
role,
|
||||||
preferredUsername
|
actor {
|
||||||
|
preferredUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,7 +465,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
|
|
||||||
assert json_response(res, 200)["errors"] == nil
|
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" => %{
|
"actor" => %{
|
||||||
"preferredUsername" => actor.preferred_username
|
"preferredUsername" => actor.preferred_username
|
||||||
|
@ -485,9 +488,11 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
||||||
actor.id
|
actor.id
|
||||||
}") {
|
}") {
|
||||||
role,
|
elements {
|
||||||
actor {
|
role,
|
||||||
preferredUsername
|
actor {
|
||||||
|
preferredUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -500,7 +505,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
||||||
|
|
||||||
sorted_participants =
|
sorted_participants =
|
||||||
json_response(res, 200)["data"]["event"]["participants"]
|
json_response(res, 200)["data"]["event"]["participants"]["elements"]
|
||||||
|> Enum.filter(&(&1["role"] == "PARTICIPANT"))
|
|> Enum.filter(&(&1["role"] == "PARTICIPANT"))
|
||||||
|
|
||||||
assert sorted_participants == [
|
assert sorted_participants == [
|
||||||
|
@ -518,9 +523,11 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
|
||||||
actor.id
|
actor.id
|
||||||
}") {
|
}") {
|
||||||
role,
|
elements {
|
||||||
actor {
|
role,
|
||||||
preferredUsername
|
actor {
|
||||||
|
preferredUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,7 +540,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
|
||||||
|
|
||||||
sorted_participants =
|
sorted_participants =
|
||||||
json_response(res, 200)["data"]["event"]["participants"]
|
json_response(res, 200)["data"]["event"]["participants"]["elements"]
|
||||||
|> Enum.sort_by(
|
|> Enum.sort_by(
|
||||||
&(&1
|
&(&1
|
||||||
|> Map.get("actor")
|
|> 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"]["event"]["id"] == to_string(event.id)
|
||||||
assert res["data"]["joinEvent"]["actor"]["id"] == to_string(actor_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
|
assert participant.metadata.email == @email
|
||||||
|
|
||||||
|
@ -1093,7 +1101,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
assert %Participant{
|
assert %Participant{
|
||||||
metadata: %{confirmation_token: confirmation_token},
|
metadata: %{confirmation_token: confirmation_token},
|
||||||
role: :not_confirmed
|
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
|
# hack to avoid preloading event in participant
|
||||||
participant = Map.put(participant, :event, event)
|
participant = Map.put(participant, :event, event)
|
||||||
|
@ -1118,7 +1128,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert %Participant{role: :participant} =
|
assert %Participant{role: :participant} =
|
||||||
event.id |> Events.list_participants_for_event() |> hd()
|
event.id |> Events.list_participants_for_event() |> Map.get(:elements) |> hd()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "I can participate anonymously and and confirm my participation with bad token",
|
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"]["event"]["id"] == to_string(event.id)
|
||||||
assert res["data"]["joinEvent"]["actor"]["id"] == to_string(actor_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
|
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 hd(res["errors"])["message"] == "This token is invalid"
|
||||||
|
|
||||||
assert %Participant{role: :not_confirmed} =
|
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
|
end
|
||||||
|
|
||||||
test "I can participate anonymously but change my mind and cancel my participation",
|
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)
|
{: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
|
assert participant.metadata.email == @email
|
||||||
|
|
||||||
|
@ -1205,7 +1219,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
id: participant_id,
|
id: participant_id,
|
||||||
role: :not_confirmed,
|
role: :not_confirmed,
|
||||||
metadata: %{cancellation_token: cancellation_token}
|
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 =
|
res =
|
||||||
conn
|
conn
|
||||||
|
@ -1221,7 +1235,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
assert res["data"]["leaveEvent"]["id"] == participant_id
|
assert res["data"]["leaveEvent"]["id"] == participant_id
|
||||||
|
|
||||||
{:ok, %Event{participant_stats: %{not_confirmed: 0}}} = Events.get_event(event.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
|
end
|
||||||
|
|
||||||
test "I can participate anonymously, confirm my participation and then be confirmed by the organizer",
|
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{
|
assert %Participant{
|
||||||
role: :not_confirmed,
|
role: :not_confirmed,
|
||||||
metadata: %{confirmation_token: confirmation_token, email: @email}
|
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
|
conn
|
||||||
|> AbsintheHelpers.graphql_query(
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
@ -1293,7 +1307,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
}} = Events.get_event(event.id)
|
}} = Events.get_event(event.id)
|
||||||
|
|
||||||
assert %Participant{role: :not_approved, id: participant_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 = """
|
update_participation_mutation = """
|
||||||
mutation UpdateParticipation($participantId: ID!, $role: String!, $moderatorActorId: ID!) {
|
mutation UpdateParticipation($participantId: ID!, $role: String!, $moderatorActorId: ID!) {
|
||||||
|
@ -1325,7 +1339,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
|
||||||
assert %Participant{role: :participant} =
|
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,
|
assert {:ok,
|
||||||
%Event{
|
%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.ends_on == DateTime.from_naive!(~N[2010-04-17 14:00:00Z], "Etc/UTC")
|
||||||
assert event.title == "some title"
|
assert event.title == "some title"
|
||||||
|
|
||||||
assert hd(Events.list_participants_for_event(event.id)).actor.id == actor.id
|
assert %Participant{} =
|
||||||
assert hd(Events.list_participants_for_event(event.id)).role == :creator
|
participant = hd(Events.list_participants_for_event(event.id).elements)
|
||||||
|
|
||||||
|
assert participant.actor.id == actor.id
|
||||||
|
assert participant.role == :creator
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_event/1 with invalid data returns error changeset" do
|
test "create_event/1 with invalid data returns error changeset" do
|
||||||
|
|
Loading…
Reference in a new issue