Merge branch 'feature/drafts' into 'master'

Add draft feature

See merge request framasoft/mobilizon!214
This commit is contained in:
Thomas Citharel 2019-10-02 18:23:49 +02:00
commit 0feca0d0dc
22 changed files with 587 additions and 66 deletions

View File

@ -15,7 +15,7 @@
</div> </div>
<h2 class="title" ref="title">{{ event.title }}</h2> <h2 class="title" ref="title">{{ event.title }}</h2>
</div> </div>
<span> <span class="organizer-place-wrapper">
<span v-if="actorDisplayName && actorDisplayName !== '@'">{{ $t('By {name}', { name: actorDisplayName }) }}</span> <span v-if="actorDisplayName && actorDisplayName !== '@'">{{ $t('By {name}', { name: actorDisplayName }) }}</span>
<span v-if="event.physicalAddress && (event.physicalAddress.locality || event.physicalAddress.description)"> <span v-if="event.physicalAddress && (event.physicalAddress.locality || event.physicalAddress.description)">
- {{ 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; line-height: 1em;
font-size: 1.6em; font-size: 1.6em;
padding-bottom: 5px; 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;
} }
} }
} }

View File

@ -60,7 +60,7 @@ export const UPDATE_CURRENT_ACTOR_CLIENT = gql`
`; `;
export const LOGGED_USER_PARTICIPATIONS = 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 { loggedUser {
participations(afterDatetime: $afterDateTime, beforeDatetime: $beforeDateTime, page: $page, limit: $limit) { participations(afterDatetime: $afterDateTime, beforeDatetime: $beforeDateTime, page: $page, limit: $limit) {
event { 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` export const IDENTITIES = gql`
query { query {
identities { identities {

View File

@ -69,6 +69,7 @@ export const FETCH_EVENT = gql`
status, status,
visibility, visibility,
joinOptions, joinOptions,
draft,
picture { picture {
id id
url url
@ -190,6 +191,7 @@ export const CREATE_EVENT = gql`
$status: EventStatus, $status: EventStatus,
$visibility: EventVisibility, $visibility: EventVisibility,
$joinOptions: EventJoinOptions, $joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String], $tags: [String],
$picture: PictureInput, $picture: PictureInput,
$onlineAddress: String, $onlineAddress: String,
@ -207,6 +209,7 @@ export const CREATE_EVENT = gql`
status: $status, status: $status,
visibility: $visibility, visibility: $visibility,
joinOptions: $joinOptions, joinOptions: $joinOptions,
draft: $draft,
tags: $tags, tags: $tags,
picture: $picture, picture: $picture,
onlineAddress: $onlineAddress, onlineAddress: $onlineAddress,
@ -224,6 +227,7 @@ export const CREATE_EVENT = gql`
status, status,
visibility, visibility,
joinOptions, joinOptions,
draft,
picture { picture {
id id
url url
@ -255,6 +259,7 @@ export const EDIT_EVENT = gql`
$status: EventStatus, $status: EventStatus,
$visibility: EventVisibility, $visibility: EventVisibility,
$joinOptions: EventJoinOptions, $joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String], $tags: [String],
$picture: PictureInput, $picture: PictureInput,
$onlineAddress: String, $onlineAddress: String,
@ -272,6 +277,7 @@ export const EDIT_EVENT = gql`
status: $status, status: $status,
visibility: $visibility, visibility: $visibility,
joinOptions: $joinOptions, joinOptions: $joinOptions,
draft: $draft,
tags: $tags, tags: $tags,
picture: $picture, picture: $picture,
onlineAddress: $onlineAddress, onlineAddress: $onlineAddress,
@ -289,6 +295,7 @@ export const EDIT_EVENT = gql`
status, status,
visibility, visibility,
joinOptions, joinOptions,
draft,
picture { picture {
id id
url url

View File

@ -13,10 +13,14 @@
"Allow all comments": "Allow all comments", "Allow all comments": "Allow all comments",
"Approve": "Approve", "Approve": "Approve",
"Are you going to this event?": "Are you going to this event?", "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 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.", "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 creation": "Cancel creation",
"Cancel edition": "Cancel edition",
"Cancel my participation request…": "Cancel my participation request…", "Cancel my participation request…": "Cancel my participation request…",
"Cancel my participation…": "Cancel my participation…", "Cancel my participation…": "Cancel my participation…",
"Cancel": "Cancel", "Cancel": "Cancel",
@ -33,6 +37,7 @@
"Comments": "Comments", "Comments": "Comments",
"Confirm my particpation": "Confirm my particpation", "Confirm my particpation": "Confirm my particpation",
"Confirmed: Will happen": "Confirmed: Will happen", "Confirmed: Will happen": "Confirmed: Will happen",
"Continue editing": "Continue editing",
"Country": "Country", "Country": "Country",
"Create a new event": "Create a new event", "Create a new event": "Create a new event",
"Create a new group": "Create a new group", "Create a new group": "Create a new group",
@ -59,12 +64,15 @@
"Display participation price": "Display participation price", "Display participation price": "Display participation price",
"Displayed name": "Displayed name", "Displayed name": "Displayed name",
"Do you want to participate in {title}?": "Do you want to participate in {title}?", "Do you want to participate in {title}?": "Do you want to participate in {title}?",
"Draft": "Draft",
"Drafts": "Drafts",
"Edit": "Edit", "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.", "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", "Email": "Email",
"Ends on…": "Ends on…", "Ends on…": "Ends on…",
"Enter some tags": "Enter some tags", "Enter some tags": "Enter some tags",
"Error while validating account": "Error while validating account", "Error while validating account": "Error while validating account",
"Event already passed": "Event already passed",
"Event list": "Event list", "Event list": "Event list",
"Event {eventTitle} deleted": "Event {eventTitle} deleted", "Event {eventTitle} deleted": "Event {eventTitle} deleted",
"Event {eventTitle} reported": "Event {eventTitle} reported", "Event {eventTitle} reported": "Event {eventTitle} reported",
@ -165,6 +173,7 @@
"Public event": "Public event", "Public event": "Public event",
"Public feeds": "Public feeds", "Public feeds": "Public feeds",
"Public iCal Feed": "Public iCal Feed", "Public iCal Feed": "Public iCal Feed",
"Publish": "Publish",
"Published events": "Published events", "Published events": "Published events",
"RSS/Atom Feed": "RSS/Atom Feed", "RSS/Atom Feed": "RSS/Atom Feed",
"Region": "Region", "Region": "Region",
@ -172,10 +181,14 @@
"Register": "Register", "Register": "Register",
"Registration is currently closed.": "Registration is currently closed.", "Registration is currently closed.": "Registration is currently closed.",
"Reject": "Reject", "Reject": "Reject",
"Rejected participations": "Rejected participations",
"Rejected": "Rejected",
"Report this event": "Report this event", "Report this event": "Report this event",
"Report": "Report", "Report": "Report",
"Requests": "Requests",
"Resend confirmation email": "Resend confirmation email", "Resend confirmation email": "Resend confirmation email",
"Reset my password": "Reset my password", "Reset my password": "Reset my password",
"Save draft": "Save draft",
"Save": "Save", "Save": "Save",
"Search events, groups, etc.": "Search events, groups, etc.", "Search events, groups, etc.": "Search events, groups, etc.",
"Search results: \"{search}\"": "Search results: \"{search}\"", "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 event title \"{eventTitle}\"": "To confirm, type your event title \"{eventTitle}\"",
"To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"", "To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"",
"Transfer to {outsideDomain}": "Transfer to {outsideDomain}", "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.", "Unknown error.": "Unknown error.",
"Unsaved changes": "Unsaved changes",
"Upcoming": "Upcoming", "Upcoming": "Upcoming",
"Update event {name}": "Update event {name}", "Update event {name}": "Update event {name}",
"Update my event": "Update my event", "Update my event": "Update my event",
@ -253,9 +268,5 @@
"{approved} / {total} seats": "{approved} / {total} seats", "{approved} / {total} seats": "{approved} / {total} seats",
"{count} participants": "{count} participants", "{count} participants": "{count} participants",
"{count} requests waiting": "{count} requests waiting", "{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", "© 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."
} }

View File

@ -13,10 +13,14 @@
"Allow all comments": "Autoriser tous les commentaires", "Allow all comments": "Autoriser tous les commentaires",
"Approve": "Approuver", "Approve": "Approuver",
"Are you going to this event?": "Allez-vous à cet événement ?", "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 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.", "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 creation": "Annuler la création",
"Cancel edition": "Annuler l'édition",
"Cancel my participation request…": "Cancel my participation request…", "Cancel my participation request…": "Cancel my participation request…",
"Cancel my participation…": "Annuler ma participation…", "Cancel my participation…": "Annuler ma participation…",
"Cancel": "Annuler", "Cancel": "Annuler",
@ -33,6 +37,7 @@
"Comments": "Commentaires", "Comments": "Commentaires",
"Confirm my particpation": "Confirmer ma particpation", "Confirm my particpation": "Confirmer ma particpation",
"Confirmed: Will happen": "Confirmé : aura lieu", "Confirmed: Will happen": "Confirmé : aura lieu",
"Continue editing": "Continuer l'édition",
"Country": "Pays", "Country": "Pays",
"Create a new event": "Créer un nouvel événement", "Create a new event": "Créer un nouvel événement",
"Create a new group": "Créer un nouveau groupe", "Create a new group": "Créer un nouveau groupe",
@ -59,12 +64,15 @@
"Display participation price": "Afficher un prix de participation", "Display participation price": "Afficher un prix de participation",
"Displayed name": "Nom affiché", "Displayed name": "Nom affiché",
"Do you want to participate in {title}?": "Voulez-vous participer à {title} ?", "Do you want to participate in {title}?": "Voulez-vous participer à {title} ?",
"Draft": "Brouillon",
"Drafts": "Brouillons",
"Edit": "Éditer", "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.", "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", "Email": "Email",
"Ends on…": "Se termine le…", "Ends on…": "Se termine le…",
"Enter some tags": "Écrire des tags", "Enter some tags": "Écrire des tags",
"Error while validating account": "Erreur lors de la validation du compte", "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 list": "Liste d'événements",
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé", "Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
"Event {eventTitle} reported": "Événement {eventTitle} signalé", "Event {eventTitle} reported": "Événement {eventTitle} signalé",
@ -165,6 +173,7 @@
"Public event": "Événement public", "Public event": "Événement public",
"Public feeds": "Flux publics", "Public feeds": "Flux publics",
"Public iCal Feed": "Flux iCal public", "Public iCal Feed": "Flux iCal public",
"Publish": "Publier",
"Published events": "Événements publiés", "Published events": "Événements publiés",
"RSS/Atom Feed": "Flux RSS/Atom", "RSS/Atom Feed": "Flux RSS/Atom",
"Region": "Région", "Region": "Région",
@ -172,10 +181,14 @@
"Register": "S'inscrire", "Register": "S'inscrire",
"Registration is currently closed.": "Les inscriptions sont actuellement fermées.", "Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
"Reject": "Rejetter", "Reject": "Rejetter",
"Rejected participations": "Participations rejetées",
"Rejected": "Rejetés",
"Report this event": "Signaler cet événement", "Report this event": "Signaler cet événement",
"Report": "Signaler", "Report": "Signaler",
"Requests": "Requêtes",
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation", "Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
"Reset my password": "Réinitialiser mon mot de passe", "Reset my password": "Réinitialiser mon mot de passe",
"Save draft": "Enregistrer le brouillon",
"Save": "Enregistrer", "Save": "Enregistrer",
"Search events, groups, etc.": "Rechercher des événements, des groupes, etc.", "Search events, groups, etc.": "Rechercher des événements, des groupes, etc.",
"Search results: \"{search}\"": "Résultats de recherche: « {search} »", "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 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 lidentité « {preferredUsername} »", "To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de lidentité « {preferredUsername} »",
"Transfer to {outsideDomain}": "Transférer à {outsideDomain}", "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.", "Unknown error.": "Erreur inconnue.",
"Unsaved changes": "Modifications non enregistrées",
"Upcoming": "À venir", "Upcoming": "À venir",
"Update event {name}": "Éditer l'événement {name}", "Update event {name}": "Éditer l'événement {name}",
"Update my event": "Éditer mon événement", "Update my event": "Éditer mon événement",
@ -253,9 +268,5 @@
"{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",
"{count} requests waiting": "Un⋅e demande en attente|{count} demandes en attente", "{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", "© 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."
} }

