Add a dropdown on participate menu, disallow listing participations

Now requires quering the person endpoint to know if an actor
participates in an event, organizers can make authenticated requests to
event { participants { } } to see the pending / approved participants.

Also closes #174

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-09-26 16:38:58 +02:00
parent 8a3e606c15
commit 757d2cabec
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
34 changed files with 655 additions and 439 deletions

View File

@ -71,49 +71,10 @@ export default class App extends Vue {
@import "variables"; @import "variables";
/* Bulma imports */ /* Bulma imports */
@import "~bulma/sass/utilities/_all"; @import "~bulma/bulma";
@import "~bulma/sass/base/_all.sass";
@import "~bulma/sass/components/card.sass";
@import "~bulma/sass/components/media.sass";
@import "~bulma/sass/components/message.sass";
@import "~bulma/sass/components/modal.sass";
@import "~bulma/sass/components/navbar.sass";
@import "~bulma/sass/components/pagination.sass";
@import "~bulma/sass/components/dropdown.sass";
@import "~bulma/sass/components/breadcrumb.sass";
@import "~bulma/sass/components/list.sass";
@import "~bulma/sass/components/tabs";
@import "~bulma/sass/elements/box.sass";
@import "~bulma/sass/elements/button.sass";
@import "~bulma/sass/elements/container.sass";
@import "~bulma/sass/form/_all";
@import "~bulma/sass/elements/icon.sass";
@import "~bulma/sass/elements/image.sass";
@import "~bulma/sass/elements/other.sass";
@import "~bulma/sass/elements/progress.sass";
@import "~bulma/sass/elements/tag.sass";
@import "~bulma/sass/elements/title.sass";
@import "~bulma/sass/elements/notification";
@import "~bulma/sass/elements/table";
@import "~bulma/sass/grid/_all.sass";
@import "~bulma/sass/layout/_all.sass";
/* Buefy imports */ /* Buefy imports */
@import "~buefy/src/scss/utils/_all"; @import "~buefy/src/scss/buefy";
@import "~buefy/src/scss/components/datepicker";
@import "~buefy/src/scss/components/notices";
@import "~buefy/src/scss/components/dropdown";
@import "~buefy/src/scss/components/autocomplete";
@import "~buefy/src/scss/components/form";
@import "~buefy/src/scss/components/modal";
@import "~buefy/src/scss/components/progress";
@import "~buefy/src/scss/components/tag";
@import "~buefy/src/scss/components/taginput";
@import "~buefy/src/scss/components/upload";
@import "~buefy/src/scss/components/radio";
@import "~buefy/src/scss/components/switch";
@import "~buefy/src/scss/components/table";
@import "~buefy/src/scss/components/tabs";
.router-enter-active, .router-enter-active,
.router-leave-active { .router-leave-active {

View File

@ -6,7 +6,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
@Component({}) @Component
export default class DateTimePicker extends Vue { export default class DateTimePicker extends Vue {
@Prop({ required: true, type: Date }) value!: Date; @Prop({ required: true, type: Date }) value!: Date;
@Prop({ required: false, type: String, default: 'Datetime' }) label!: string; @Prop({ required: false, type: String, default: 'Datetime' }) label!: string;

View File

@ -1,64 +1,66 @@
<template> <template>
<article class="box columns"> <article class="box">
<div class="content column"> <div class="title-wrapper">
<div class="title-wrapper"> <div class="date-component" v-if="!mergedOptions.hideDate">
<div class="date-component" v-if="!mergedOptions.hideDate"> <date-calendar-icon :date="participation.event.beginsOn" />
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<h2 class="title" ref="title">{{ participation.event.title }}</h2>
</div>
<div>
<span v-if="participation.event.physicalAddress && participation.event.physicalAddress.locality">{{ participation.event.physicalAddress.locality }} - </span>
<span v-if="participation.actor.id === participation.event.organizerActor.id">{{ $t("You're organizing this event") }}</span>
<span v-else>
<span v-if="participation.event.beginsOn < new Date()">{{ $t('Organized by {name}', { name: participation.event.organizerActor.displayName() } ) }}</span>
|
<span>{{ $t('Going as {name}', { name: participation.actor.displayName() }) }}</span>
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if=" participation.event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="lock_opened" v-if=" participation.event.visibility === EventVisibility.RESTRICTED" />
<b-icon icon="lock" v-if=" participation.event.visibility === EventVisibility.PRIVATE" />
</span>
<span class="column">
<span v-if="!participation.event.options.maximumAttendeeCapacity">
{{ $tc('{count} participants', participation.event.participantStats.approved, { count: participation.event.participantStats.approved })}}
</span>
<b-progress
v-if="participation.event.options.maximumAttendeeCapacity > 0"
type="is-primary"
size="is-medium"
:value="participation.event.participantStats.approved * 100 / participation.event.options.maximumAttendeeCapacity" show-value>
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.approved, total: participation.event.options.maximumAttendeeCapacity }) }}
</b-progress>
<span
v-if="participation.event.participantStats.unapproved > 0">
{{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}}
</span>
</span>
</div> </div>
<h2 class="title" ref="title">{{ participation.event.title }}</h2>
</div> </div>
<div class="actions column is-narrow"> <div class="columns">
<ul> <div class="content column">
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))"> <div>
<router-link :to="{ name: EventRouteName.EDIT_EVENT, params: { eventId: participation.event.uuid } }"> <span v-if="participation.event.physicalAddress && participation.event.physicalAddress.locality">{{ participation.event.physicalAddress.locality }} - </span>
<b-icon icon="pencil" /> {{ $t('Edit') }} <span v-if="participation.actor.id === participation.event.organizerActor.id">{{ $t("You're organizing this event") }}</span>
</router-link> <span v-else>
</li> <span v-if="participation.event.beginsOn < new Date()">{{ $t('Organized by {name}', { name: participation.event.organizerActor.displayName() } ) }}</span>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))"> |
<a @click="openDeleteEventModalWrapper"><b-icon icon="delete" /> {{ $t('Delete') }}</a> <span>{{ $t('Going as {name}', { name: participation.actor.displayName() }) }}</span>
</li> </span>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))"> </div>
<router-link :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } }"> <div class="columns">
<b-icon icon="account-multiple-plus" /> {{ $t('Manage participations') }} <span class="column is-narrow">
</router-link> <b-icon icon="earth" v-if=" participation.event.visibility === EventVisibility.PUBLIC" />
</li> <b-icon icon="lock_opened" v-if=" participation.event.visibility === EventVisibility.RESTRICTED" />
<li> <b-icon icon="lock" v-if=" participation.event.visibility === EventVisibility.PRIVATE" />
<router-link :to="{ name: EventRouteName.EVENT, params: { uuid: participation.event.uuid } }"><b-icon icon="view-compact" /> {{ $t('View event page') }}</router-link> </span>
</li> <span class="column">
</ul> <span v-if="!participation.event.options.maximumAttendeeCapacity">
{{ $tc('{count} participants', participation.event.participantStats.approved, { count: participation.event.participantStats.approved })}}
</span>
<b-progress
v-if="participation.event.options.maximumAttendeeCapacity > 0"
type="is-primary"
size="is-medium"
:value="participation.event.participantStats.approved * 100 / participation.event.options.maximumAttendeeCapacity" show-value>
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.approved, total: participation.event.options.maximumAttendeeCapacity }) }}
</b-progress>
<span
v-if="participation.event.participantStats.unapproved > 0">
{{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}}
</span>
</span>
</div>
</div>
<div class="actions column is-narrow">
<ul>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
<router-link :to="{ name: EventRouteName.EDIT_EVENT, params: { eventId: participation.event.uuid } }">
<b-icon icon="pencil" /> {{ $t('Edit') }}
</router-link>
</li>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
<a @click="openDeleteEventModalWrapper"><b-icon icon="delete" /> {{ $t('Delete') }}</a>
</li>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
<router-link :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } }">
<b-icon icon="account-multiple-plus" /> {{ $t('Manage participations') }}
</router-link>
</li>
<li>
<router-link :to="{ name: EventRouteName.EVENT, params: { uuid: participation.event.uuid } }"><b-icon icon="view-compact" /> {{ $t('View event page') }}</router-link>
</li>
</ul>
</div>
</div> </div>
</article> </article>
</template> </template>

View File

