From 442a011490fe87bd66d2252c6182d5c610d8c6b0 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Wed, 2 Oct 2019 17:59:07 +0200 Subject: [PATCH] Add draft feature Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- js/src/components/Event/EventCard.vue | 13 +- js/src/graphql/actor.ts | 36 ++- js/src/graphql/event.ts | 7 + js/src/i18n/en_US.json | 21 +- js/src/i18n/fr_FR.json | 21 +- js/src/types/event.model.ts | 8 +- .../views/Account/IdentityPickerWrapper.vue | 2 +- js/src/views/Event/Edit.vue | 43 ++- js/src/views/Event/Event.vue | 44 +-- js/src/views/Event/MyEvents.vue | 29 +- lib/mobilizon/events/event.ex | 4 + lib/mobilizon/events/events.ex | 77 +++++- lib/mobilizon_web/api/events.ex | 8 +- lib/mobilizon_web/resolvers/event.ex | 17 ++ lib/mobilizon_web/resolvers/user.ex | 13 + lib/mobilizon_web/schema/event.ex | 9 +- lib/mobilizon_web/schema/user.ex | 8 +- lib/service/activity_pub/converter/event.ex | 2 + lib/service/activity_pub/utils.ex | 1 + ...0191002093208_add_draft_flag_to_events.exs | 9 + schema.graphql | 21 +- .../resolvers/event_resolver_test.exs | 260 ++++++++++++++++++ 22 files changed, 587 insertions(+), 66 deletions(-) create mode 100644 priv/repo/migrations/20191002093208_add_draft_flag_to_events.exs diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue index d1771e1b1..e7c175931 100644 --- a/js/src/components/Event/EventCard.vue +++ b/js/src/components/Event/EventCard.vue @@ -15,7 +15,7 @@ </div> <h2 class="title" ref="title">{{ event.title }}</h2> </div> - <span> + <span class="organizer-place-wrapper"> <span v-if="actorDisplayName && actorDisplayName !== '@'">{{ $t('By {name}', { name: actorDisplayName }) }}</span> <span v-if="event.physicalAddress && (event.physicalAddress.locality || event.physicalAddress.description)"> - {{ event.physicalAddress.locality || event.physicalAddress.description }} @@ -142,6 +142,17 @@ export default class EventCard extends Vue { line-height: 1em; font-size: 1.6em; padding-bottom: 5px; + margin-top: auto; + } + } + span.organizer-place-wrapper { + display: flex; + + span:last-child { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 02c08215b..d8edd45f3 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -60,7 +60,7 @@ export const UPDATE_CURRENT_ACTOR_CLIENT = gql` `; export const LOGGED_USER_PARTICIPATIONS = gql` -query LoggedUserParticipations($afterDateTime: DateTime, $beforeDateTime: DateTime $page: Int, $limit: Int) { +query LoggedUserParticipations($afterDateTime: DateTime, $beforeDateTime: DateTime, $page: Int, $limit: Int) { loggedUser { participations(afterDatetime: $afterDateTime, beforeDatetime: $beforeDateTime, page: $page, limit: $limit) { event { @@ -106,6 +106,40 @@ query LoggedUserParticipations($afterDateTime: DateTime, $beforeDateTime: DateTi } }`; +export const LOGGED_USER_DRAFTS = gql` + query LoggedUserDrafts($page: Int, $limit: Int) { + loggedUser { + drafts(page: $page, limit: $limit) { + id, + uuid, + title, + picture { + url, + alt + }, + beginsOn, + visibility, + organizerActor { + id, + preferredUsername, + name, + domain, + avatar { + url + } + }, + participantStats { + approved, + unapproved + }, + options { + maximumAttendeeCapacity + remainingAttendeeCapacity + } + } + } + }`; + export const IDENTITIES = gql` query { identities { diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 0c40da1db..aa1798174 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -69,6 +69,7 @@ export const FETCH_EVENT = gql` status, visibility, joinOptions, + draft, picture { id url @@ -190,6 +191,7 @@ export const CREATE_EVENT = gql` $status: EventStatus, $visibility: EventVisibility, $joinOptions: EventJoinOptions, + $draft: Boolean, $tags: [String], $picture: PictureInput, $onlineAddress: String, @@ -207,6 +209,7 @@ export const CREATE_EVENT = gql` status: $status, visibility: $visibility, joinOptions: $joinOptions, + draft: $draft, tags: $tags, picture: $picture, onlineAddress: $onlineAddress, @@ -224,6 +227,7 @@ export const CREATE_EVENT = gql` status, visibility, joinOptions, + draft, picture { id url @@ -255,6 +259,7 @@ export const EDIT_EVENT = gql` $status: EventStatus, $visibility: EventVisibility, $joinOptions: EventJoinOptions, + $draft: Boolean, $tags: [String], $picture: PictureInput, $onlineAddress: String, @@ -272,6 +277,7 @@ export const EDIT_EVENT = gql` status: $status, visibility: $visibility, joinOptions: $joinOptions, + draft: $draft, tags: $tags, picture: $picture, onlineAddress: $onlineAddress, @@ -289,6 +295,7 @@ export const EDIT_EVENT = gql` status, visibility, joinOptions, + draft, picture { id url diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index bba7cd255..1db3b06eb 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -13,10 +13,14 @@ "Allow all comments": "Allow all comments", "Approve": "Approve", "Are you going to this event?": "Are you going to this event?", + "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Are you sure you want to cancel the event creation? You'll lose all modifications.", + "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Are you sure you want to cancel the event edition? You'll lose all modifications.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Are you sure you want to cancel your participation at event \"{title}\"?", "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", "By {name}": "By {name}", + "Cancel creation": "Cancel creation", + "Cancel edition": "Cancel edition", "Cancel my participation request…": "Cancel my participation request…", "Cancel my participation…": "Cancel my participation…", "Cancel": "Cancel", @@ -33,6 +37,7 @@ "Comments": "Comments", "Confirm my particpation": "Confirm my particpation", "Confirmed: Will happen": "Confirmed: Will happen", + "Continue editing": "Continue editing", "Country": "Country", "Create a new event": "Create a new event", "Create a new group": "Create a new group", @@ -59,12 +64,15 @@ "Display participation price": "Display participation price", "Displayed name": "Displayed name", "Do you want to participate in {title}?": "Do you want to participate in {title}?", + "Draft": "Draft", + "Drafts": "Drafts", "Edit": "Edit", "Either the account is already validated, either the validation token is incorrect.": "Either the account is already validated, either the validation token is incorrect.", "Email": "Email", "Ends on…": "Ends on…", "Enter some tags": "Enter some tags", "Error while validating account": "Error while validating account", + "Event already passed": "Event already passed", "Event list": "Event list", "Event {eventTitle} deleted": "Event {eventTitle} deleted", "Event {eventTitle} reported": "Event {eventTitle} reported", @@ -165,6 +173,7 @@ "Public event": "Public event", "Public feeds": "Public feeds", "Public iCal Feed": "Public iCal Feed", + "Publish": "Publish", "Published events": "Published events", "RSS/Atom Feed": "RSS/Atom Feed", "Region": "Region", @@ -172,10 +181,14 @@ "Register": "Register", "Registration is currently closed.": "Registration is currently closed.", "Reject": "Reject", + "Rejected participations": "Rejected participations", + "Rejected": "Rejected", "Report this event": "Report this event", "Report": "Report", + "Requests": "Requests", "Resend confirmation email": "Resend confirmation email", "Reset my password": "Reset my password", + "Save draft": "Save draft", "Save": "Save", "Search events, groups, etc.": "Search events, groups, etc.", "Search results: \"{search}\"": "Search results: \"{search}\"", @@ -210,7 +223,9 @@ "To confirm, type your event title \"{eventTitle}\"": "To confirm, type your event title \"{eventTitle}\"", "To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"", "Transfer to {outsideDomain}": "Transfer to {outsideDomain}", + "Unfortunately, your participation request was rejected by the organizers.": "Unfortunately, your participation request was rejected by the organizers.", "Unknown error.": "Unknown error.", + "Unsaved changes": "Unsaved changes", "Upcoming": "Upcoming", "Update event {name}": "Update event {name}", "Update my event": "Update my event", @@ -253,9 +268,5 @@ "{approved} / {total} seats": "{approved} / {total} seats", "{count} participants": "{count} participants", "{count} requests waiting": "{count} requests waiting", - "© 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", - "Requests": "Requests", - "Rejected": "Rejected", - "Rejected participations": "Rejected participations", - "Unfortunately, your participation request was rejected by the organizers.": "Unfortunately, your participation request was rejected by the organizers." + "© 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" } \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 2b97a6d64..c06c13f86 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -13,10 +13,14 @@ "Allow all comments": "Autoriser tous les commentaires", "Approve": "Approuver", "Are you going to this event?": "Allez-vous à cet événement ?", + "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Étes-vous certain⋅e de vouloir annuler la création de l'événement ? Vous allez perdre toutes vos modifications.", + "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Étes-vous certain⋅e de vouloir annuler l'édition de l'événement ? Vous allez perdre toutes vos modifications.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain⋅e de vouloir annuler votre participation à l'événement « {title} » ?", "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", "By {name}": "Par {name}", + "Cancel creation": "Annuler la création", + "Cancel edition": "Annuler l'édition", "Cancel my participation request…": "Cancel my participation request…", "Cancel my participation…": "Annuler ma participation…", "Cancel": "Annuler", @@ -33,6 +37,7 @@ "Comments": "Commentaires", "Confirm my particpation": "Confirmer ma particpation", "Confirmed: Will happen": "Confirmé : aura lieu", + "Continue editing": "Continuer l'édition", "Country": "Pays", "Create a new event": "Créer un nouvel événement", "Create a new group": "Créer un nouveau groupe", @@ -59,12 +64,15 @@ "Display participation price": "Afficher un prix de participation", "Displayed name": "Nom affiché", "Do you want to participate in {title}?": "Voulez-vous participer à {title} ?", + "Draft": "Brouillon", + "Drafts": "Brouillons", "Edit": "Éditer", "Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est incorrect.", "Email": "Email", "Ends on…": "Se termine le…", "Enter some tags": "Écrire des tags", "Error while validating account": "Erreur lors de la validation du compte", + "Event already passed": "Événement déjà passé", "Event list": "Liste d'événements", "Event {eventTitle} deleted": "Événement {eventTitle} supprimé", "Event {eventTitle} reported": "Événement {eventTitle} signalé", @@ -165,6 +173,7 @@ "Public event": "Événement public", "Public feeds": "Flux publics", "Public iCal Feed": "Flux iCal public", + "Publish": "Publier", "Published events": "Événements publiés", "RSS/Atom Feed": "Flux RSS/Atom", "Region": "Région", @@ -172,10 +181,14 @@ "Register": "S'inscrire", "Registration is currently closed.": "Les inscriptions sont actuellement fermées.", "Reject": "Rejetter", + "Rejected participations": "Participations rejetées", + "Rejected": "Rejetés", "Report this event": "Signaler cet événement", "Report": "Signaler", + "Requests": "Requêtes", "Resend confirmation email": "Envoyer à nouveau l'email de confirmation", "Reset my password": "Réinitialiser mon mot de passe", + "Save draft": "Enregistrer le brouillon", "Save": "Enregistrer", "Search events, groups, etc.": "Rechercher des événements, des groupes, etc.", "Search results: \"{search}\"": "Résultats de recherche: « {search} »", @@ -210,7 +223,9 @@ "To confirm, type your event title \"{eventTitle}\"": "Pour confirmer, entrez le titre de l'événement « {eventTitle} »", "To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de l’identité « {preferredUsername} »", "Transfer to {outsideDomain}": "Transférer à {outsideDomain}", + "Unfortunately, your participation request was rejected by the organizers.": "Malheureusement, votre demande de participation a été refusée par les organisateur⋅ices.", "Unknown error.": "Erreur inconnue.", + "Unsaved changes": "Modifications non enregistrées", "Upcoming": "À venir", "Update event {name}": "Éditer l'événement {name}", "Update my event": "Éditer mon événement", @@ -253,9 +268,5 @@ "{approved} / {total} seats": "{approved} / {total} places", "{count} participants": "Un⋅e participant⋅e|{count} participant⋅e⋅s", "{count} requests waiting": "Un⋅e demande en attente|{count} demandes en attente", - "© 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", - "Requests": "Requêtes", - "Rejected": "Rejetés", - "Rejected participations": "Participations rejetées", - "Unfortunately, your participation request was rejected by the organizers.": "Malheureusement, votre demande de participation a été refusée par les organisateur⋅ices." + "© 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" } \ No newline at end of file diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index f81735889..82aac6692 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -96,15 +96,13 @@ export interface IEvent { slug: string; description: string; category: Category | null; - beginsOn: Date; endsOn: Date | null; publishAt: Date; - status: EventStatus; visibility: EventVisibility; - joinOptions: EventJoinOptions; + draft: boolean; picture: IPicture | null; @@ -176,6 +174,7 @@ export class EventModel implements IEvent { category: Category | null = Category.MEETING; joinOptions = EventJoinOptions.FREE; status = EventStatus.CONFIRMED; + draft = true; publishAt = new Date(); @@ -210,8 +209,8 @@ export class EventModel implements IEvent { this.status = hash.status; this.visibility = hash.visibility; - this.joinOptions = hash.joinOptions; + this.draft = hash.draft; this.picture = hash.picture; @@ -240,6 +239,7 @@ export class EventModel implements IEvent { status: this.status, visibility: this.visibility, joinOptions: this.joinOptions, + draft: this.draft, tags: this.tags.map(t => t.title), picture: this.picture, onlineAddress: this.onlineAddress, diff --git a/js/src/views/Account/IdentityPickerWrapper.vue b/js/src/views/Account/IdentityPickerWrapper.vue index 028c29802..87ad24574 100644 --- a/js/src/views/Account/IdentityPickerWrapper.vue +++ b/js/src/views/Account/IdentityPickerWrapper.vue @@ -5,7 +5,7 @@ {{ $t('Change') }} </b-button> <b-modal :active.sync="isComponentModalActive" has-modal-card> - <identity-picker :currentIdentity="currentIdentity" @input="relay" /> + <identity-picker v-model="currentIdentity" @input="relay" /> </b-modal> </div> </template> diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index b98edb718..20b91be13 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -9,7 +9,7 @@ </h1> <div class="columns is-centered"> - <form class="column is-two-thirds-desktop"> + <form class="column is-two-thirds-desktop" ref="form"> <h2 class="subtitle"> {{ $t('General information') }} </h2> @@ -170,14 +170,16 @@ {{ $t('Cancel') }} </b-button> </span> - <span class="navbar-item" v-if="isUpdate === false"> - <b-button type="is-primary" outlined> + <!-- If an event has been published we can't make it draft anymore --> + <span class="navbar-item" v-if="event.draft === true"> + <b-button type="is-primary" outlined @click="createOrUpdateDraft"> {{ $t('Save draft') }} </b-button> </span> <span class="navbar-item"> - <b-button type="is-primary" @click="createOrUpdate" @keyup.enter="createOrUpdate"> + <b-button type="is-primary" @click="createOrUpdatePublish" @keyup.enter="createOrUpdatePublish"> <span v-if="isUpdate === false">{{ $t('Create my event') }}</span> + <span v-else-if="event.draft === true"> {{ $t('Publish') }}</span> <span v-else> {{ $t('Update my event') }}</span> </b-button> </span> @@ -238,7 +240,7 @@ import { EventVisibility, IEvent, } from '@/types/event.model'; import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor'; -import { Person } from '@/types/actor'; +import { IActor, Person } from '@/types/actor'; import PictureUpload from '@/components/PictureUpload.vue'; import Editor from '@/components/Editor.vue'; import DateTimePicker from '@/components/Event/DateTimePicker.vue'; @@ -312,7 +314,6 @@ export default class EditEvent extends Vue { this.observer = new IntersectionObserver((entries, observer) => { for (const entry of entries) { if (entry) { - console.log(entry); this.showFixedNavbar = !entry.isIntersecting; } } @@ -322,12 +323,29 @@ export default class EditEvent extends Vue { this.observer.observe(this.$refs.bottomObserver as Element); } - createOrUpdate(e: Event) { + createOrUpdateDraft(e: Event) { e.preventDefault(); + if (this.validateForm()) { + if (this.eventId) return this.updateEvent(); - if (this.eventId) return this.updateEvent(); + return this.createEvent(); + } + } - return this.createEvent(); + createOrUpdatePublish(e: Event) { + if (this.validateForm()) { + this.event.draft = false; + this.createOrUpdateDraft(e); + } + } + + private validateForm() { + const form = this.$refs.form as HTMLFormElement; + if (form.checkValidity()) { + return true; + } + form.reportValidity(); + return false; } async createEvent() { @@ -412,13 +430,16 @@ export default class EditEvent extends Vue { * Confirm cancel */ confirmGoBack() { + if (!this.isEventModified) { + return this.$router.go(-1); + } const title: string = this.isUpdate ? this.$t('Cancel edition') as string : this.$t('Cancel creation') as string; const message: string = this.isUpdate ? - this.$t('Are you sure you want to cancel the event edition? You\'ll lose all modifications.', + this.$t("Are you sure you want to cancel the event edition? You'll lose all modifications.", { title: this.event.title }) as string : - this.$t('Are you sure you want to cancel the event creation? You\'ll lose all modifications.', + this.$t("Are you sure you want to cancel the event creation? You'll lose all modifications.", { title: this.event.title }) as string; this.$buefy.dialog.confirm({ diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index 68ef37505..ce884a815 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -18,15 +18,15 @@ </div> <h1 class="title">{{ event.title }}</h1> </div> - <div class="has-text-right"> + <div class="has-text-right" v-if="new Date(endDate) > new Date()"> <small v-if="event.participantStats.approved > 0 && !actorIsParticipant"> {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} </small> - <small v-else> + <small v-else-if="event.participantStats.approved > 0 && actorIsParticipant"> {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }} </small> <participation-button - v-if="currentActor.id && !actorIsOrganizer" + v-if="currentActor.id && !actorIsOrganizer && !event.draft" :participation="participations[0]" :current-actor="currentActor" @joinEvent="joinEvent" @@ -34,15 +34,25 @@ @confirmLeave="confirmLeave" /> </div> + <div v-else> + <button class="button is-primary" type="button" slot="trigger" disabled> + <template> + <span>{{ $t('Event already passed')}}</span> + </template> + <b-icon icon="menu-down"></b-icon> + </button> + </div> </div> <div class="metadata columns"> <div class="column is-three-quarters-desktop"> <p class="tags" v-if="event.category || event.tags.length > 0"> + <b-tag type="is-warning" size="is-medium" v-if="event.draft">{{ $t('Draft') }}</b-tag> <!-- <span class="tag" v-if="event.category">{{ event.category }}</span>--> - <span class="tag" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</span> - <span class="visibility"> - <span v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}</span> - <span v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</span> + <b-tag type="is-success" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</b-tag> + <span v-if="event.tags > 0">⋅</span> + <span class="visibility" v-if="!event.draft"> + <b-tag type="is-info" v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}</b-tag> + <b-tag type="is-info" v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</b-tag> </span> </p> <div class="date-and-add-to-calendar"> @@ -50,7 +60,7 @@ <b-icon icon="calendar-clock" /> <event-full-date :beginsOn="event.beginsOn" :endsOn="event.endsOn" /> </div> - <a class="add-to-calendar" @click="downloadIcsEvent()"> + <a class="add-to-calendar" @click="downloadIcsEvent()" v-if="!event.draft"> <b-icon icon="calendar-plus" /> {{ $t('Add to my calendar') }} </a> @@ -61,7 +71,7 @@ </div> <div class="column sidebar"> <div class="field has-addons" v-if="currentActor.id"> - <p class="control" v-if="actorIsOrganizer"> + <p class="control" v-if="actorIsOrganizer || event.draft"> <router-link class="button" :to="{ name: 'EditEvent', params: {eventId: event.uuid}}" @@ -69,7 +79,7 @@ {{ $t('Edit') }} </router-link> </p> - <p class="control" v-if="actorIsOrganizer"> + <p class="control" v-if="actorIsOrganizer || event.draft"> <a class="button is-danger" @click="openDeleteEventModalWrapper"> {{ $t('Delete') }} </a> @@ -133,7 +143,7 @@ </div> </div> </div> - <section class="share"> + <section class="share" v-if="!event.draft"> <div class="container"> <div class="columns"> <div class="column is-half has-text-centered"> @@ -433,6 +443,10 @@ export default class Event extends EventMixin { return this.participations.length > 0 && this.participations[0].role === ParticipantRole.CREATOR; } + get endDate() { + return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn ? this.event.endsOn : this.event.beginsOn; + } + get twitterShareUrl(): string { return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${this.event.title}`; } @@ -597,18 +611,14 @@ export default class Event extends EventMixin { p.tags { span { - &.tag { + &.tag.is-success { &::before { content: '#'; } text-transform: uppercase; + color: #111111; } - &.visibility::before { - content: "⋅" - } - - margin: auto 5px; } margin-bottom: 1rem; diff --git a/js/src/views/Event/MyEvents.vue b/js/src/views/Event/MyEvents.vue index 8d039645a..5114bfda0 100644 --- a/js/src/views/Event/MyEvents.vue +++ b/js/src/views/Event/MyEvents.vue @@ -26,6 +26,19 @@ v-if="hasMoreFutureParticipations && (futureParticipations.length === limit)" @click="loadMoreFutureParticipations" size="is-large" type="is-primary">{{ $t('Load more') }}</b-button> </div> </section> + <section v-if="drafts.length > 0"> + <h2 class="subtitle"> + {{ $t('Drafts') }} + </h2> + <div class="columns is-multiline"> + <EventCard + v-for="draft in drafts" + :key="draft.uuid" + :event="draft" + class="is-one-quarter-desktop column" + /> + </div> + </section> <section v-if="pastParticipations.length > 0"> <h2 class="subtitle"> {{ $t('Past events') }} @@ -56,13 +69,15 @@ <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; -import { LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor'; -import { IParticipant, Participant } from '@/types/event.model'; +import { LOGGED_USER_PARTICIPATIONS, LOGGED_USER_DRAFTS } from '@/graphql/actor'; +import { EventModel, IEvent, IParticipant, Participant } from '@/types/event.model'; import EventListCard from '@/components/Event/EventListCard.vue'; +import EventCard from '@/components/Event/EventCard.vue'; @Component({ components: { + EventCard, EventListCard, }, apollo: { @@ -75,6 +90,14 @@ import EventListCard from '@/components/Event/EventListCard.vue'; }, update: data => data.loggedUser.participations.map(participation => new Participant(participation)), }, + drafts: { + query: LOGGED_USER_DRAFTS, + variables: { + page: 1, + limit: 10, + }, + update: data => data.loggedUser.drafts.map(event => new EventModel(event)), + }, pastParticipations: { query: LOGGED_USER_PARTICIPATIONS, variables: { @@ -97,6 +120,8 @@ export default class MyEvents extends Vue { pastParticipations: IParticipant[] = []; hasMorePastParticipations: boolean = true; + drafts: IEvent[] = []; + private monthlyParticipations(participations: IParticipant[]): Map<string, Participant[]> { const res = participations.filter(({ event }) => event.beginsOn != null); res.sort( diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 8a33cf090..8ca0f501c 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -32,6 +32,7 @@ defmodule Mobilizon.Events.Event do ends_on: DateTime.t(), title: String.t(), status: EventStatus.t(), + draft: boolean, visibility: EventVisibility.t(), join_options: JoinOptions.t(), publish_at: DateTime.t(), @@ -57,6 +58,7 @@ defmodule Mobilizon.Events.Event do :ends_on, :category, :status, + :draft, :visibility, :publish_at, :online_address, @@ -74,6 +76,7 @@ defmodule Mobilizon.Events.Event do :ends_on, :category, :status, + :draft, :visibility, :join_options, :publish_at, @@ -93,6 +96,7 @@ defmodule Mobilizon.Events.Event do field(:ends_on, :utc_datetime) field(:title, :string) field(:status, EventStatus, default: :confirmed) + field(:draft, :boolean, default: false) field(:visibility, EventVisibility, default: :public) field(:join_options, JoinOptions, default: :free) field(:publish_at, :utc_datetime) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index f8920a0ee..41849d9bf 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -169,6 +169,7 @@ defmodule Mobilizon.Events do url |> event_by_url_query() |> filter_public_visibility() + |> filter_draft() |> preload_for_event() |> Repo.one() @@ -190,18 +191,32 @@ defmodule Mobilizon.Events do url |> event_by_url_query() |> filter_public_visibility() + |> filter_draft() |> preload_for_event() |> Repo.one!() end @doc """ - Gets an event by its UUID, with all associations loaded. + Gets a public event by its UUID, with all associations loaded. """ @spec get_public_event_by_uuid_with_preload(String.t()) :: Event.t() | nil def get_public_event_by_uuid_with_preload(uuid) do uuid |> event_by_uuid_query() |> filter_public_visibility() + |> filter_draft() + |> preload_for_event() + |> Repo.one() + end + + @doc """ + Gets an event by its UUID, with all associations loaded. + """ + @spec get_own_event_by_uuid_with_preload(String.t(), integer()) :: Event.t() | nil + def get_own_event_by_uuid_with_preload(uuid, user_id) do + uuid + |> event_by_uuid_query() + |> user_events_query(user_id) |> preload_for_event() |> Repo.one() end @@ -215,6 +230,7 @@ defmodule Mobilizon.Events do |> upcoming_public_event_for_actor_query() |> filter_public_visibility() |> filter_not_event_uuid(not_event_uuid) + |> filter_draft() |> Repo.one() end @@ -223,16 +239,18 @@ defmodule Mobilizon.Events do """ @spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} def create_event(attrs \\ %{}) do - with {:ok, %Event{} = event} <- do_create_event(attrs), + with {:ok, %Event{draft: false} = event} <- do_create_event(attrs), {:ok, %Participant{} = _participant} <- - %Participant{} - |> Participant.changeset(%{ + create_participant(%{ actor_id: event.organizer_actor_id, role: :creator, event_id: event.id - }) - |> Repo.insert() do + }) do {:ok, event} + else + # We don't create a creator participant if the event is a draft + {:ok, %Event{draft: true} = event} -> {:ok, event} + err -> err end end @@ -262,10 +280,24 @@ defmodule Mobilizon.Events do Updates an event. """ @spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} - def update_event(%Event{} = old_event, attrs) do + def update_event( + %Event{draft: old_draft_status, id: event_id, organizer_actor_id: organizer_actor_id} = + old_event, + attrs + ) do with %Ecto.Changeset{changes: changes} = changeset <- old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do - with {:ok, %Event{} = new_event} <- Repo.update(changeset) do + with {:ok, %Event{draft: new_draft_status} = new_event} <- Repo.update(changeset) do + # If the event is no longer a draft + if old_draft_status == true && new_draft_status == false do + {:ok, %Participant{} = _participant} = + create_participant(%{ + event_id: event_id, + role: :creator, + actor_id: organizer_actor_id + }) + end + Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications( old_event, new_event, @@ -309,6 +341,7 @@ defmodule Mobilizon.Events do |> sort(sort, direction) |> filter_future_events(is_future) |> filter_unlisted(is_unlisted) + |> filter_draft() |> Repo.all() end @@ -320,6 +353,7 @@ defmodule Mobilizon.Events do tags |> Enum.map(& &1.id) |> events_by_tags_query(limit) + |> filter_draft() |> Repo.all() end @@ -333,6 +367,7 @@ defmodule Mobilizon.Events do actor_id |> event_for_actor_query() |> filter_public_visibility() + |> filter_draft() |> preload_for_event() |> Page.paginate(page, limit) |> Repo.all() @@ -345,6 +380,15 @@ defmodule Mobilizon.Events do {:ok, events, events_count} end + @spec list_drafts_for_user(integer, integer | nil, integer | nil) :: [Event.t()] + def list_drafts_for_user(user_id, page \\ nil, limit \\ nil) do + Event + |> user_events_query(user_id) + |> filter_draft(true) + |> Page.paginate(page, limit) + |> Repo.all() + end + @doc """ Finds close events to coordinates. Radius is in meters and defaults to 50km. @@ -354,6 +398,7 @@ defmodule Mobilizon.Events do "SRID=#{srid};POINT(#{lon} #{lat})" |> Geo.WKT.decode!() |> close_events_query(radius) + |> filter_draft() |> Repo.all() end @@ -364,6 +409,7 @@ defmodule Mobilizon.Events do def count_local_events do count_local_events_query() |> filter_public_visibility() + |> filter_draft() |> Repo.one() end @@ -1134,6 +1180,16 @@ defmodule Mobilizon.Events do ) end + @spec user_events_query(Ecto.Query.t(), number()) :: Ecto.Query.t() + defp user_events_query(query, user_id) do + from( + e in query, + join: a in Actor, + on: a.id == e.organizer_actor_id, + where: a.user_id == ^user_id + ) + end + @spec events_by_name_query(String.t()) :: Ecto.Query.t() defp events_by_name_query(name) do from( @@ -1372,6 +1428,11 @@ defmodule Mobilizon.Events do from(e in query, where: e.uuid != ^not_event_uuid) end + @spec filter_draft(Ecto.Query.t(), boolean) :: Ecto.Query.t() + defp filter_draft(query, is_draft \\ false) do + from(e in query, where: e.draft == ^is_draft) + end + @spec filter_future_events(Ecto.Query.t(), boolean) :: Ecto.Query.t() defp filter_future_events(query, true) do from(q in query, where: q.begins_on > ^DateTime.utc_now()) diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex index 6e1c93b8c..cfc9ce9d4 100644 --- a/lib/mobilizon_web/api/events.ex +++ b/lib/mobilizon_web/api/events.ex @@ -31,7 +31,8 @@ defmodule MobilizonWeb.API.Events do to: args.to, actor: organizer_actor, object: event, - local: true + # For now we don't federate drafts but it will be needed if we want to edit them as groups + local: args.metadata.draft == false }) end end @@ -65,7 +66,7 @@ defmodule MobilizonWeb.API.Events do actor: organizer_actor.url, cc: [], object: event, - local: true + local: args.metadata.draft == false }) end end @@ -95,7 +96,8 @@ defmodule MobilizonWeb.API.Events do join_options: Map.get(args, :join_options), status: Map.get(args, :status), online_address: Map.get(args, :online_address), - phone_address: Map.get(args, :phone_address) + phone_address: Map.get(args, :phone_address), + draft: Map.get(args, :draft) } } end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 48ffc73db..5dc961fb2 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -31,6 +31,20 @@ defmodule MobilizonWeb.Resolvers.Event do {:error, :events_max_limit_reached} end + def find_event( + _parent, + %{uuid: uuid}, + %{context: %{current_user: %User{id: user_id}}} = _resolution + ) do + case {:has_event, Mobilizon.Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do + {:has_event, %Event{} = event} -> + {:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))} + + {:has_event, _} -> + {:error, "Event with UUID #{uuid} not found"} + end + end + def find_event(_parent, %{uuid: uuid}, _resolution) do case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do {:has_event, %Event{} = event} -> @@ -264,6 +278,9 @@ defmodule MobilizonWeb.Resolvers.Event do else {:is_owned, nil} -> {:error, "Organizer actor id is not owned by the user"} + + {:error, %Ecto.Changeset{} = error} -> + {:error, error} end end diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index 613b7dde2..af009dcb2 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -243,6 +243,19 @@ defmodule MobilizonWeb.Resolvers.User do end end + @doc """ + Returns the list of draft events for the current user + """ + def user_drafted_events(%User{id: user_id}, args, %{ + context: %{current_user: %User{id: logged_user_id}} + }) do + with {:same_user, true} <- {:same_user, user_id == logged_user_id}, + events <- + Events.list_drafts_for_user(user_id, Map.get(args, :page), Map.get(args, :limit)) do + {:ok, events} + end + end + def change_password(_parent, %{old_password: old_password, new_password: new_password}, %{ context: %{current_user: %User{password_hash: old_password_hash} = user} }) do diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index 7a49037a2..d6cc0b584 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.EventType do use Absinthe.Schema.Notation import Absinthe.Resolution.Helpers, only: [dataloader: 1] + import MobilizonWeb.Schema.Utils alias Mobilizon.{Actors, Addresses} @@ -60,6 +61,8 @@ defmodule MobilizonWeb.Schema.EventType do field(:category, :string, description: "The event's category") + field(:draft, :boolean, description: "Whether or not the event is a draft") + field(:participant_stats, :participant_stats, resolve: &Event.stats_participants_for_event/3) field(:participants, list_of(:participant), description: "The event's participants") do @@ -252,8 +255,9 @@ defmodule MobilizonWeb.Schema.EventType do arg(:category, :string, default_value: "meeting") arg(:physical_address, :address_input) arg(:options, :event_options_input) + arg(:draft, :boolean, default_value: false) - resolve(&Event.create_event/3) + resolve(handle_errors(&Event.create_event/3)) end @desc "Update an event" @@ -280,8 +284,9 @@ defmodule MobilizonWeb.Schema.EventType do arg(:category, :string) arg(:physical_address, :address_input) arg(:options, :event_options_input) + arg(:draft, :boolean) - resolve(&Event.update_event/3) + resolve(handle_errors(&Event.update_event/3)) end @desc "Delete an event" diff --git a/lib/mobilizon_web/schema/user.ex b/lib/mobilizon_web/schema/user.ex index fc3021d3b..d23547e29 100644 --- a/lib/mobilizon_web/schema/user.ex +++ b/lib/mobilizon_web/schema/user.ex @@ -49,7 +49,7 @@ defmodule MobilizonWeb.Schema.UserType do field(:locale, :string, description: "The user's locale") field(:participations, list_of(:participant), - description: "The list of events this user goes to" + description: "The list of participations this user has" ) do arg(:after_datetime, :datetime) arg(:before_datetime, :datetime) @@ -57,6 +57,12 @@ defmodule MobilizonWeb.Schema.UserType do arg(:limit, :integer, default_value: 10) resolve(&User.user_participations/3) end + + field(:drafts, list_of(:event), description: "The list of draft events this user has created") do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + resolve(&User.user_drafted_events/3) + end end enum :user_role do diff --git a/lib/service/activity_pub/converter/event.ex b/lib/service/activity_pub/converter/event.ex index c849c4815..97ebe344c 100644 --- a/lib/service/activity_pub/converter/event.ex +++ b/lib/service/activity_pub/converter/event.ex @@ -70,6 +70,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do "status" => object["status"], "online_address" => object["onlineAddress"], "phone_address" => object["phoneAddress"], + "draft" => object["draft"] || false, "url" => object["id"], "uuid" => object["uuid"], "tags" => tags, @@ -111,6 +112,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do "joinOptions" => to_string(event.join_options), "endTime" => event.ends_on |> date_to_string(), "tag" => event.tags |> build_tags(), + "draft" => event.draft, "id" => event.url, "url" => event.url } diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index 24e76bd0e..397e8dceb 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -319,6 +319,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "status" => metadata.status, "onlineAddress" => metadata.online_address, "phoneAddress" => metadata.phone_address, + "draft" => metadata.draft, "uuid" => uuid, "tag" => tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end) diff --git a/priv/repo/migrations/20191002093208_add_draft_flag_to_events.exs b/priv/repo/migrations/20191002093208_add_draft_flag_to_events.exs new file mode 100644 index 000000000..a3014c0a8 --- /dev/null +++ b/priv/repo/migrations/20191002093208_add_draft_flag_to_events.exs @@ -0,0 +1,9 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddDraftFlagToEvents do + use Ecto.Migration + + def change do + alter table(:events) do + add(:draft, :boolean, default: false) + end + end +end diff --git a/schema.graphql b/schema.graphql index 6ac98af50..1ef086b1c 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,5 @@ # source: http://localhost:4000/api -# timestamp: Mon Sep 30 2019 09:56:05 GMT+0200 (GMT+02:00) +# timestamp: Wed Oct 02 2019 16:30:43 GMT+0200 (GMT+02:00) schema { query: RootQueryType @@ -264,6 +264,9 @@ type Event implements ActionLogObject { """The event's description""" description: String + """Whether or not the event is a draft""" + draft: Boolean + """Datetime for when the event ends""" endsOn: DateTime @@ -897,6 +900,7 @@ type RootMutationType { beginsOn: DateTime! category: String = "meeting" description: String! + draft: Boolean = false endsOn: DateTime joinOptions: EventJoinOptions = FREE onlineAddress: String @@ -973,7 +977,7 @@ type RootMutationType { createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote """Create an user""" - createUser(email: String!, password: String!): User + createUser(email: String!, locale: String, password: String!): User """Delete an event""" deleteEvent(actorId: ID!, eventId: ID!): DeletedObject @@ -1030,19 +1034,20 @@ type RootMutationType { ): Person """Resend registration confirmation token""" - resendConfirmationEmail(email: String!, locale: String = "en"): String + resendConfirmationEmail(email: String!, locale: String): String """Reset user password""" resetPassword(locale: String = "en", password: String!, token: String!): Login """Send a link through email to reset user password""" - sendResetPassword(email: String!, locale: String = "en"): String + sendResetPassword(email: String!, locale: String): String """Update an event""" updateEvent( beginsOn: DateTime category: String description: String + draft: Boolean endsOn: DateTime eventId: ID! joinOptions: EventJoinOptions = FREE @@ -1212,6 +1217,9 @@ type User { """The user's default actor""" defaultActor: Person + """The list of draft events this user has created""" + drafts(limit: Int = 10, page: Int = 1): [Event] + """The user's email""" email: String! @@ -1221,7 +1229,10 @@ type User { """The user's ID""" id: ID! - """The list of events this user goes to""" + """The user's locale""" + locale: String + + """The list of participations this user has""" participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant] """The user's list of profiles (identities)""" diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index d6ec3b59b..848b4e9b0 100644 --- a/test/mobilizon_web/resolvers/event_resolver_test.exs +++ b/test/mobilizon_web/resolvers/event_resolver_test.exs @@ -119,6 +119,97 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event" end + test "create_event/3 creates an event as a draft", %{conn: conn, actor: actor, user: user} do + mutation = """ + mutation { + createEvent( + title: "come to my event", + description: "it will be fine", + begins_on: "#{ + DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() + }", + organizer_actor_id: "#{actor.id}", + category: "birthday", + draft: true + ) { + title, + uuid, + id, + draft + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event" + assert json_response(res, 200)["data"]["createEvent"]["draft"] == true + + event_uuid = json_response(res, 200)["data"]["createEvent"]["uuid"] + event_id = json_response(res, 200)["data"]["createEvent"]["id"] + + query = """ + { + event(uuid: "#{event_uuid}") { + uuid, + draft + } + } + """ + + res = + conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "not found" + + query = """ + { + event(uuid: "#{event_uuid}") { + uuid, + draft + } + } + """ + + res = + conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["event"]["draft"] == true + + query = """ + { + person(preferredUsername: "#{actor.preferred_username}") { + id, + participations(eventId: #{event_id}) { + id, + role, + actor { + id + }, + event { + id + } + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "person")) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["person"]["participations"] == [] + end + test "create_event/3 creates an event with options", %{conn: conn, actor: actor, user: user} do begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() ends_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() @@ -684,6 +775,157 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do "picture for my event" end + test "update_event/3 respects the draft status", %{conn: conn, actor: actor, user: user} do + event = insert(:event, organizer_actor: actor, draft: true) + + mutation = """ + mutation { + updateEvent( + event_id: #{event.id}, + title: "my event updated but still draft" + ) { + draft, + title, + uuid + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["data"]["updateEvent"]["draft"] == true + + query = """ + { + event(uuid: "#{event.uuid}") { + uuid, + draft + } + } + """ + + res = + conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "not found" + + query = """ + { + event(uuid: "#{event.uuid}") { + uuid, + draft + } + } + """ + + res = + conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["event"]["draft"] == true + + query = """ + { + person(preferredUsername: "#{actor.preferred_username}") { + id, + participations(eventId: #{event.id}) { + id, + role, + actor { + id + }, + event { + id + } + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "person")) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["person"]["participations"] == [] + + mutation = """ + mutation { + updateEvent( + event_id: #{event.id}, + title: "my event updated and no longer draft", + draft: false + ) { + draft, + title, + uuid + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["data"]["updateEvent"]["draft"] == false + + query = """ + { + event(uuid: "#{event.uuid}") { + uuid, + draft + } + } + """ + + res = + conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["event"]["draft"] == false + + query = """ + { + person(preferredUsername: "#{actor.preferred_username}") { + id, + participations(eventId: #{event.id}) { + role, + actor { + id + }, + event { + id + } + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "person")) + + assert json_response(res, 200)["errors"] == nil + + assert json_response(res, 200)["data"]["person"]["participations"] == [ + %{ + "actor" => %{"id" => to_string(actor.id)}, + "event" => %{"id" => to_string(event.id)}, + "role" => "CREATOR" + } + ] + end + test "list_events/3 returns events", context do event = insert(:event) @@ -782,6 +1024,24 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do assert json_response(res, 200)["data"]["events"] |> Enum.map(& &1["uuid"]) == [] end + test "list_events/3 doesn't list draft events", context do + insert(:event, visibility: :public, draft: true) + + query = """ + { + events { + uuid, + } + } + """ + + res = + context.conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert json_response(res, 200)["data"]["events"] |> Enum.map(& &1["uuid"]) == [] + end + test "find_event/3 returns an unlisted event", context do event = insert(:event, visibility: :unlisted)