View File

@ -96,15 +96,13 @@ export interface IEvent {
slug: string; slug: string;
description: string; description: string;
category: Category | null; category: Category | null;
beginsOn: Date; beginsOn: Date;
endsOn: Date | null; endsOn: Date | null;
publishAt: Date; publishAt: Date;
status: EventStatus; status: EventStatus;
visibility: EventVisibility; visibility: EventVisibility;
joinOptions: EventJoinOptions; joinOptions: EventJoinOptions;
draft: boolean;
picture: IPicture | null; picture: IPicture | null;
@ -176,6 +174,7 @@ export class EventModel implements IEvent {
category: Category | null = Category.MEETING; category: Category | null = Category.MEETING;
joinOptions = EventJoinOptions.FREE; joinOptions = EventJoinOptions.FREE;
status = EventStatus.CONFIRMED; status = EventStatus.CONFIRMED;
draft = true;
publishAt = new Date(); publishAt = new Date();
@ -210,8 +209,8 @@ export class EventModel implements IEvent {
this.status = hash.status; this.status = hash.status;
this.visibility = hash.visibility; this.visibility = hash.visibility;
this.joinOptions = hash.joinOptions; this.joinOptions = hash.joinOptions;
this.draft = hash.draft;
this.picture = hash.picture; this.picture = hash.picture;
@ -240,6 +239,7 @@ export class EventModel implements IEvent {
status: this.status, status: this.status,
visibility: this.visibility, visibility: this.visibility,
joinOptions: this.joinOptions, joinOptions: this.joinOptions,
draft: this.draft,
tags: this.tags.map(t => t.title), tags: this.tags.map(t => t.title),
picture: this.picture, picture: this.picture,
onlineAddress: this.onlineAddress, onlineAddress: this.onlineAddress,

View File

@ -5,7 +5,7 @@
{{ $t('Change') }} {{ $t('Change') }}
</b-button> </b-button>
<b-modal :active.sync="isComponentModalActive" has-modal-card> <b-modal :active.sync="isComponentModalActive" has-modal-card>
<identity-picker :currentIdentity="currentIdentity" @input="relay" /> <identity-picker v-model="currentIdentity" @input="relay" />
</b-modal> </b-modal>
</div> </div>
</template> </template>

View File

@ -9,7 +9,7 @@
</h1> </h1>
<div class="columns is-centered"> <div class="columns is-centered">
<form class="column is-two-thirds-desktop"> <form class="column is-two-thirds-desktop" ref="form">
<h2 class="subtitle"> <h2 class="subtitle">
{{ $t('General information') }} {{ $t('General information') }}
</h2> </h2>
@ -170,14 +170,16 @@
{{ $t('Cancel') }} {{ $t('Cancel') }}
</b-button> </b-button>
</span> </span>
<span class="navbar-item" v-if="isUpdate === false"> <!-- If an event has been published we can't make it draft anymore -->
<b-button type="is-primary" outlined> <span class="navbar-item" v-if="event.draft === true">
<b-button type="is-primary" outlined @click="createOrUpdateDraft">
{{ $t('Save draft') }} {{ $t('Save draft') }}
</b-button> </b-button>
</span> </span>
<span class="navbar-item"> <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-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> <span v-else> {{ $t('Update my event') }}</span>
</b-button> </b-button>
</span> </span>
@ -238,7 +240,7 @@ import {
EventVisibility, IEvent, EventVisibility, IEvent,
} 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 { IActor, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue'; import PictureUpload from '@/components/PictureUpload.vue';
import Editor from '@/components/Editor.vue'; import Editor from '@/components/Editor.vue';
import DateTimePicker from '@/components/Event/DateTimePicker.vue'; import DateTimePicker from '@/components/Event/DateTimePicker.vue';
@ -312,7 +314,6 @@ export default class EditEvent extends Vue {
this.observer = new IntersectionObserver((entries, observer) => { this.observer = new IntersectionObserver((entries, observer) => {
for (const entry of entries) { for (const entry of entries) {
if (entry) { if (entry) {
console.log(entry);
this.showFixedNavbar = !entry.isIntersecting; this.showFixedNavbar = !entry.isIntersecting;
} }
} }
@ -322,12 +323,29 @@ export default class EditEvent extends Vue {
this.observer.observe(this.$refs.bottomObserver as Element); this.observer.observe(this.$refs.bottomObserver as Element);
} }
createOrUpdate(e: Event) { createOrUpdateDraft(e: Event) {
e.preventDefault(); 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() { async createEvent() {
@ -412,13 +430,16 @@ export default class EditEvent extends Vue {
* Confirm cancel * Confirm cancel
*/ */
confirmGoBack() { confirmGoBack() {
if (!this.isEventModified) {
return this.$router.go(-1);
}
const title: string = this.isUpdate ? const title: string = this.isUpdate ?
this.$t('Cancel edition') as string : this.$t('Cancel edition') as string :
this.$t('Cancel creation') as string; this.$t('Cancel creation') as string;
const message: string = this.isUpdate ? 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 : { 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; { title: this.event.title }) as string;
this.$buefy.dialog.confirm({ this.$buefy.dialog.confirm({

View File

@ -18,15 +18,15 @@
</div> </div>
<h1 class="title">{{ event.title }}</h1> <h1 class="title">{{ event.title }}</h1>
</div> </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"> <small v-if="event.participantStats.approved > 0 && !actorIsParticipant">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
</small> </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}) }} {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }}
</small> </small>
<participation-button <participation-button
v-if="currentActor.id && !actorIsOrganizer" v-if="currentActor.id && !actorIsOrganizer && !event.draft"
:participation="participations[0]" :participation="participations[0]"
:current-actor="currentActor" :current-actor="currentActor"
@joinEvent="joinEvent" @joinEvent="joinEvent"
@ -34,15 +34,25 @@
@confirmLeave="confirmLeave" @confirmLeave="confirmLeave"
/> />
</div> </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>
<div class="metadata columns"> <div class="metadata columns">
<div class="column is-three-quarters-desktop"> <div class="column is-three-quarters-desktop">
<p class="tags" v-if="event.category || event.tags.length > 0"> <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.category">{{ event.category }}</span>-->
<span class="tag" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</span> <b-tag type="is-success" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</b-tag>
<span class="visibility"> <span v-if="event.tags > 0"></span>
<span v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}</span> <span class="visibility" v-if="!event.draft">
<span v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</span> <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> </span>
</p> </p>
<div class="date-and-add-to-calendar"> <div class="date-and-add-to-calendar">
@ -50,7 +60,7 @@
<b-icon icon="calendar-clock" /> <b-icon icon="calendar-clock" />
<event-full-date :beginsOn="event.beginsOn" :endsOn="event.endsOn" /> <event-full-date :beginsOn="event.beginsOn" :endsOn="event.endsOn" />
</div> </div>
<a class="add-to-calendar" @click="downloadIcsEvent()"> <a class="add-to-calendar" @click="downloadIcsEvent()" v-if="!event.draft">
<b-icon icon="calendar-plus" /> <b-icon icon="calendar-plus" />
{{ $t('Add to my calendar') }} {{ $t('Add to my calendar') }}
</a> </a>
@ -61,7 +71,7 @@
</div> </div>
<div class="column sidebar"> <div class="column sidebar">
<div class="field has-addons" v-if="currentActor.id"> <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 <router-link
class="button" class="button"
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}" :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
@ -69,7 +79,7 @@
{{ $t('Edit') }} {{ $t('Edit') }}
</router-link> </router-link>
</p> </p>
<p class="control" v-if="actorIsOrganizer"> <p class="control" v-if="actorIsOrganizer || event.draft">
<a class="button is-danger" @click="openDeleteEventModalWrapper"> <a class="button is-danger" @click="openDeleteEventModalWrapper">
{{ $t('Delete') }} {{ $t('Delete') }}
</a> </a>
@ -133,7 +143,7 @@
</div> </div>
</div> </div>
</div> </div>
<section class="share"> <section class="share" v-if="!event.draft">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column is-half has-text-centered"> <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; 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 { get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${this.event.title}`; 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 { p.tags {
span { span {
&.tag { &.tag.is-success {
&::before { &::before {
content: '#'; content: '#';
} }
text-transform: uppercase; text-transform: uppercase;
color: #111111;
} }
&.visibility::before {
content: "⋅"
}
margin: auto 5px; margin: auto 5px;
} }
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@ -26,6 +26,19 @@
v-if="hasMoreFutureParticipations && (futureParticipations.length === limit)" @click="loadMoreFutureParticipations" size="is-large" type="is-primary">{{ $t('Load more') }}</b-button> v-if="hasMoreFutureParticipations && (futureParticipations.length === limit)" @click="loadMoreFutureParticipations" size="is-large" type="is-primary">{{ $t('Load more') }}</b-button>
</div> </div>
</section> </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"> <section v-if="pastParticipations.length > 0">
<h2 class="subtitle"> <h2 class="subtitle">
{{ $t('Past events') }} {{ $t('Past events') }}
@ -56,13 +69,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor'; import { LOGGED_USER_PARTICIPATIONS, LOGGED_USER_DRAFTS } from '@/graphql/actor';
import { IParticipant, Participant } from '@/types/event.model'; import { EventModel, IEvent, IParticipant, Participant } from '@/types/event.model';
import EventListCard from '@/components/Event/EventListCard.vue'; import EventListCard from '@/components/Event/EventListCard.vue';
import EventCard from '@/components/Event/EventCard.vue';
@Component({ @Component({
components: { components: {
EventCard,
EventListCard, EventListCard,
}, },
apollo: { apollo: {
@ -75,6 +90,14 @@ import EventListCard from '@/components/Event/EventListCard.vue';
}, },
update: data => data.loggedUser.participations.map(participation => new Participant(participation)), 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: { pastParticipations: {
query: LOGGED_USER_PARTICIPATIONS, query: LOGGED_USER_PARTICIPATIONS,
variables: { variables: {
@ -97,6 +120,8 @@ export default class MyEvents extends Vue {
pastParticipations: IParticipant[] = []; pastParticipations: IParticipant[] = [];
hasMorePastParticipations: boolean = true; hasMorePastParticipations: boolean = true;
drafts: IEvent[] = [];
private monthlyParticipations(participations: IParticipant[]): Map<string, Participant[]> { private monthlyParticipations(participations: IParticipant[]): Map<string, Participant[]> {
const res = participations.filter(({ event }) => event.beginsOn != null); const res = participations.filter(({ event }) => event.beginsOn != null);
res.sort( res.sort(

View File

@ -32,6 +32,7 @@ defmodule Mobilizon.Events.Event do
ends_on: DateTime.t(), ends_on: DateTime.t(),
title: String.t(), title: String.t(),
status: EventStatus.t(), status: EventStatus.t(),
draft: boolean,
visibility: EventVisibility.t(), visibility: EventVisibility.t(),
join_options: JoinOptions.t(), join_options: JoinOptions.t(),
publish_at: DateTime.t(), publish_at: DateTime.t(),
@ -57,6 +58,7 @@ defmodule Mobilizon.Events.Event do
:ends_on, :ends_on,
:category, :category,
:status, :status,
:draft,
:visibility, :visibility,
:publish_at, :publish_at,
:online_address, :online_address,
@ -74,6 +76,7 @@ defmodule Mobilizon.Events.Event do
:ends_on, :ends_on,
:category, :category,
:status, :status,
:draft,
:visibility, :visibility,
:join_options, :join_options,
:publish_at, :publish_at,
@ -93,6 +96,7 @@ defmodule Mobilizon.Events.Event do
field(:ends_on, :utc_datetime) field(:ends_on, :utc_datetime)
field(:title, :string) field(:title, :string)
field(:status, EventStatus, default: :confirmed) field(:status, EventStatus, default: :confirmed)
field(:draft, :boolean, default: false)
field(:visibility, EventVisibility, default: :public) field(:visibility, EventVisibility, default: :public)
field(:join_options, JoinOptions, default: :free) field(:join_options, JoinOptions, default: :free)
field(:publish_at, :utc_datetime) field(:publish_at, :utc_datetime)

View File

@ -169,6 +169,7 @@ defmodule Mobilizon.Events do
url url
|> event_by_url_query() |> event_by_url_query()
|> filter_public_visibility() |> filter_public_visibility()
|> filter_draft()
|> preload_for_event() |> preload_for_event()
|> Repo.one() |> Repo.one()
@ -190,18 +191,32 @@ defmodule Mobilizon.Events do
url url
|> event_by_url_query() |> event_by_url_query()
|> filter_public_visibility() |> filter_public_visibility()
|> filter_draft()
|> preload_for_event() |> preload_for_event()
|> Repo.one!() |> Repo.one!()
end end
@doc """ @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 @spec get_public_event_by_uuid_with_preload(String.t()) :: Event.t() | nil
def get_public_event_by_uuid_with_preload(uuid) do def get_public_event_by_uuid_with_preload(uuid) do
uuid uuid
|> event_by_uuid_query() |> event_by_uuid_query()
|> filter_public_visibility() |> 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() |> preload_for_event()
|> Repo.one() |> Repo.one()
end end
@ -215,6 +230,7 @@ defmodule Mobilizon.Events do
|> upcoming_public_event_for_actor_query() |> upcoming_public_event_for_actor_query()
|> filter_public_visibility() |> filter_public_visibility()
|> filter_not_event_uuid(not_event_uuid) |> filter_not_event_uuid(not_event_uuid)
|> filter_draft()
|> Repo.one() |> Repo.one()
end end
@ -223,16 +239,18 @@ defmodule Mobilizon.Events do
""" """
@spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} @spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
def create_event(attrs \\ %{}) do 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} <- {:ok, %Participant{} = _participant} <-
%Participant{} create_participant(%{
|> Participant.changeset(%{
actor_id: event.organizer_actor_id, actor_id: event.organizer_actor_id,
role: :creator, role: :creator,
event_id: event.id event_id: event.id
}) }) do
|> Repo.insert() do
{:ok, event} {: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
end end
@ -262,10 +280,24 @@ defmodule Mobilizon.Events do
Updates an event. Updates an event.
""" """
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} @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 <- with %Ecto.Changeset{changes: changes} = changeset <-
old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do 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( Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event, old_event,
new_event, new_event,
@ -309,6 +341,7 @@ defmodule Mobilizon.Events do
|> sort(sort, direction) |> sort(sort, direction)
|> filter_future_events(is_future) |> filter_future_events(is_future)
|> filter_unlisted(is_unlisted) |> filter_unlisted(is_unlisted)
|> filter_draft()
|> Repo.all() |> Repo.all()
end end
@ -320,6 +353,7 @@ defmodule Mobilizon.Events do
tags tags
|> Enum.map(& &1.id) |> Enum.map(& &1.id)
|> events_by_tags_query(limit) |> events_by_tags_query(limit)
|> filter_draft()
|> Repo.all() |> Repo.all()
end end
@ -333,6 +367,7 @@ defmodule Mobilizon.Events do
actor_id actor_id
|> event_for_actor_query() |> event_for_actor_query()
|> filter_public_visibility() |> filter_public_visibility()
|> filter_draft()
|> preload_for_event() |> preload_for_event()
|> Page.paginate(page, limit) |> Page.paginate(page, limit)
|> Repo.all() |> Repo.all()
@ -345,6 +380,15 @@ defmodule Mobilizon.Events do
{:ok, events, events_count} {:ok, events, events_count}
end 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 """ @doc """
Finds close events to coordinates. Finds close events to coordinates.
Radius is in meters and defaults to 50km. Radius is in meters and defaults to 50km.
@ -354,6 +398,7 @@ defmodule Mobilizon.Events do
"SRID=#{srid};POINT(#{lon} #{lat})" "SRID=#{srid};POINT(#{lon} #{lat})"
|> Geo.WKT.decode!() |> Geo.WKT.decode!()
|> close_events_query(radius) |> close_events_query(radius)
|> filter_draft()
|> Repo.all() |> Repo.all()
end end
@ -364,6 +409,7 @@ defmodule Mobilizon.Events do
def count_local_events do def count_local_events do
count_local_events_query() count_local_events_query()
|> filter_public_visibility() |> filter_public_visibility()
|> filter_draft()
|> Repo.one() |> Repo.one()
end end
@ -1134,6 +1180,16 @@ defmodule Mobilizon.Events do
) )
end 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() @spec events_by_name_query(String.t()) :: Ecto.Query.t()
defp events_by_name_query(name) do defp events_by_name_query(name) do
from( from(
@ -1372,6 +1428,11 @@ defmodule Mobilizon.Events do
from(e in query, where: e.uuid != ^not_event_uuid) from(e in query, where: e.uuid != ^not_event_uuid)
end 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() @spec filter_future_events(Ecto.Query.t(), boolean) :: Ecto.Query.t()
defp filter_future_events(query, true) do defp filter_future_events(query, true) do
from(q in query, where: q.begins_on > ^DateTime.utc_now()) from(q in query, where: q.begins_on > ^DateTime.utc_now())

View File

@ -31,7 +31,8 @@ defmodule MobilizonWeb.API.Events do
to: args.to, to: args.to,
actor: organizer_actor, actor: organizer_actor,
object: event, 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
end end
@ -65,7 +66,7 @@ defmodule MobilizonWeb.API.Events do
actor: organizer_actor.url, actor: organizer_actor.url,
cc: [], cc: [],
object: event, object: event,
local: true local: args.metadata.draft == false
}) })
end end
end end
@ -95,7 +96,8 @@ defmodule MobilizonWeb.API.Events do
join_options: Map.get(args, :join_options), join_options: Map.get(args, :join_options),
status: Map.get(args, :status), status: Map.get(args, :status),
online_address: Map.get(args, :online_address), 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 end

View File

@ -31,6 +31,20 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, :events_max_limit_reached} {:error, :events_max_limit_reached}
end 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 def find_event(_parent, %{uuid: uuid}, _resolution) do
case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do
{:has_event, %Event{} = event} -> {:has_event, %Event{} = event} ->
@ -264,6 +278,9 @@ defmodule MobilizonWeb.Resolvers.Event do
else else
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, "Organizer actor id is not owned by the user"} {:error, "Organizer actor id is not owned by the user"}
{:error, %Ecto.Changeset{} = error} ->
{:error, error}
end end
end end

View File

@ -243,6 +243,19 @@ defmodule MobilizonWeb.Resolvers.User do
end end
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}, %{ def change_password(_parent, %{old_password: old_password, new_password: new_password}, %{
context: %{current_user: %User{password_hash: old_password_hash} = user} context: %{current_user: %User{password_hash: old_password_hash} = user}
}) do }) do

View File

@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.EventType do
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1] import Absinthe.Resolution.Helpers, only: [dataloader: 1]
import MobilizonWeb.Schema.Utils
alias Mobilizon.{Actors, Addresses} alias Mobilizon.{Actors, Addresses}
@ -60,6 +61,8 @@ defmodule MobilizonWeb.Schema.EventType do
field(:category, :string, description: "The event's category") 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(:participant_stats, :participant_stats, resolve: &Event.stats_participants_for_event/3)
field(:participants, list_of(:participant), description: "The event's participants") do 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(:category, :string, default_value: "meeting")
arg(:physical_address, :address_input) arg(:physical_address, :address_input)
arg(:options, :event_options_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 end
@desc "Update an event" @desc "Update an event"
@ -280,8 +284,9 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:category, :string) arg(:category, :string)
arg(:physical_address, :address_input) arg(:physical_address, :address_input)
arg(:options, :event_options_input) arg(:options, :event_options_input)
arg(:draft, :boolean)
resolve(&Event.update_event/3) resolve(handle_errors(&Event.update_event/3))
end end
@desc "Delete an event" @desc "Delete an event"

View File

@ -49,7 +49,7 @@ defmodule MobilizonWeb.Schema.UserType do
field(:locale, :string, description: "The user's locale") field(:locale, :string, description: "The user's locale")
field(:participations, list_of(:participant), field(:participations, list_of(:participant),
description: "The list of events this user goes to" description: "The list of participations this user has"
) do ) do
arg(:after_datetime, :datetime) arg(:after_datetime, :datetime)
arg(:before_datetime, :datetime) arg(:before_datetime, :datetime)
@ -57,6 +57,12 @@ defmodule MobilizonWeb.Schema.UserType do
arg(:limit, :integer, default_value: 10) arg(:limit, :integer, default_value: 10)
resolve(&User.user_participations/3) resolve(&User.user_participations/3)
end 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 end
enum :user_role do enum :user_role do

View File

@ -70,6 +70,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"status" => object["status"], "status" => object["status"],
"online_address" => object["onlineAddress"], "online_address" => object["onlineAddress"],
"phone_address" => object["phoneAddress"], "phone_address" => object["phoneAddress"],
"draft" => object["draft"] || false,
"url" => object["id"], "url" => object["id"],
"uuid" => object["uuid"], "uuid" => object["uuid"],
"tags" => tags, "tags" => tags,
@ -111,6 +112,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"joinOptions" => to_string(event.join_options), "joinOptions" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(), "endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(), "tag" => event.tags |> build_tags(),
"draft" => event.draft,
"id" => event.url, "id" => event.url,
"url" => event.url "url" => event.url
} }

View File

@ -319,6 +319,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"status" => metadata.status, "status" => metadata.status,
"onlineAddress" => metadata.online_address, "onlineAddress" => metadata.online_address,
"phoneAddress" => metadata.phone_address, "phoneAddress" => metadata.phone_address,
"draft" => metadata.draft,
"uuid" => uuid, "uuid" => uuid,
"tag" => "tag" =>
tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end) tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)

View File

@ -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

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api # 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 { schema {
query: RootQueryType query: RootQueryType
@ -264,6 +264,9 @@ type Event implements ActionLogObject {
"""The event's description""" """The event's description"""
description: String description: String
"""Whether or not the event is a draft"""
draft: Boolean
"""Datetime for when the event ends""" """Datetime for when the event ends"""
endsOn: DateTime endsOn: DateTime
@ -897,6 +900,7 @@ type RootMutationType {
beginsOn: DateTime! beginsOn: DateTime!
category: String = "meeting" category: String = "meeting"
description: String! description: String!
draft: Boolean = false
endsOn: DateTime endsOn: DateTime
joinOptions: EventJoinOptions = FREE joinOptions: EventJoinOptions = FREE
onlineAddress: String onlineAddress: String
@ -973,7 +977,7 @@ type RootMutationType {
createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote
"""Create an user""" """Create an user"""
createUser(email: String!, password: String!): User createUser(email: String!, locale: String, password: String!): User
"""Delete an event""" """Delete an event"""
deleteEvent(actorId: ID!, eventId: ID!): DeletedObject deleteEvent(actorId: ID!, eventId: ID!): DeletedObject
@ -1030,19 +1034,20 @@ type RootMutationType {
): Person ): Person
"""Resend registration confirmation token""" """Resend registration confirmation token"""
resendConfirmationEmail(email: String!, locale: String = "en"): String resendConfirmationEmail(email: String!, locale: String): String
"""Reset user password""" """Reset user password"""
resetPassword(locale: String = "en", password: String!, token: String!): Login resetPassword(locale: String = "en", password: String!, token: String!): Login
"""Send a link through email to reset user password""" """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""" """Update an event"""
updateEvent( updateEvent(
beginsOn: DateTime beginsOn: DateTime
category: String category: String
description: String description: String
draft: Boolean
endsOn: DateTime endsOn: DateTime
eventId: ID! eventId: ID!
joinOptions: EventJoinOptions = FREE joinOptions: EventJoinOptions = FREE
@ -1212,6 +1217,9 @@ type User {
"""The user's default actor""" """The user's default actor"""
defaultActor: Person defaultActor: Person
"""The list of draft events this user has created"""
drafts(limit: Int = 10, page: Int = 1): [Event]
"""The user's email""" """The user's email"""
email: String! email: String!
@ -1221,7 +1229,10 @@ type User {
"""The user's ID""" """The user's ID"""
id: 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] 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

@ -119,6 +119,97 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event" assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
end 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 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() begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
ends_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" "picture for my event"
end 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 test "list_events/3 returns events", context do
event = insert(:event) event = insert(:event)
@ -782,6 +1024,24 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["events"] |> Enum.map(& &1["uuid"]) == [] assert json_response(res, 200)["data"]["events"] |> Enum.map(& &1["uuid"]) == []
end 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 test "find_event/3 returns an unlisted event", context do
event = insert(:event, visibility: :unlisted) event = insert(:event, visibility: :unlisted)