@ -0,0 +1,111 @@
<template>
<div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
<button class="button is-success" type="button" slot="trigger">
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave">
{{ $t('Cancel my participation…')}}
</b-dropdown-item>
</b-dropdown>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success" type="button" slot="trigger">
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave">
{{ $t('Cancel my participation request…')}}
</b-dropdown-item>
</b-dropdown>
<small>{{ $t('Participation requested!')}}</small><br />
<small>{{ $t('Waiting for organization team approval.')}}</small>
</div>
<b-dropdown aria-role="list" position="is-bottom-left" v-if="!participation">
<button class="button is-primary" type="button" slot="trigger">
<template>
<span>{{ $t('Participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
<div class="media">
<div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" :src="currentActor.avatar.url" alt="" />
</figure>
</div>
<div class="media-content">
<span>{{ $t('with {identity}', {identity: currentActor.preferredUsername }) }}</span>
</div>
</div>
</b-dropdown-item>
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal">
{{ $t('with another identity…')}}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson } from '@/types/actor';
@Component
export default class ParticipationButton extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) currentActor!: IPerson;
ParticipantRole = ParticipantRole;
joinEvent(actor: IPerson) {
this.$emit('joinEvent', actor);
}
joinModal() {
this.$emit('joinModal');
}
confirmLeave() {
this.$emit('confirmLeave');
}
}
</script>
<style lang="scss">
.participation-button {
.dropdown {
display: flex;
justify-content: flex-end;
&.dropdown-disabled button {
opacity: 0.5;
}
}
button {
font-size: 1.5rem;
}
}
</style>

View File

@ -1,92 +0,0 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t('Join event {title}', {title: event.title}) }}</p>
</header>
<section class="modal-card-body is-flex">
<div class="media">
<div
class="media-left">
<b-icon
icon="alert"
type="is-warning"
size="is-large"/>
</div>
<div class="media-content">
<p>{{ $t('Do you want to participate in {title}?', {title: event.title}) }}?</p>
<b-field :label="$t('Identity')">
<identity-picker v-model="identity"></identity-picker>
</b-field>
<p v-if="event.joinOptions === EventJoinOptions.RESTRICTED">
{{ $t('The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved')}}
</p>
<p v-if="!event.local">
{{ $t('The event came from another instance. Your participation will be confirmed after we confirm it with the other instance.') }}
</p>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button
class="button"
ref="cancelButton"
@click="close">
{{ $t('Cancel') }}
</button>
<button
class="button is-primary"
ref="confirmButton"
@click="confirm">
{{ $t('Confirm my particpation') }}
</button>
</footer>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IEvent, EventJoinOptions } from '@/types/event.model';
import IdentityPicker from '@/views/Account/IdentityPicker.vue';
import { IPerson } from '@/types/actor';
@Component({
components: {
IdentityPicker,
},
mounted() {
this.$data.isActive = true;
},
})
export default class ReportModal extends Vue {
@Prop({ type: Function, default: () => {} }) onConfirm;
@Prop({ type: Object }) event! : IEvent;
@Prop({ type: Object }) defaultIdentity!: IPerson;
isActive: boolean = false;
identity: IPerson = this.defaultIdentity;
EventJoinOptions = EventJoinOptions;
confirm() {
this.onConfirm(this.identity);
}
/**
* Close the Dialog.
*/
close() {
this.isActive = false;
this.$emit('close');
}
}
</script>
<style lang="scss">
.modal-card .modal-card-foot {
justify-content: flex-end;
}
</style>

View File

@ -10,6 +10,9 @@ const participantQuery = `
}, },
name, name,
id id
},
event {
id
} }
`; `;
@ -52,7 +55,7 @@ const optionsQuery = `
`; `;
export const FETCH_EVENT = gql` export const FETCH_EVENT = gql`
query($uuid:UUID!, $roles: String) { query($uuid:UUID!) {
event(uuid: $uuid) { event(uuid: $uuid) {
id, id,
uuid, uuid,
@ -95,9 +98,6 @@ export const FETCH_EVENT = gql`
# preferredUsername, # preferredUsername,
# name, # name,
# }, # },
participants (roles: $roles) {
${participantQuery}
},
participantStats { participantStats {
approved, approved,
unapproved unapproved
@ -363,9 +363,10 @@ export const DELETE_EVENT = gql`
`; `;
export const PARTICIPANTS = gql` export const PARTICIPANTS = gql`
query($uuid: UUID!, $page: Int, $limit: Int, $roles: String) { query($uuid: UUID!, $page: Int, $limit: Int, $roles: String, $actorId: ID!) {
event(uuid: $uuid) { event(uuid: $uuid) {
participants(page: $page, limit: $limit, roles: $roles) { id,
participants(page: $page, limit: $limit, roles: $roles, actorId: $actorId) {
${participantQuery} ${participantQuery}
}, },
participantStats { participantStats {
@ -375,3 +376,21 @@ export const PARTICIPANTS = gql`
} }
} }
`; `;
export const EVENT_PERSON_PARTICIPATION = gql`
query($name: String!, $eventId: ID!) {
person(preferredUsername: $name) {
id,
participations(eventId: $eventId) {
id,
role,
actor {
id
},
event {
id
}
}
}
}
`;

View File

@ -17,8 +17,13 @@
"Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.", "Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.",
"Before you can login, you need to click on the link inside it to validate your account": "Before you can login, you need to click on the link inside it to validate your account", "Before you can login, you need to click on the link inside it to validate your account": "Before you can login, you need to click on the link inside it to validate your account",
"By {name}": "By {name}", "By {name}": "By {name}",
"Cancel my participation request…": "Cancel my participation request…",
"Cancel my participation…": "Cancel my participation…",
"Cancel": "Cancel", "Cancel": "Cancel",
"Category": "Category", "Category": "Category",
"Change my identity…": "Change my identity…",
"Change my password": "Change my password",
"Change password": "Change password",
"Change": "Change", "Change": "Change",
"Clear": "Clear", "Clear": "Clear",
"Click to select": "Click to select", "Click to select": "Click to select",
@ -82,6 +87,7 @@
"Group": "Group", "Group": "Group",
"Groups": "Groups", "Groups": "Groups",
"I create an identity": "I create an identity", "I create an identity": "I create an identity",
"I participate": "I participate",
"I want to approve every participation request": "I want to approve every participation request", "I want to approve every participation request": "I want to approve every participation request",
"Identities": "Identities", "Identities": "Identities",
"Identity {displayName} created": "Identity {displayName} created", "Identity {displayName} created": "Identity {displayName} created",
@ -116,6 +122,7 @@
"My events": "My events", "My events": "My events",
"My identities": "My identities", "My identities": "My identities",
"Name": "Name", "Name": "Name",
"New password": "New password",
"No address defined": "No address defined", "No address defined": "No address defined",
"No events found": "No events found", "No events found": "No events found",
"No group found": "No group found", "No group found": "No group found",
@ -123,6 +130,7 @@
"No participants yet.": "No participants yet.", "No participants yet.": "No participants yet.",
"No results for \"{queryText}\"": "No results for \"{queryText}\"", "No results for \"{queryText}\"": "No results for \"{queryText}\"",
"Number of places": "Number of places", "Number of places": "Number of places",
"Old password": "Old password",
"One person is going": "No one is going | One person is going | {approved} persons are going", "One person is going": "No one is going | One person is going | {approved} persons are going",
"Only accessible through link and search (private)": "Only accessible through link and search (private)", "Only accessible through link and search (private)": "Only accessible through link and search (private)",
"Opened reports": "Opened reports", "Opened reports": "Opened reports",
@ -133,8 +141,11 @@
"Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.", "Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.",
"Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)", "Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)",
"Participants": "Participants", "Participants": "Participants",
"Participate": "Participate",
"Participation approval": "Participation approval", "Participation approval": "Participation approval",
"Participation requested!": "Participation requested!",
"Password (confirmation)": "Password (confirmation)", "Password (confirmation)": "Password (confirmation)",
"Password change": "Password change",
"Password reset": "Password reset", "Password reset": "Password reset",
"Password": "Password", "Password": "Password",
"Past events": "Passed events", "Past events": "Passed events",
@ -187,6 +198,7 @@
"The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved", "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved",
"The event title will be ellipsed.": "The event title will be ellipsed.", "The event title will be ellipsed.": "The event title will be ellipsed.",
"The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.", "The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.",
"The password was successfully changed": "The password was successfully changed",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.",
"The {date} at {time}": "The {date} at {time}", "The {date} at {time}": "The {date} at {time}",
"The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}", "The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}",
@ -208,6 +220,7 @@
"View event page": "View event page", "View event page": "View event page",
"View everything": "View everything", "View everything": "View everything",
"Visible everywhere on the web (public)": "Visible everywhere on the web (public)", "Visible everywhere on the web (public)": "Visible everywhere on the web (public)",
"Waiting for organization team approval.": "Waiting for organization team approval.",
"Waiting list": "Waiting list", "Waiting list": "Waiting list",
"We just sent an email to {email}": "We just sent an email to {email}", "We just sent an email to {email}": "We just sent an email to {email}",
"Website / URL": "Website / URL", "Website / URL": "Website / URL",
@ -220,6 +233,7 @@
"You announced that you're going to this event.": "You announced that you're going to this event.", "You announced that you're going to this event.": "You announced that you're going to this event.",
"You are already logged-in.": "You are already logged-in.", "You are already logged-in.": "You are already logged-in.",
"You are an organizer.": "You are an organizer.", "You are an organizer.": "You are an organizer.",
"You have been disconnected": "You have been disconnected",
"You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days", "You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days",
"You have one event today.": "You have no events today | You have one event today. | You have {count} events today", "You have one event today.": "You have no events today | You have one event today. | You have {count} events today",
"You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow", "You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow",
@ -233,6 +247,8 @@
"e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot", "e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot",
"iCal Feed": "iCal Feed", "iCal Feed": "iCal Feed",
"meditate a bit": "meditate a bit", "meditate a bit": "meditate a bit",
"with another identity…": "with another identity…",
"with {identity}": "with {identity}",
"{actor}'s avatar": "{actor}'s avatar", "{actor}'s avatar": "{actor}'s avatar",
"{approved} / {total} seats": "{approved} / {total} seats", "{approved} / {total} seats": "{approved} / {total} seats",
"{count} participants": "{count} participants", "{count} participants": "{count} participants",

View File

@ -17,8 +17,13 @@
"Are you sure you want to delete this event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet événement ? Cette action ne peut être annulée.", "Are you sure you want to delete this event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet événement ? Cette action ne peut être annulée.",
"Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte", "Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
"By {name}": "Par {name}", "By {name}": "Par {name}",
"Cancel my participation request…": "Cancel my participation request…",
"Cancel my participation…": "Annuler ma participation…",
"Cancel": "Annuler", "Cancel": "Annuler",
"Category": "Catégorie", "Category": "Catégorie",
"Change my identity…": "Changer mon identité…",
"Change my password": "Modifier mon mot de passe",
"Change password": "Modifier mot de passe",
"Change": "Modifier", "Change": "Modifier",
"Clear": "Effacer", "Clear": "Effacer",
"Click to select": "Cliquez pour sélectionner", "Click to select": "Cliquez pour sélectionner",
@ -82,6 +87,7 @@
"Group": "Groupe", "Group": "Groupe",
"Groups": "Groupes", "Groups": "Groupes",
"I create an identity": "Je crée une identité", "I create an identity": "Je crée une identité",
"I participate": "Je participe",
"I want to approve every participation request": "Je veux approuver chaque demande de participation", "I want to approve every participation request": "Je veux approuver chaque demande de participation",
"Identities": "Identités", "Identities": "Identités",
"Identity {displayName} created": "Identité {displayName} créée", "Identity {displayName} created": "Identité {displayName} créée",
@ -116,6 +122,7 @@
"My events": "Mes événements", "My events": "Mes événements",
"My identities": "Mes identités", "My identities": "Mes identités",
"Name": "Nom", "Name": "Nom",
"New password": "Nouveau mot de passe",
"No address defined": "Aucune adresse définie", "No address defined": "Aucune adresse définie",
"No events found": "Aucun événement trouvé", "No events found": "Aucun événement trouvé",
"No group found": "Aucun groupe trouvé", "No group found": "Aucun groupe trouvé",
@ -123,6 +130,7 @@
"No participants yet.": "Pas de participants pour le moment.", "No participants yet.": "Pas de participants pour le moment.",
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »", "No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
"Number of places": "Nombre de places", "Number of places": "Nombre de places",
"Old password": "Ancien mot de passe",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont", "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)", "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Opened reports": "Signalements ouverts", "Opened reports": "Signalements ouverts",
@ -133,8 +141,11 @@
"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)",
"Participants": "Participants", "Participants": "Participants",
"Participate": "Participer",
"Participation approval": "Validation des participations", "Participation approval": "Validation des participations",
"Participation requested!": "Participation demandée !",
"Password (confirmation)": "Mot de passe (confirmation)", "Password (confirmation)": "Mot de passe (confirmation)",
"Password change": "Changement de mot de passe",
"Password reset": "Réinitialisation du mot de passe", "Password reset": "Réinitialisation du mot de passe",
"Password": "Mot de passe", "Password": "Mot de passe",
"Past events": "Événements passés", "Past events": "Événements passés",
@ -187,6 +198,7 @@
"The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "L'organisateur⋅ice de l'événement a choisi d'approuver manuellement les participations à cet événement. Vous recevrez une notification lorsque votre participation sera approuvée", "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "L'organisateur⋅ice de l'événement a choisi d'approuver manuellement les participations à cet événement. Vous recevrez une notification lorsque votre participation sera approuvée",
"The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.", "The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
"The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.", "The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
"The password was successfully changed": "Le mot de passe a été changé avec succès",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
"The {date} at {time}": "Le {date} à {time}", "The {date} at {time}": "Le {date} à {time}",
"The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}", "The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
@ -208,6 +220,7 @@
"View event page": "Voir la page de l'événement", "View event page": "Voir la page de l'événement",
"View everything": "Voir tout", "View everything": "Voir tout",
"Visible everywhere on the web (public)": "Visible partout sur le web (public)", "Visible everywhere on the web (public)": "Visible partout sur le web (public)",
"Waiting for organization team approval.": "En attente d'approbation par l'organisation.",
"Waiting list": "Liste d'attente", "Waiting list": "Liste d'attente",
"We just sent an email to {email}": "Nous venons d'envoyer un email à {email}", "We just sent an email to {email}": "Nous venons d'envoyer un email à {email}",
"Website / URL": "Site web / URL", "Website / URL": "Site web / URL",
@ -220,6 +233,7 @@
"You announced that you're going to this event.": "Vous avez annoncé vous rendre à cet événement.", "You announced that you're going to this event.": "Vous avez annoncé vous rendre à cet événement.",
"You are already logged-in.": "Vous êtes déjà connecté.", "You are already logged-in.": "Vous êtes déjà connecté.",
"You are an organizer.": "Vous êtes un organisateur.", "You are an organizer.": "Vous êtes un organisateur.",
"You have been disconnected": "Vous avez été déconnecté⋅e",
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours", "You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
"You have one event today.": "Vous n'avez pas d'évenement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui", "You have one event today.": "Vous n'avez pas d'évenement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
"You have one event tomorrow.": "Vous n'avez pas d'événement demain | Vous avez un événement demain. | Vous avez {count} événements demain", "You have one event tomorrow.": "Vous n'avez pas d'événement demain | Vous avez un événement demain. | Vous avez {count} événements demain",
@ -233,6 +247,8 @@
"e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot", "e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot",
"iCal Feed": "Flux iCal", "iCal Feed": "Flux iCal",
"meditate a bit": "méditez un peu", "meditate a bit": "méditez un peu",
"with another identity…": "avec une autre identité…",
"with {identity}": "avec {identity}",
"{actor}'s avatar": "Avatar de {actor}", "{actor}'s avatar": "Avatar de {actor}",
"{approved} / {total} seats": "{approved} / {total} places", "{approved} / {total} seats": "{approved} / {total} places",
"{count} participants": "Un⋅e participant⋅e|{count} participant⋅e⋅s", "{count} participants": "Un⋅e participant⋅e|{count} participant⋅e⋅s",

View File

@ -1,5 +1,5 @@
import { ICurrentUser } from '@/types/current-user.model'; import { ICurrentUser } from '@/types/current-user.model';
import { IEvent } from '@/types/event.model'; import { IEvent, IParticipant } from '@/types/event.model';
import { Actor, IActor } from '@/types/actor/actor.model'; import { Actor, IActor } from '@/types/actor/actor.model';
export interface IFeedToken { export interface IFeedToken {
@ -11,11 +11,13 @@ export interface IFeedToken {
export interface IPerson extends IActor { export interface IPerson extends IActor {
feedTokens: IFeedToken[]; feedTokens: IFeedToken[];
goingToEvents: IEvent[]; goingToEvents: IEvent[];
participations: IParticipant[];
} }
export class Person extends Actor implements IPerson { export class Person extends Actor implements IPerson {
feedTokens: IFeedToken[] = []; feedTokens: IFeedToken[] = [];
goingToEvents: IEvent[] = []; goingToEvents: IEvent[] = [];
participations: IParticipant[] = [];
constructor(hash: IPerson | {} = {}) { constructor(hash: IPerson | {} = {}) {
super(hash); super(hash);

View File

@ -1,29 +1,22 @@
<template> <template>
<div class="identity-picker"> <div class="modal-card">
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }} <header class="modal-card-head">
<b-button type="is-text" @click="isComponentModalActive = true"> <p class="modal-card-title">{{ $t('Pick an identity') }}</p>
{{ $t('Change') }} </header>
</b-button> <section class="modal-card-body">
<b-modal :active.sync="isComponentModalActive" has-modal-card> <div class="list is-hoverable">
<div class="modal-card"> <a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
<header class="modal-card-head"> <div class="media">
<p class="modal-card-title">{{ $t('Pick an identity') }}</p> <img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" />
</header> <div class="media-content">
<section class="modal-card-body"> <h3>@{{ identity.preferredUsername }}</h3>
<div class="list is-hoverable"> <small>{{ identity.name }}</small>
<a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)"> </div>
<div class="media">
<img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" />
<div class="media-content">
<h3>@{{ identity.preferredUsername }}</h3>
<small>{{ identity.name }}</small>
</div>
</div>
</a>
</div> </div>
</section> </a>
</div> </div>
</b-modal> </section>
<slot name="footer"></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -40,22 +33,13 @@ import { IDENTITIES } from '@/graphql/actor';
}) })
export default class IdentityPicker extends Vue { export default class IdentityPicker extends Vue {
@Prop() value!: IActor; @Prop() value!: IActor;
isComponentModalActive: boolean = false;
identities: IActor[] = []; identities: IActor[] = [];
currentIdentity: IActor = this.value; currentIdentity: IActor = this.value;
changeCurrentIdentity(identity: IActor) { changeCurrentIdentity(identity: IActor) {
this.currentIdentity = identity; this.currentIdentity = identity;
this.$emit('input', identity); this.$emit('input', identity);
this.isComponentModalActive = false;
} }
} }
</script> </script>
<style lang="scss">
.identity-picker img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<div class="identity-picker">
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
<b-button type="is-text" @click="isComponentModalActive = true">
{{ $t('Change') }}
</b-button>
<b-modal :active.sync="isComponentModalActive" has-modal-card>
<identity-picker :currentIdentity="currentIdentity" @input="relay" />
</b-modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IActor } from '@/types/actor';
import IdentityPicker from './IdentityPicker.vue';
@Component({
components: { IdentityPicker },
})
export default class IdentityPickerWrapper extends Vue {
@Prop() value!: IActor;
isComponentModalActive: boolean = false;
currentIdentity: IActor = this.value;
relay(identity: IActor) {
this.currentIdentity = identity;
this.$emit('input', identity);
this.isComponentModalActive = false;
}
}
</script>
<style lang="scss">
.identity-picker img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
</style>

View File

@ -92,6 +92,7 @@ export default class Register extends Vue {
domain: null, domain: null,
feedTokens: [], feedTokens: [],
goingToEvents: [], goingToEvents: [],
participations: [],
}; };
errors: object = {}; errors: object = {};
validationSent: boolean = false; validationSent: boolean = false;

View File

@ -29,7 +29,7 @@ import {EventJoinOptions} from "@/types/event.model";
<address-auto-complete v-model="event.physicalAddress" /> <address-auto-complete v-model="event.physicalAddress" />
<b-field :label="$t('Organizer')"> <b-field :label="$t('Organizer')">
<identity-picker v-model="event.organizerActor"></identity-picker> <identity-picker-wrapper v-model="event.organizerActor"></identity-picker-wrapper>
</b-field> </b-field>
<div class="field"> <div class="field">
@ -188,7 +188,6 @@ import {
EventModel, EventModel,
EventStatus, EventStatus,
EventVisibility, EventVisibility,
EventVisibilityJoinOptions,
} from '@/types/event.model'; } from '@/types/event.model';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor'; import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { Person } from '@/types/actor'; import { Person } from '@/types/actor';
@ -200,10 +199,10 @@ import { TAGS } from '@/graphql/tags';
import { ITag } from '@/types/tag.model'; import { ITag } from '@/types/tag.model';
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue'; import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
import { buildFileFromIPicture, buildFileVariable } from '@/utils/image'; import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
import IdentityPicker from '@/views/Account/IdentityPicker.vue'; import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
@Component({ @Component({
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor, IdentityPicker }, components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
apollo: { apollo: {
currentActor: { currentActor: {
query: CURRENT_ACTOR_CLIENT, query: CURRENT_ACTOR_CLIENT,

View File

@ -1,3 +1,6 @@
import {ParticipantRole} from "@/types/event.model";
import {ParticipantRole} from "@/types/event.model";
import {ParticipantRole} from "@/types/event.model";
<template> <template>
<div> <div>
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
@ -10,7 +13,7 @@
<img src="https://picsum.photos/600/200/"> <img src="https://picsum.photos/600/200/">
</figure> </figure>
</div> </div>
<section class="container"> <section>
<div class="title-and-participate-button"> <div class="title-and-participate-button">
<div class="title-wrapper"> <div class="title-wrapper">
<div class="date-component"> <div class="date-component">
@ -18,21 +21,21 @@
</div> </div>
<h1 class="title">{{ event.title }}</h1> <h1 class="title">{{ event.title }}</h1>
</div> </div>
<span v-if="event.participantStats.approved > 0 && !actorIsParticipant()"> <div class="has-text-right">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} <small v-if="event.participantStats.approved > 0 && !actorIsParticipant">
</span> {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
<span v-else> </small>
{{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }} <small v-else>
</span> {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }}
<div v-if="!actorIsOrganizer()" class="participate-button has-text-centered"> </small>
<a v-if="!actorIsParticipant()" @click="isJoinModalActive = true" class="button is-large is-primary is-rounded"> <participation-button
<b-icon icon="circle-outline"></b-icon> v-if="currentActor.id && !actorIsOrganizer"
{{ $t('Join') }} :participation="participations[0]"
</a> :current-actor="currentActor"
<a v-if="actorIsParticipant()" @click="confirmLeave()" class="button is-large is-primary is-rounded"> @joinEvent="joinEvent"
<b-icon icon="check-circle"></b-icon> @joinModal="isJoinModalActive = true"
{{ $t('Leave') }} @confirmLeave="confirmLeave"
</a> />
</div> </div>
</div> </div>
<div class="metadata columns"> <div class="metadata columns">
@ -60,8 +63,8 @@
</p> </p>
</div> </div>
<div class="column sidebar"> <div class="column sidebar">
<div class="field has-addons"> <div class="field has-addons" v-if="currentActor.id">
<p class="control" v-if="actorIsOrganizer()"> <p class="control" v-if="actorIsOrganizer">
<router-link <router-link
class="button" class="button"
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}" :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
@ -69,7 +72,7 @@
{{ $t('Edit') }} {{ $t('Edit') }}
</router-link> </router-link>
</p> </p>
<p class="control" v-if="actorIsOrganizer()"> <p class="control" v-if="actorIsOrganizer">
<a class="button is-danger" @click="openDeleteEventModalWrapper"> <a class="button is-danger" @click="openDeleteEventModalWrapper">
{{ $t('Delete') }} {{ $t('Delete') }}
</a> </a>
@ -133,26 +136,6 @@
</div> </div>
</div> </div>
</div> </div>
<section class="container">
<h3 class="title">{{ $t('Participants') }}</h3>
<router-link v-if="currentActor.id === event.organizerActor.id" :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: event.uuid } }">
{{ $t('Manage participants') }}
</router-link>
<span v-if="event.participants.length === 0">{{ $t('No participants yet.') }}</span>
<div class="columns">
<div
class="column"
v-for="participant in event.participants"
:key="participant.id"
>
<figure class="image is-48x48">
<img v-if="!participant.actor.avatar.url" src="https://picsum.photos/48/48/" class="is-rounded">
<img v-else :src="participant.actor.avatar.url" class="is-rounded">
</figure>
<span>{{ participant.actor.preferredUsername }}</span>
</div>
</div>
</section>
<section class="share"> <section class="share">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
@ -188,19 +171,35 @@
<report-modal :on-confirm="reportEvent" :title="$t('Report this event')" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" /> <report-modal :on-confirm="reportEvent" :title="$t('Report this event')" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" />
</b-modal> </b-modal>
<b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal"> <b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal">
<participation-modal :on-confirm="joinEvent" :event="event" :defaultIdentity="currentActor" @close="$refs.participationModal.close()" /> <identity-picker v-model="identity">
<template v-slot:footer>
<footer class="modal-card-foot">
<button
class="button"
ref="cancelButton"
@click="isJoinModalActive = false">
{{ $t('Cancel') }}
</button>
<button
class="button is-primary"
ref="confirmButton"
@click="joinEvent(identity)">
{{ $t('Confirm my particpation') }}
</button>
</footer>
</template>
</identity-picker>
</b-modal> </b-modal>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event'; import { EVENT_PERSON_PARTICIPATION, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop } from 'vue-property-decorator';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor'; import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model'; import { EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson } from '@/types/actor'; import { IPerson, Person } from '@/types/actor';
import { RouteName } from '@/router';
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';
import BIcon from 'buefy/src/components/icon/Icon.vue'; import BIcon from 'buefy/src/components/icon/Icon.vue';
@ -208,11 +207,11 @@ import EventCard from '@/components/Event/EventCard.vue';
import EventFullDate from '@/components/Event/EventFullDate.vue'; import EventFullDate from '@/components/Event/EventFullDate.vue';
import ActorLink from '@/components/Account/ActorLink.vue'; import ActorLink from '@/components/Account/ActorLink.vue';
import ReportModal from '@/components/Report/ReportModal.vue'; import ReportModal from '@/components/Report/ReportModal.vue';
import ParticipationModal from '@/components/Event/ParticipationModal.vue';
import { IReport } from '@/types/report.model'; import { IReport } from '@/types/report.model';
import { CREATE_REPORT } from '@/graphql/report'; import { CREATE_REPORT } from '@/graphql/report';
import EventMixin from '@/mixins/event'; import EventMixin from '@/mixins/event';
import { EventRouteName } from '@/router/event'; import IdentityPicker from '@/views/Account/IdentityPicker.vue';
import ParticipationButton from '@/components/Event/ParticipationButton.vue';
@Component({ @Component({
components: { components: {
@ -222,7 +221,8 @@ import { EventRouteName } from '@/router/event';
BIcon, BIcon,
DateCalendarIcon, DateCalendarIcon,
ReportModal, ReportModal,
ParticipationModal, IdentityPicker,
ParticipationButton,
// tslint:disable:space-in-parens // tslint:disable:space-in-parens
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'), 'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
// tslint:enable // tslint:enable
@ -233,13 +233,25 @@ import { EventRouteName } from '@/router/event';
variables() { variables() {
return { return {
uuid: this.uuid, uuid: this.uuid,
roles: [ParticipantRole.CREATOR, ParticipantRole.MODERATOR, ParticipantRole.MODERATOR, ParticipantRole.PARTICIPANT].join(),
}; };
}, },
}, },
currentActor: { currentActor: {
query: CURRENT_ACTOR_CLIENT, query: CURRENT_ACTOR_CLIENT,
}, },
participations: {
query: EVENT_PERSON_PARTICIPATION,
variables() {
return {
eventId: this.event.id,
name: this.currentActor.preferredUsername,
};
},
update: (data) => {
if (data && data.person) return data.person.participations;
return [];
},
},
}, },
}) })
export default class Event extends EventMixin { export default class Event extends EventMixin {
@ -247,13 +259,17 @@ export default class Event extends EventMixin {
event!: IEvent; event!: IEvent;
currentActor!: IPerson; currentActor!: IPerson;
validationSent: boolean = false; identity: IPerson = new Person();
participations: IParticipant[] = [];
showMap: boolean = false; showMap: boolean = false;
isReportModalActive: boolean = false; isReportModalActive: boolean = false;
isJoinModalActive: boolean = false; isJoinModalActive: boolean = false;
EventVisibility = EventVisibility; EventVisibility = EventVisibility;
EventRouteName = EventRouteName;
mounted() {
this.identity = this.currentActor;
}
/** /**
* Delete the event, then redirect to home. * Delete the event, then redirect to home.
@ -298,6 +314,24 @@ export default class Event extends EventMixin {
}, },
update: (store, { data }) => { update: (store, { data }) => {
if (data == null) return; if (data == null) return;
const participationCachedData = store.readQuery<{ person: IPerson }>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, name: identity.preferredUsername },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error('Cannot update participation cache, because of null value.');
return;
}
person.participations.push(data.joinEvent);
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, name: identity.preferredUsername },
data: { person },
});
const cachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } }); const cachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
if (cachedData == null) return; if (cachedData == null) return;
const { event } = cachedData; const { event } = cachedData;
@ -306,9 +340,13 @@ export default class Event extends EventMixin {
return; return;
} }
event.participants = event.participants.concat([data.joinEvent]); if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved + 1;
} else {
event.participantStats.approved = event.participantStats.approved + 1;
}
store.writeQuery({ query: FETCH_EVENT, data: { event } }); store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
}, },
}); });
} catch (error) { } catch (error) {
@ -338,19 +376,38 @@ export default class Event extends EventMixin {
}, },
update: (store, { data }) => { update: (store, { data }) => {
if (data == null) return; if (data == null) return;
const cachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
if (cachedData == null) return; const participationCachedData = store.readQuery<{ person: IPerson }>({
const { event } = cachedData; query: EVENT_PERSON_PARTICIPATION,
if (event === null) { variables: { eventId: this.event.id, name: this.currentActor.preferredUsername },
console.error('Cannot update event participant cache, because of null value.'); });
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error('Cannot update participation cache, because of null value.');
return; return;
} }
const participation = person.participations[0];
person.participations = [];
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, name: this.currentActor.preferredUsername },
data: { person },
});
event.participants = event.participants const eventCachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
.filter(p => p.actor.id !== data.leaveEvent.actor.id); if (eventCachedData == null) return;
event.participantStats.approved = event.participantStats.approved - 1; const { event } = eventCachedData;
if (event === null) {
store.writeQuery({ query: FETCH_EVENT, data: { event } }); console.error('Cannot update event cache, because of null value.');
return;
}
if (participation.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved - 1;
} else {
event.participantStats.approved = event.participantStats.approved - 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
}, },
}); });
} catch (error) { } catch (error) {
@ -369,17 +426,14 @@ export default class Event extends EventMixin {
document.body.removeChild(link); document.body.removeChild(link);
} }
actorIsParticipant() { get actorIsParticipant() {
if (this.actorIsOrganizer()) return true; if (this.actorIsOrganizer) return true;
return this.currentActor && return this.participations.length > 0 && this.participations[0].role === ParticipantRole.PARTICIPANT;
this.event.participants
.some(participant => participant.actor.id === this.currentActor.id);
} }
actorIsOrganizer() { get actorIsOrganizer() {
return this.currentActor && this.event.organizerActor && return this.participations.length > 0 && this.participations[0].role === ParticipantRole.CREATOR;
this.currentActor.id === this.event.organizerActor.id;
} }
get twitterShareUrl(): string { get twitterShareUrl(): string {

View File

@ -68,6 +68,7 @@ import { IPerson } from '@/types/actor';
page: 1, page: 1,
limit: 10, limit: 10,
roles: [ParticipantRole.PARTICIPANT].join(), roles: [ParticipantRole.PARTICIPANT].join(),
actorId: this.currentActor.id,
}; };
}, },
}, },
@ -79,6 +80,7 @@ import { IPerson } from '@/types/actor';
page: 1, page: 1,
limit: 20, limit: 20,
roles: [ParticipantRole.CREATOR].join(), roles: [ParticipantRole.CREATOR].join(),
actorId: this.currentActor.id,
}; };
}, },
update: data => data.event.participants.map(participation => new Participant(participation)), update: data => data.event.participants.map(participation => new Participant(participation)),
@ -91,6 +93,7 @@ import { IPerson } from '@/types/actor';
page: 1, page: 1,
limit: 20, limit: 20,
roles: [ParticipantRole.NOT_APPROVED].join(), roles: [ParticipantRole.NOT_APPROVED].join(),
actorId: this.currentActor.id,
}; };
}, },
update: data => data.event.participants.map(participation => new Participant(participation)), update: data => data.event.participants.map(participation => new Participant(participation)),

View File

@ -35,7 +35,6 @@
<h3 class="title"> <h3 class="title">
{{ $t("Upcoming") }} {{ $t("Upcoming") }}
</h3> </h3>
<pre>{{ Array.from(goingToEvents.entries()) }}</pre>
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<div v-for="row in goingToEvents" class="upcoming-events"> <div v-for="row in goingToEvents" class="upcoming-events">
<span class="date-component-container" v-if="isInLessThanSevenDays(row[0])"> <span class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
@ -53,13 +52,12 @@
{{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }} {{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }}
</h3> </h3>
</span> </span>
<div class="level"> <div>
<EventListCard <EventListCard
v-for="participation in row[1]" v-for="participation in row[1]"
v-if="isInLessThanSevenDays(row[0])" v-if="isInLessThanSevenDays(row[0])"
:key="participation[1].event.uuid" :key="participation[1].event.uuid"
:participation="participation[1]" :participation="participation[1]"
class="level-item"
/> />
</div> </div>
</div> </div>
@ -72,12 +70,11 @@
{{ $t("Last week") }} {{ $t("Last week") }}
</h3> </h3>
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<div class="level"> <div>
<EventListCard <EventListCard
v-for="participation in lastWeekEvents" v-for="participation in lastWeekEvents"
:key="participation.id" :key="participation.id"
:participation="participation" :participation="participation"
class="level-item"
:options="{ hideDate: false }" :options="{ hideDate: false }"
/> />
</div> </div>
@ -295,12 +292,6 @@ export default class Home extends Vue {
} }
} }
.upcoming-events {
.level {
margin-left: 4rem;
}
}
section.container { section.container {
margin: auto auto 3rem; margin: auto auto 3rem;
} }

View File

@ -169,7 +169,7 @@ export default class Report extends Vue {
report.notes = report.notes.concat([note]); report.notes = report.notes.concat([note]);
store.writeQuery({ query: REPORT, data: { report } }); store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
}, },
}); });
@ -235,7 +235,7 @@ export default class Report extends Vue {
const updatedReport = data.updateReportStatus; const updatedReport = data.updateReportStatus;
report.status = updatedReport.status; report.status = updatedReport.status;
store.writeQuery({ query: REPORT, data: { report } }); store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -593,12 +593,12 @@ defmodule Mobilizon.Events do
@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()] [Participant.t()]
def list_participants_for_event( def list_participants_for_event(
uuid, id,
roles \\ @default_participant_roles, roles \\ @default_participant_roles,
page \\ nil, page \\ nil,
limit \\ nil limit \\ nil
) do ) do
uuid id
|> list_participants_for_event_query() |> list_participants_for_event_query()
|> filter_role(roles) |> filter_role(roles)
|> Page.paginate(page, limit) |> Page.paginate(page, limit)
@ -688,7 +688,7 @@ defmodule Mobilizon.Events do
Returns the list of participations for an actor. Returns the list of participations for an actor.
""" """
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) :: @spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) ::
[Event.t()] [Participant.t()]
def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
actor_id actor_id
|> event_participations_for_actor_query() |> event_participations_for_actor_query()
@ -1241,13 +1241,11 @@ defmodule Mobilizon.Events do
@spec event_participations_for_actor_query(integer) :: Ecto.Query.t() @spec event_participations_for_actor_query(integer) :: Ecto.Query.t()
def event_participations_for_actor_query(actor_id) do def event_participations_for_actor_query(actor_id) do
from( from(
e in Event, p in Participant,
join: p in Participant, join: e in Event,
join: a in Actor,
on: p.actor_id == a.id,
on: p.event_id == e.id, on: p.event_id == e.id,
where: a.id == ^actor_id and p.role != ^:not_approved, where: p.actor_id == ^actor_id and p.role != ^:not_approved,
preload: [:picture, :tags] preload: [:event]
) )
end end
@ -1281,12 +1279,12 @@ defmodule Mobilizon.Events do
end end
@spec list_participants_for_event_query(String.t()) :: Ecto.Query.t() @spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
defp list_participants_for_event_query(event_uuid) do defp list_participants_for_event_query(event_id) do
from( from(
p in Participant, p in Participant,
join: e in Event, join: e in Event,
on: p.event_id == e.id, on: p.event_id == e.id,
where: e.uuid == ^event_uuid, where: e.id == ^event_id,
preload: [:actor] preload: [:actor]
) )
end end

View File

@ -38,34 +38,42 @@ defmodule MobilizonWeb.Resolvers.Event do
end end
end end
@doc """
List participant for event (separate request)
"""
def list_participants_for_event(_parent, %{uuid: uuid, page: page, limit: limit}, _resolution) do
{:ok, Mobilizon.Events.list_participants_for_event(uuid, [], page, limit)}
end
@doc """ @doc """
List participants for event (through an event request) List participants for event (through an event request)
""" """
def list_participants_for_event( def list_participants_for_event(
%Event{uuid: uuid}, %Event{id: event_id},
%{page: page, limit: limit, roles: roles}, %{page: page, limit: limit, roles: roles, actor_id: actor_id},
_resolution %{context: %{current_user: %User{} = user}} = _resolution
) do ) do
roles = with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
case roles do # Check that moderator has right
"" -> {:actor_approve_permission, true} <-
[] {:actor_approve_permission, Mobilizon.Events.moderator_for_event?(event_id, actor_id)} do
roles =
case roles do
"" ->
[]
roles -> roles ->
roles roles
|> String.split(",") |> String.split(",")
|> Enum.map(&String.downcase/1) |> Enum.map(&String.downcase/1)
|> Enum.map(&String.to_existing_atom/1) |> Enum.map(&String.to_existing_atom/1)
end end
{:ok, Mobilizon.Events.list_participants_for_event(uuid, roles, page, limit)} {:ok, Mobilizon.Events.list_participants_for_event(event_id, roles, page, limit)}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
end
end
def list_participants_for_event(_, _args, _resolution) do
{:ok, []}
end end
def stats_participants_for_event(%Event{id: id}, _args, _resolution) do def stats_participants_for_event(%Event{id: id}, _args, _resolution) do

View File

@ -6,6 +6,7 @@ defmodule MobilizonWeb.Resolvers.Person do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.Participant
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -173,27 +174,33 @@ defmodule MobilizonWeb.Resolvers.Person do
end end
@doc """ @doc """
Returns the list of events this person is going to Returns the participation for a specific event
""" """
def person_going_to_events(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do def person_participations(%Actor{id: actor_id}, %{event_id: event_id}, %{
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), context: %{current_user: user}
events <- Events.list_event_participations_for_actor(actor) do }) do
{:ok, events} with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
{:no_participant, {:ok, %Participant{} = participant}} <-
{:no_participant, Events.get_participant(event_id, actor_id)} do
{:ok, [participant]}
else else
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"} {:error, "Actor id is not owned by authenticated user"}
{:no_participant, _} ->
{:ok, []}
end end
end end
@doc """ @doc """
Returns the list of events this person is going to Returns the list of events this person is going to
""" """
def person_going_to_events(_parent, %{}, %{context: %{current_user: user}}) do def person_participations(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
with %Actor{} = actor <- Users.get_actor_for_user(user), with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
events <- Events.list_event_participations_for_actor(actor) do participations <- Events.list_event_participations_for_actor(actor) do
{:ok, events} {:ok, participations}
else else
{:is_owned, false} -> {:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"} {:error, "Actor id is not owned by authenticated user"}
end end
end end

View File

@ -225,10 +225,11 @@ defmodule MobilizonWeb.Resolvers.User do
@doc """ @doc """
Returns the list of events for all of this user's identities are going to Returns the list of events for all of this user's identities are going to
""" """
def user_participations(_parent, args, %{ def user_participations(%User{id: user_id}, args, %{
context: %{current_user: %User{id: user_id}} context: %{current_user: %User{id: logged_user_id}}
}) do }) do
with participations <- with true <- user_id == logged_user_id,
participations <-
Events.list_participations_for_user( Events.list_participations_for_user(
user_id, user_id,
Map.get(args, :after_datetime), Map.get(args, :after_datetime),

View File

@ -116,7 +116,6 @@ defmodule MobilizonWeb.Schema do
import_fields(:person_queries) import_fields(:person_queries)
import_fields(:group_queries) import_fields(:group_queries)
import_fields(:event_queries) import_fields(:event_queries)
import_fields(:participant_queries)
import_fields(:tag_queries) import_fields(:tag_queries)
import_fields(:address_queries) import_fields(:address_queries)
import_fields(:config_queries) import_fields(:config_queries)

View File

@ -56,8 +56,11 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
) )
@desc "The list of events this person goes to" @desc "The list of events this person goes to"
field :going_to_events, list_of(:event) do field(:participations, list_of(:participant),
resolve(&Person.person_going_to_events/3) description: "The list of events this person goes to"
) do
arg(:event_id, :id)
resolve(&Person.person_participations/3)
end end
end end

View File

@ -61,6 +61,7 @@ defmodule MobilizonWeb.Schema.EventType 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: "")
arg(:actor_id, :id)
resolve(&Event.list_participants_for_event/3) resolve(&Event.list_participants_for_event/3)
end end

View File

@ -44,16 +44,6 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do
field(:actor, :deleted_object) field(:actor, :deleted_object)
end end
object :participant_queries do
@desc "Get all participants for an event uuid"
field :participants, list_of(:participant) do
arg(:uuid, non_null(:uuid))
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Event.list_participants_for_event/3)
end
end
object :participant_mutations do object :participant_mutations do
@desc "Join an event" @desc "Join an event"
field :join_event, :participant do field :join_event, :participant do

View File

@ -47,7 +47,7 @@ defmodule MobilizonWeb.Schema.UserType do
field(:role, :user_role, description: "The role for the user") field(:role, :user_role, description: "The role for the user")
field(:participations, list_of(:participant), field(:participations, list_of(:participant),
description: "The list of events this person goes to" description: "The list of events this user goes to"
) do ) do
arg(:after_datetime, :datetime) arg(:after_datetime, :datetime)
arg(:before_datetime, :datetime) arg(:before_datetime, :datetime)

View File

@ -128,14 +128,18 @@ defmodule Mobilizon.Service.Export.Feed do
%FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do %FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
case actor do case actor do
%Actor{} = actor -> %Actor{} = actor ->
events = fetch_identity_going_to_events(actor) events = actor |> fetch_identity_participations() |> participations_to_events()
{:ok, build_actor_feed(actor, events, false)} {:ok, build_actor_feed(actor, events, false)}
nil -> nil ->
with actors <- Users.get_actors_for_user(user), with actors <- Users.get_actors_for_user(user),
events <- events <-
actors actors
|> Enum.map(&Events.list_event_participations_for_actor/1) |> Enum.map(fn actor ->
actor
|> Events.list_event_participations_for_actor()
|> participations_to_events()
end)
|> Enum.concat() do |> Enum.concat() do
{:ok, build_user_feed(events, user, token)} {:ok, build_user_feed(events, user, token)}
end end
@ -143,12 +147,18 @@ defmodule Mobilizon.Service.Export.Feed do
end end
end end
defp fetch_identity_going_to_events(%Actor{} = actor) do defp fetch_identity_participations(%Actor{} = actor) do
with events <- Events.list_event_participations_for_actor(actor) do with events <- Events.list_event_participations_for_actor(actor) do
events events
end end
end end
defp participations_to_events(participations) do
participations
|> Enum.map(& &1.event_id)
|> Enum.map(&Events.get_event_with_preload!/1)
end
# Build an atom feed from actor and it's public events # Build an atom feed from actor and it's public events
@spec build_user_feed(list(), User.t(), String.t()) :: String.t() @spec build_user_feed(list(), User.t(), String.t()) :: String.t()
defp build_user_feed(events, %User{email: email}, token) do defp build_user_feed(events, %User{email: email}, token) do

View File

@ -33,7 +33,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
dtend: event.ends_on, dtend: event.ends_on,
description: event.description, description: event.description,
uid: event.uuid, uid: event.uuid,
categories: [event.category] ++ (event.tags |> Enum.map(& &1.slug)) categories: event.tags |> Enum.map(& &1.slug)
} }
end end
@ -52,7 +52,8 @@ defmodule Mobilizon.Service.Export.ICalendar do
@spec export_private_actor(Actor.t()) :: String.t() @spec export_private_actor(Actor.t()) :: String.t()
def export_private_actor(%Actor{} = actor) do def export_private_actor(%Actor{} = actor) do
with events <- Events.list_event_participations_for_actor(actor) do with events <-
actor |> Events.list_event_participations_for_actor() |> participations_to_events() do
{:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
end end
end end
@ -107,7 +108,11 @@ defmodule Mobilizon.Service.Export.ICalendar do
with actors <- Users.get_actors_for_user(user), with actors <- Users.get_actors_for_user(user),
events <- events <-
actors actors
|> Enum.map(&Events.list_event_participations_for_actor/1) |> Enum.map(fn actor ->
actor
|> Events.list_event_participations_for_actor()
|> participations_to_events()
end)
|> Enum.concat() do |> Enum.concat() do
{:ok, {:ok,
%ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
@ -115,4 +120,10 @@ defmodule Mobilizon.Service.Export.ICalendar do
end end
end end
end end
defp participations_to_events(participations) do
participations
|> Enum.map(& &1.event_id)
|> Enum.map(&Events.get_event_with_preload!/1)
end
end end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api # source: http://localhost:4000/api
# timestamp: Tue Sep 24 2019 18:20:05 GMT+0200 (GMT+02:00) # timestamp: Wed Sep 25 2019 16:41:05 GMT+0200 (GMT+02:00)
schema { schema {
query: RootQueryType query: RootQueryType
@ -287,7 +287,7 @@ type Event implements ActionLogObject {
participantStats: ParticipantStats participantStats: ParticipantStats
"""The event's participants""" """The event's participants"""
participants(limit: Int = 10, page: Int = 1, roles: String = ""): [Participant] participants(actorId: ID, limit: Int = 10, page: Int = 1, roles: String = ""): [Participant]
"""Phone address for the event""" """Phone address for the event"""
phoneAddress: String phoneAddress: String
@ -710,9 +710,6 @@ type Person implements Actor {
"""Number of actors following this actor""" """Number of actors following this actor"""
followingCount: Int followingCount: Int
"""The list of events this person goes to"""
goingToEvents: [Event]
"""Internal ID for this person""" """Internal ID for this person"""
id: ID id: ID
@ -734,6 +731,9 @@ type Person implements Actor {
"""A list of the events this actor has organized""" """A list of the events this actor has organized"""
organizedEvents: [Event] organizedEvents: [Event]
"""The list of events this person goes to"""
participations(eventId: ID): [Participant]
"""The actor's preferred username""" """The actor's preferred username"""
preferredUsername: String preferredUsername: String
@ -1128,9 +1128,6 @@ type RootQueryType {
"""Get the current user""" """Get the current user"""
loggedUser: User loggedUser: User
"""Get all participants for an event uuid"""
participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant]
"""Get a person by it's preferred username""" """Get a person by it's preferred username"""
person(preferredUsername: String!): Person person(preferredUsername: String!): Person
@ -1223,7 +1220,7 @@ type User {
"""The user's ID""" """The user's ID"""
id: ID! id: ID!
"""The list of events this person goes to""" """The list of events this user goes to"""
participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant] participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant]
"""The user's list of profiles (identities)""" """The user's list of profiles (identities)"""

View File

@ -99,8 +99,8 @@ 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.uuid)).actor.id == actor.id assert hd(Events.list_participants_for_event(event.id)).actor.id == actor.id
assert hd(Events.list_participants_for_event(event.uuid)).role == :creator assert hd(Events.list_participants_for_event(event.id)).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

View File

@ -784,7 +784,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert :error == Transmogrifier.handle_incoming(reject_data) assert :error == Transmogrifier.handle_incoming(reject_data)
# Organiser is not present since we use factories directly # Organiser is not present since we use factories directly
assert Events.list_participants_for_event(event.uuid) |> Enum.map(& &1.id) == assert Events.list_participants_for_event(event.id) |> Enum.map(& &1.id) ==
[] []
end end
@ -812,7 +812,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert activity.data["actor"] == participant_url assert activity.data["actor"] == participant_url
# The only participant left is the organizer # The only participant left is the organizer
assert Events.list_participants_for_event(event.uuid) |> Enum.map(& &1.id) == [ assert Events.list_participants_for_event(event.id) |> Enum.map(& &1.id) == [
organizer_participation.id organizer_participation.id
] ]
end end

View File

@ -106,8 +106,8 @@ defmodule MobilizonWeb.FeedControllerTest do
assert entry.summary in [event1.title, event2.title] assert entry.summary in [event1.title, event2.title]
end) end)
assert entry1.categories == [event1.category, tag1.slug] assert entry1.categories == [tag1.slug]
assert entry2.categories == [event2.category, tag1.slug, tag2.slug] assert entry2.categories == [tag1.slug, tag2.slug]
end end
test "it returns a 404 page for the actor's public events iCal feed with an actor not publicly visible", test "it returns a 404 page for the actor's public events iCal feed with an actor not publicly visible",
@ -174,7 +174,7 @@ defmodule MobilizonWeb.FeedControllerTest do
assert entry1.summary == event1.title assert entry1.summary == event1.title
assert entry1.categories == [event1.category, tag1.slug, tag2.slug] assert entry1.categories == [tag1.slug, tag2.slug]
end end
end end
@ -311,6 +311,7 @@ defmodule MobilizonWeb.FeedControllerTest do
[entry1] = ExIcal.parse(conn.resp_body) [entry1] = ExIcal.parse(conn.resp_body)
assert entry1.summary == event1.title assert entry1.summary == event1.title
assert entry1.categories == event1.tags |> Enum.map(& &1.slug)
end end
test "it returns 404 for an not existing feed", %{conn: conn} do test "it returns 404 for an not existing feed", %{conn: conn} do

View File

@ -129,13 +129,15 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
actor: actor actor: actor
} do } do
event = insert(:event, %{organizer_actor: actor}) event = insert(:event, %{organizer_actor: actor})
participant = insert(:participant, %{actor: actor, event: event}) insert(:participant, %{actor: actor, event: event, role: :creator})
participant2 = insert(:participant, %{event: event}) user2 = insert(:user)
actor2 = insert(:actor, user: user2)
participant2 = insert(:participant, %{event: event, actor: actor2, role: :participant})
mutation = """ mutation = """
mutation { mutation {
leaveEvent( leaveEvent(
actor_id: #{participant.actor.id}, actor_id: #{participant2.actor.id},
event_id: #{event.id} event_id: #{event.id}
) { ) {
actor { actor {
@ -150,40 +152,64 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user2)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["leaveEvent"]["event"]["id"] == to_string(event.id) assert json_response(res, 200)["data"]["leaveEvent"]["event"]["id"] == to_string(event.id)
assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] == assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] ==
to_string(participant.actor.id) to_string(participant2.actor.id)
query = """ query = """
{ {
event(uuid: "#{event.uuid}") { person(preferredUsername: "#{actor.preferred_username}") {
participants { participations(eventId: "#{event.id}") {
role, event {
actor { uuid,
preferredUsername title
},
role
} }
}
} }
} }
""" """
res = res =
conn conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
assert json_response(res, 200)["data"]["event"]["participants"] == [ assert json_response(res, 200)["data"]["person"]["participations"] == [
%{ %{
"actor" => %{ "event" => %{
"preferredUsername" => participant2.actor.preferred_username "uuid" => event.uuid,
"title" => event.title
}, },
"role" => "CREATOR" "role" => "CREATOR"
} }
] ]
query = """
{
person(preferredUsername: "#{actor2.preferred_username}") {
participations(eventId: "#{event.id}") {
event {
uuid,
title
},
role
}
}
}
"""
res =
conn
|> auth_conn(user2)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
assert json_response(res, 200)["data"]["person"]["participations"] == []
end end
test "actor_leave_event/3 should check if the participant is the only creator", %{ test "actor_leave_event/3 should check if the participant is the only creator", %{
@ -324,17 +350,23 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
assert hd(json_response(res, 200)["errors"])["message"] =~ "Participant not found" assert hd(json_response(res, 200)["errors"])["message"] =~ "Participant not found"
end end
test "list_participants_for_event/3 returns participants for an event", context do test "list_participants_for_event/3 returns participants for an event", %{
conn: conn,
actor: actor,
user: user
} do
event = event =
@event @event
|> Map.put(:organizer_actor_id, context.actor.id) |> Map.put(:organizer_actor_id, actor.id)
{:ok, event} = Events.create_event(event) {:ok, event} = Events.create_event(event)
query = """ query = """
{ {
event(uuid: "#{event.uuid}") { event(uuid: "#{event.uuid}") {
participants(roles: "participant,moderator,administrator,creator") { participants(roles: "participant,moderator,administrator,creator", actor_id: "#{
actor.id
}") {
role, role,
actor { actor {
preferredUsername preferredUsername
@ -345,13 +377,16 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
""" """
res = res =
context.conn conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["event"]["participants"] == [ assert json_response(res, 200)["data"]["event"]["participants"] == [
%{ %{
"actor" => %{ "actor" => %{
"preferredUsername" => context.actor.preferred_username "preferredUsername" => actor.preferred_username
}, },
"role" => "CREATOR" "role" => "CREATOR"
} }
@ -368,7 +403,9 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
query = """ query = """
{ {
event(uuid: "#{event.uuid}") { event(uuid: "#{event.uuid}") {
participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator") { participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
actor.id
}") {
role, role,
actor { actor {
preferredUsername preferredUsername
@ -379,7 +416,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
""" """
res = res =
context.conn conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
sorted_participants = sorted_participants =
@ -402,7 +440,9 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
query = """ query = """
{ {
event(uuid: "#{event.uuid}") { event(uuid: "#{event.uuid}") {
participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator") { participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
actor.id
}") {
role, role,
actor { actor {
preferredUsername preferredUsername
@ -413,7 +453,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
""" """
res = res =
context.conn conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
sorted_participants = sorted_participants =
@ -427,7 +468,7 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
assert sorted_participants == [ assert sorted_participants == [
%{ %{
"actor" => %{ "actor" => %{
"preferredUsername" => context.actor.preferred_username "preferredUsername" => actor.preferred_username
}, },
"role" => "CREATOR" "role" => "CREATOR"
} }

View File

@ -473,7 +473,9 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
assert hd(json_response(res, 200)["errors"])["message"] == "Person with name riri not found" assert hd(json_response(res, 200)["errors"])["message"] == "Person with name riri not found"
end end
end
describe "get_current_person/3" do
test "get_current_person/3 can return the events the person is going to", context do test "get_current_person/3 can return the events the person is going to", context do
user = insert(:user) user = insert(:user)
actor = insert(:actor, user: user) actor = insert(:actor, user: user)
@ -481,9 +483,11 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
query = """ query = """
{ {
loggedPerson { loggedPerson {
goingToEvents { participations {
uuid, event {
title uuid,
title
}
} }
} }
} }
@ -494,7 +498,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
|> auth_conn(user) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person")) |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == [] assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == []
event = insert(:event, %{organizer_actor: actor}) event = insert(:event, %{organizer_actor: actor})
insert(:participant, %{actor: actor, event: event}) insert(:participant, %{actor: actor, event: event})
@ -504,8 +508,8 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
|> auth_conn(user) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person")) |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == [ assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == [
%{"title" => event.title, "uuid" => event.uuid} %{"event" => %{"title" => event.title, "uuid" => event.uuid}}
] ]
end end
@ -519,9 +523,11 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
query = """ query = """
{ {
person(preferredUsername: "#{actor.preferred_username}") { person(preferredUsername: "#{actor.preferred_username}") {
goingToEvents { participations {
uuid, event {
title uuid,
title
}
} }
} }
} }
@ -532,14 +538,16 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
|> auth_conn(user) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person")) |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
assert json_response(res, 200)["data"]["person"]["goingToEvents"] == [] assert json_response(res, 200)["data"]["person"]["participations"] == []
query = """ query = """
{ {
person(preferredUsername: "#{actor_from_other_user.preferred_username}") { person(preferredUsername: "#{actor_from_other_user.preferred_username}") {
goingToEvents { participations {
uuid, event {
title uuid,
title
}
} }
} }
} }
@ -550,10 +558,45 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
|> auth_conn(user) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person")) |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
assert json_response(res, 200)["data"]["person"]["goingToEvents"] == nil assert json_response(res, 200)["data"]["person"]["participations"] == nil
assert hd(json_response(res, 200)["errors"])["message"] == assert hd(json_response(res, 200)["errors"])["message"] ==
"Actor id is not owned by authenticated user" "Actor id is not owned by authenticated user"
end end
test "find_person/3 can return the participation for an identity on a specific event",
context do
user = insert(:user)
actor = insert(:actor, user: user)
event = insert(:event, organizer_actor: actor)
insert(:participant, event: event, actor: actor)
query = """
{
person(preferredUsername: "#{actor.preferred_username}") {
participations(eventId: "#{event.id}") {
event {
uuid,
title
}
}
}
}
"""
res =
context.conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
assert json_response(res, 200)["data"]["person"]["participations"] == [
%{
"event" => %{
"uuid" => event.uuid,
"title" => event.title
}
}
]
end
end end
end end