Improve event creation form by introducting EventOptions
It's a subentity that holds additional metadata in a map database type Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
f928be3200
commit
cb96b807a0
|
@ -99,6 +99,7 @@ export default class App extends Vue {
|
||||||
@import "~buefy/src/scss/components/taginput";
|
@import "~buefy/src/scss/components/taginput";
|
||||||
@import "~buefy/src/scss/components/upload";
|
@import "~buefy/src/scss/components/upload";
|
||||||
@import "~buefy/src/scss/components/radio";
|
@import "~buefy/src/scss/components/radio";
|
||||||
|
@import "~buefy/src/scss/components/switch";
|
||||||
|
|
||||||
.router-enter-active,
|
.router-enter-active,
|
||||||
.router-leave-active {
|
.router-leave-active {
|
||||||
|
|
|
@ -460,18 +460,6 @@ export default class CreateEvent extends Vue {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
|
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
|
||||||
|
|
||||||
&.bar-is-hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-focused {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
height: auto;
|
|
||||||
transition: visibility 0.2s, opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -510,10 +498,14 @@ export default class CreateEvent extends Vue {
|
||||||
div.ProseMirror {
|
div.ProseMirror {
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
|
||||||
|
box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #363636;
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: #3273dc;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,7 @@ export const CREATE_EVENT = gql`
|
||||||
$beginsOn: DateTime!,
|
$beginsOn: DateTime!,
|
||||||
$picture: PictureInput,
|
$picture: PictureInput,
|
||||||
$tags: [String],
|
$tags: [String],
|
||||||
|
$options: EventOptionsInput,
|
||||||
$physicalAddress: AddressInput,
|
$physicalAddress: AddressInput,
|
||||||
$visibility: EventVisibility
|
$visibility: EventVisibility
|
||||||
) {
|
) {
|
||||||
|
@ -155,6 +156,7 @@ export const CREATE_EVENT = gql`
|
||||||
beginsOn: $beginsOn,
|
beginsOn: $beginsOn,
|
||||||
organizerActorId: $organizerActorId,
|
organizerActorId: $organizerActorId,
|
||||||
category: $category,
|
category: $category,
|
||||||
|
options: $options,
|
||||||
picture: $picture,
|
picture: $picture,
|
||||||
tags: $tags,
|
tags: $tags,
|
||||||
physicalAddress: $physicalAddress,
|
physicalAddress: $physicalAddress,
|
||||||
|
|
|
@ -22,6 +22,12 @@ export enum EventJoinOptions {
|
||||||
INVITE,
|
INVITE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum EventVisibilityJoinOptions {
|
||||||
|
PUBLIC = 'PUBLIC',
|
||||||
|
LINK = 'LINK',
|
||||||
|
LIMITED = 'LIMITED',
|
||||||
|
}
|
||||||
|
|
||||||
export enum ParticipantRole {
|
export enum ParticipantRole {
|
||||||
NOT_APPROVED = 'not_approved',
|
NOT_APPROVED = 'not_approved',
|
||||||
PARTICIPANT = 'participant',
|
PARTICIPANT = 'participant',
|
||||||
|
@ -44,6 +50,24 @@ export interface IParticipant {
|
||||||
event: IEvent;
|
event: IEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOffer {
|
||||||
|
price: number;
|
||||||
|
priceCurrency: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParticipationCondition {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CommentModeration {
|
||||||
|
ALLOW_ALL = 'ALLOW_ALL',
|
||||||
|
MODERATED = 'MODERATED',
|
||||||
|
CLOSED = 'CLOSED',
|
||||||
|
}
|
||||||
|
|
||||||
export interface IEvent {
|
export interface IEvent {
|
||||||
id?: number;
|
id?: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@ -77,6 +101,31 @@ export interface IEvent {
|
||||||
physicalAddress?: IAddress;
|
physicalAddress?: IAddress;
|
||||||
|
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
|
options: IEventOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEventOptions {
|
||||||
|
maximumAttendeeCapacity: number;
|
||||||
|
remainingAttendeeCapacity: number;
|
||||||
|
showRemainingAttendeeCapacity: boolean;
|
||||||
|
offers: IOffer[];
|
||||||
|
participationConditions: IParticipationCondition[];
|
||||||
|
attendees: string[];
|
||||||
|
program: string;
|
||||||
|
commentModeration: CommentModeration;
|
||||||
|
showParticipationPrice: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventOptions implements IEventOptions {
|
||||||
|
maximumAttendeeCapacity: number = 0;
|
||||||
|
remainingAttendeeCapacity: number = 0;
|
||||||
|
showRemainingAttendeeCapacity: boolean = false;
|
||||||
|
offers: IOffer[] = [];
|
||||||
|
participationConditions: IParticipationCondition[] = [];
|
||||||
|
attendees: string[] = [];
|
||||||
|
program: string = '';
|
||||||
|
commentModeration: CommentModeration = CommentModeration.ALLOW_ALL;
|
||||||
|
showParticipationPrice: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventModel implements IEvent {
|
export class EventModel implements IEvent {
|
||||||
|
@ -113,6 +162,7 @@ export class EventModel implements IEvent {
|
||||||
organizerActor = new Actor();
|
organizerActor = new Actor();
|
||||||
|
|
||||||
tags: ITag[] = [];
|
tags: ITag[] = [];
|
||||||
|
options: IEventOptions = new EventOptions();
|
||||||
|
|
||||||
constructor(hash?: IEvent) {
|
constructor(hash?: IEvent) {
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
|
@ -150,5 +200,6 @@ export class EventModel implements IEvent {
|
||||||
this.physicalAddress = hash.physicalAddress;
|
this.physicalAddress = hash.physicalAddress;
|
||||||
|
|
||||||
this.tags = hash.tags;
|
this.tags = hash.tags;
|
||||||
|
this.options = hash.options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,7 +226,7 @@ export default class EditIdentity extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteIdentityConfirmation() {
|
openDeleteIdentityConfirmation() {
|
||||||
this.$buefy.dialog.prompt({
|
this.$dialog.prompt({
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
title: this.$gettext('Delete your identity'),
|
title: this.$gettext('Delete your identity'),
|
||||||
message: this.$gettextInterpolate(
|
message: this.$gettextInterpolate(
|
||||||
|
|
|
@ -32,6 +32,10 @@
|
||||||
<editor v-model="event.description" />
|
<editor v-model="event.description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<b-field :label="$gettext('Website / URL')">
|
||||||
|
<b-input v-model="event.onlineAddress" placeholder="URL" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
<!--<b-field :label="$gettext('Category')">
|
<!--<b-field :label="$gettext('Category')">
|
||||||
<b-select placeholder="Select a category" v-model="event.category">
|
<b-select placeholder="Select a category" v-model="event.category">
|
||||||
<option
|
<option
|
||||||
|
@ -44,23 +48,124 @@
|
||||||
|
|
||||||
<h2 class="subtitle">
|
<h2 class="subtitle">
|
||||||
<translate>
|
<translate>
|
||||||
Visibility
|
Who can view this event and participate
|
||||||
</translate>
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<label class="label">{{ $gettext('Event visibility') }}</label>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PUBLIC">
|
<b-radio v-model="eventVisibilityJoinOptions"
|
||||||
|
name="eventVisibilityJoinOptions"
|
||||||
|
:native-value="EventVisibilityJoinOptions.PUBLIC">
|
||||||
<translate>Visible everywhere on the web (public)</translate>
|
<translate>Visible everywhere on the web (public)</translate>
|
||||||
</b-radio>
|
</b-radio>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PRIVATE">
|
<b-radio v-model="eventVisibilityJoinOptions"
|
||||||
|
name="eventVisibilityJoinOptions"
|
||||||
|
:native-value="EventVisibilityJoinOptions.LINK">
|
||||||
<translate>Only accessible through link and search (private)</translate>
|
<translate>Only accessible through link and search (private)</translate>
|
||||||
</b-radio>
|
</b-radio>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="eventVisibilityJoinOptions"
|
||||||
|
name="eventVisibilityJoinOptions"
|
||||||
|
:native-value="EventVisibilityJoinOptions.LIMITED">
|
||||||
|
<translate>Page limited to my group (asks for auth)</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Approbation des participations</label>
|
||||||
|
<b-switch v-model="needsApproval">
|
||||||
|
Je veux approuver chaque demande de participation
|
||||||
|
</b-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Mise en avant</label>
|
||||||
|
<b-switch v-model="doNotPromote" :disabled="canPromote === false">
|
||||||
|
Ne pas autoriser la mise en avant sur sur Mobilizon
|
||||||
|
</b-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-switch v-model="limitedPlaces">
|
||||||
|
Places limitées
|
||||||
|
</b-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" v-if="limitedPlaces">
|
||||||
|
<b-field label="Number of places">
|
||||||
|
<b-numberinput v-model="event.options.maximumAttendeeCapacity"></b-numberinput>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-switch v-model="event.options.showRemainingAttendeeCapacity">
|
||||||
|
Show remaining number of places
|
||||||
|
</b-switch>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-switch v-model="event.options.showParticipationPrice">
|
||||||
|
Display participation price
|
||||||
|
</b-switch>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="subtitle">
|
||||||
|
<translate>
|
||||||
|
Modération des commentaires publics
|
||||||
|
</translate>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<label>Comments on the event page</label>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="event.options.commentModeration"
|
||||||
|
name="commentModeration"
|
||||||
|
:native-value="CommentModeration.ALLOW_ALL">
|
||||||
|
<translate>Allow all comments</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="event.options.commentModeration"
|
||||||
|
name="commentModeration"
|
||||||
|
:native-value="CommentModeration.MODERATED">
|
||||||
|
<translate>Moderated comments (shown after approval)</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="event.options.commentModeration"
|
||||||
|
name="commentModeration"
|
||||||
|
:native-value="CommentModeration.CLOSED">
|
||||||
|
<translate>Close all comments (except for admins)</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="subtitle">
|
||||||
|
<translate>
|
||||||
|
Status
|
||||||
|
</translate>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="event.status"
|
||||||
|
name="status"
|
||||||
|
:native-value="EventStatus.TENTATIVE">
|
||||||
|
<translate>Tentative: Will be confirmed later</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<b-radio v-model="event.status"
|
||||||
|
name="status"
|
||||||
|
:native-value="EventStatus.CONFIRMED">
|
||||||
|
<translate>Confirmed: Will happen</translate>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="button is-primary">
|
<button class="button is-primary">
|
||||||
|
|
||||||
<translate v-if="isUpdate === false">Create my event</translate>
|
<translate v-if="isUpdate === false">Create my event</translate>
|
||||||
<translate v-else>Update my event</translate>
|
<translate v-else>Update my event</translate>
|
||||||
</button>
|
</button>
|
||||||
|
@ -72,7 +177,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
|
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
import { EventModel, EventVisibility, IEvent } from '@/types/event.model';
|
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, IEvent, CommentModeration } from '@/types/event.model';
|
||||||
import { LOGGED_PERSON } from '@/graphql/actor';
|
import { LOGGED_PERSON } from '@/graphql/actor';
|
||||||
import { IPerson, Person } from '@/types/actor';
|
import { IPerson, Person } from '@/types/actor';
|
||||||
import PictureUpload from '@/components/PictureUpload.vue';
|
import PictureUpload from '@/components/PictureUpload.vue';
|
||||||
|
@ -105,7 +210,14 @@ export default class EditEvent extends Vue {
|
||||||
event = new EventModel();
|
event = new EventModel();
|
||||||
pictureFile: File | null = null;
|
pictureFile: File | null = null;
|
||||||
|
|
||||||
EventVisibility = EventVisibility;
|
EventStatus = EventStatus;
|
||||||
|
EventVisibilityJoinOptions = EventVisibilityJoinOptions;
|
||||||
|
eventVisibilityJoinOptions: EventVisibilityJoinOptions = EventVisibilityJoinOptions.PUBLIC;
|
||||||
|
needsApproval: boolean = false;
|
||||||
|
doNotPromote: boolean = false;
|
||||||
|
canPromote: boolean = true;
|
||||||
|
limitedPlaces: boolean = false;
|
||||||
|
CommentModeration = CommentModeration;
|
||||||
|
|
||||||
// categories: string[] = Object.keys(Category);
|
// categories: string[] = Object.keys(Category);
|
||||||
|
|
||||||
|
@ -129,6 +241,7 @@ export default class EditEvent extends Vue {
|
||||||
|
|
||||||
this.event.beginsOn = now;
|
this.event.beginsOn = now;
|
||||||
this.event.endsOn = end;
|
this.event.endsOn = end;
|
||||||
|
console.log('eventvisibilityjoinoptions', this.eventVisibilityJoinOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
createOrUpdate(e: Event) {
|
createOrUpdate(e: Event) {
|
||||||
|
@ -141,7 +254,7 @@ export default class EditEvent extends Vue {
|
||||||
|
|
||||||
async createEvent() {
|
async createEvent() {
|
||||||
try {
|
try {
|
||||||
const data = await this.$apollo.mutate({
|
const { data } = await this.$apollo.mutate({
|
||||||
mutation: CREATE_EVENT,
|
mutation: CREATE_EVENT,
|
||||||
variables: this.buildVariables(),
|
variables: this.buildVariables(),
|
||||||
});
|
});
|
||||||
|
@ -204,6 +317,26 @@ export default class EditEvent extends Vue {
|
||||||
return new EventModel(result.data.event);
|
return new EventModel(result.data.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Watch('eventVisibilityJoinOptions')
|
||||||
|
calculateVisibilityAndJoinOptions(eventVisibilityJoinOptions) {
|
||||||
|
switch (eventVisibilityJoinOptions) {
|
||||||
|
case EventVisibilityJoinOptions.PUBLIC:
|
||||||
|
this.event.visibility = EventVisibility.UNLISTED;
|
||||||
|
this.canPromote = true;
|
||||||
|
break;
|
||||||
|
case EventVisibilityJoinOptions.LINK:
|
||||||
|
this.event.visibility = EventVisibility.PRIVATE;
|
||||||
|
this.canPromote = false;
|
||||||
|
this.doNotPromote = false;
|
||||||
|
break;
|
||||||
|
case EventVisibilityJoinOptions.LIMITED:
|
||||||
|
this.event.visibility = EventVisibility.RESTRICTED;
|
||||||
|
this.canPromote = false;
|
||||||
|
this.doNotPromote = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getAddressData(addressData) {
|
// getAddressData(addressData) {
|
||||||
// if (addressData !== null) {
|
// if (addressData !== null) {
|
||||||
// this.event.address = {
|
// this.event.address = {
|
||||||
|
@ -224,3 +357,16 @@ export default class EditEvent extends Vue {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/variables.scss";
|
||||||
|
|
||||||
|
h2.subtitle {
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 5px 7px;
|
||||||
|
display: inline;
|
||||||
|
background: $secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -54,6 +54,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
field(:online_address, :string)
|
field(:online_address, :string)
|
||||||
field(:phone_address, :string)
|
field(:phone_address, :string)
|
||||||
field(:category, :string)
|
field(:category, :string)
|
||||||
|
embeds_one(:options, Mobilizon.Events.EventOptions)
|
||||||
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
||||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||||
many_to_many(:tags, Tag, join_through: "events_tags")
|
many_to_many(:tags, Tag, join_through: "events_tags")
|
||||||
|
@ -87,6 +88,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
:picture_id,
|
:picture_id,
|
||||||
:physical_address_id
|
:physical_address_id
|
||||||
])
|
])
|
||||||
|
|> cast_embed(:options)
|
||||||
|> validate_required([
|
|> validate_required([
|
||||||
:title,
|
:title,
|
||||||
:begins_on,
|
:begins_on,
|
||||||
|
|
69
lib/mobilizon/events/event_options.ex
Normal file
69
lib/mobilizon/events/event_options.ex
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import EctoEnum
|
||||||
|
|
||||||
|
defenum(Mobilizon.Events.CommentModeration, :comment_moderation, [:allow_all, :moderated, :closed])
|
||||||
|
|
||||||
|
defmodule Mobilizon.Events.EventOffer do
|
||||||
|
@moduledoc """
|
||||||
|
Represents an event offer
|
||||||
|
"""
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:price, :float)
|
||||||
|
field(:price_currency, :string)
|
||||||
|
field(:url, :string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Mobilizon.Events.EventParticipationCondition do
|
||||||
|
@moduledoc """
|
||||||
|
Represents an event participation condition
|
||||||
|
"""
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:title, :string)
|
||||||
|
field(:content, :string)
|
||||||
|
field(:url, :string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Mobilizon.Events.EventOptions do
|
||||||
|
@moduledoc """
|
||||||
|
Represents an event options
|
||||||
|
"""
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Mobilizon.Events.{
|
||||||
|
EventOptions,
|
||||||
|
EventOffer,
|
||||||
|
EventParticipationCondition,
|
||||||
|
CommentModeration
|
||||||
|
}
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
embedded_schema do
|
||||||
|
field(:maximum_attendee_capacity, :integer)
|
||||||
|
field(:remaining_attendee_capacity, :integer)
|
||||||
|
field(:show_remaining_attendee_capacity, :boolean)
|
||||||
|
embeds_many(:offers, EventOffer)
|
||||||
|
embeds_many(:participation_condition, EventParticipationCondition)
|
||||||
|
field(:attendees, {:array, :string})
|
||||||
|
field(:program, :string)
|
||||||
|
field(:comment_moderation, CommentModeration)
|
||||||
|
field(:show_participation_price, :boolean)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(%EventOptions{} = event_options, attrs) do
|
||||||
|
event_options
|
||||||
|
|> Ecto.Changeset.cast(attrs, [
|
||||||
|
:maximum_attendee_capacity,
|
||||||
|
:remaining_attendee_capacity,
|
||||||
|
:show_remaining_attendee_capacity,
|
||||||
|
:attendees,
|
||||||
|
:program,
|
||||||
|
:comment_moderation,
|
||||||
|
:show_participation_price
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,14 +14,15 @@ defmodule MobilizonWeb.API.Events do
|
||||||
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
|
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
|
||||||
def create_event(
|
def create_event(
|
||||||
%{
|
%{
|
||||||
title: title,
|
|
||||||
description: description,
|
|
||||||
organizer_actor_id: organizer_actor_id,
|
|
||||||
begins_on: begins_on,
|
begins_on: begins_on,
|
||||||
category: category,
|
description: description,
|
||||||
tags: tags
|
options: options,
|
||||||
|
organizer_actor_id: organizer_actor_id,
|
||||||
|
tags: tags,
|
||||||
|
title: title
|
||||||
} = args
|
} = args
|
||||||
) do
|
)
|
||||||
|
when is_map(options) do
|
||||||
with %Actor{url: url} = actor <-
|
with %Actor{url: url} = actor <-
|
||||||
Actors.get_local_actor_with_everything(organizer_actor_id),
|
Actors.get_local_actor_with_everything(organizer_actor_id),
|
||||||
physical_address <- Map.get(args, :physical_address, nil),
|
physical_address <- Map.get(args, :physical_address, nil),
|
||||||
|
@ -38,7 +39,12 @@ defmodule MobilizonWeb.API.Events do
|
||||||
content_html,
|
content_html,
|
||||||
picture,
|
picture,
|
||||||
tags,
|
tags,
|
||||||
%{begins_on: begins_on, physical_address: physical_address, category: category}
|
%{
|
||||||
|
begins_on: begins_on,
|
||||||
|
physical_address: physical_address,
|
||||||
|
category: Map.get(args, :category),
|
||||||
|
options: options
|
||||||
|
}
|
||||||
) do
|
) do
|
||||||
ActivityPub.create(%{
|
ActivityPub.create(%{
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
|
|
@ -68,6 +68,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
|
|
||||||
field(:updated_at, :datetime, description: "When the event was last updated")
|
field(:updated_at, :datetime, description: "When the event was last updated")
|
||||||
field(:created_at, :datetime, description: "When the event was created")
|
field(:created_at, :datetime, description: "When the event was created")
|
||||||
|
field(:options, :event_options, description: "The event options")
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "The list of visibility options for an event"
|
@desc "The list of visibility options for an event"
|
||||||
|
@ -90,6 +91,101 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
value(:cancelled, description: "The event is cancelled")
|
value(:cancelled, description: "The event is cancelled")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
object :event_offer do
|
||||||
|
field(:price, :float, description: "The price amount for this offer")
|
||||||
|
field(:price_currency, :string, description: "The currency for this price offer")
|
||||||
|
field(:url, :string, description: "The URL to access to this offer")
|
||||||
|
end
|
||||||
|
|
||||||
|
object :event_participation_condition do
|
||||||
|
field(:title, :string, description: "The title for this condition")
|
||||||
|
field(:content, :string, description: "The content for this condition")
|
||||||
|
field(:url, :string, description: "The URL to access this condition")
|
||||||
|
end
|
||||||
|
|
||||||
|
input_object :event_offer_input do
|
||||||
|
field(:price, :float, description: "The price amount for this offer")
|
||||||
|
field(:price_currency, :string, description: "The currency for this price offer")
|
||||||
|
field(:url, :string, description: "The URL to access to this offer")
|
||||||
|
end
|
||||||
|
|
||||||
|
input_object :event_participation_condition_input do
|
||||||
|
field(:title, :string, description: "The title for this condition")
|
||||||
|
field(:content, :string, description: "The content for this condition")
|
||||||
|
field(:url, :string, description: "The URL to access this condition")
|
||||||
|
end
|
||||||
|
|
||||||
|
@desc "The list of possible options for the event's status"
|
||||||
|
enum :event_comment_moderation do
|
||||||
|
value(:allow_all, description: "Anyone can comment under the event")
|
||||||
|
value(:moderated, description: "Every comment has to be moderated by the admin")
|
||||||
|
value(:closed, description: "No one can comment except for the admin")
|
||||||
|
end
|
||||||
|
|
||||||
|
object :event_options do
|
||||||
|
field(:maximum_attendee_capacity, :integer,
|
||||||
|
description: "The maximum attendee capacity for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:remaining_attendee_capacity, :integer,
|
||||||
|
description: "The number of remaining seats for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:show_remaining_attendee_capacity, :boolean,
|
||||||
|
description: "Whether or not to show the number of remaining seats for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:offers, list_of(:event_offer), description: "The list of offers to show for this event")
|
||||||
|
|
||||||
|
field(:participation_conditions, list_of(:event_participation_condition),
|
||||||
|
description: "The list of participation conditions to accept to join this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:attendees, list_of(:string), description: "The list of special attendees")
|
||||||
|
field(:program, :string, description: "The list of the event")
|
||||||
|
|
||||||
|
field(:comment_moderation, :event_comment_moderation,
|
||||||
|
description: "The policy on public comment moderation under the event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:show_participation_price, :boolean,
|
||||||
|
description: "Whether or not to show the participation price"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
input_object :event_options_input do
|
||||||
|
field(:maximum_attendee_capacity, :integer,
|
||||||
|
description: "The maximum attendee capacity for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:remaining_attendee_capacity, :integer,
|
||||||
|
description: "The number of remaining seats for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:show_remaining_attendee_capacity, :boolean,
|
||||||
|
description: "Whether or not to show the number of remaining seats for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:offers, list_of(:event_offer_input),
|
||||||
|
description: "The list of offers to show for this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:participation_conditions, list_of(:event_participation_condition_input),
|
||||||
|
description: "The list of participation conditions to accept to join this event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:attendees, list_of(:string), description: "The list of special attendees")
|
||||||
|
field(:program, :string, description: "The list of the event")
|
||||||
|
|
||||||
|
field(:comment_moderation, :event_comment_moderation,
|
||||||
|
description: "The policy on public comment moderation under the event"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:show_participation_price, :boolean,
|
||||||
|
description: "Whether or not to show the participation price"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
object :event_queries do
|
object :event_queries do
|
||||||
@desc "Get all events"
|
@desc "Get all events"
|
||||||
field :events, list_of(:event) do
|
field :events, list_of(:event) do
|
||||||
|
@ -131,8 +227,9 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
arg(:online_address, :string)
|
arg(:online_address, :string)
|
||||||
arg(:phone_address, :string)
|
arg(:phone_address, :string)
|
||||||
arg(:organizer_actor_id, non_null(:id))
|
arg(:organizer_actor_id, non_null(:id))
|
||||||
arg(:category, :string)
|
arg(:category, :string, default_value: "meeting")
|
||||||
arg(:physical_address, :address_input)
|
arg(:physical_address, :address_input)
|
||||||
|
arg(:options, :event_options_input, default_value: %{})
|
||||||
|
|
||||||
resolve(&Event.create_event/3)
|
resolve(&Event.create_event/3)
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
{:actor, Actors.get_actor_by_url(object["actor"])},
|
{:actor, Actors.get_actor_by_url(object["actor"])},
|
||||||
{:address, address_id} <-
|
{:address, address_id} <-
|
||||||
{:address, get_address(object["location"])},
|
{:address, get_address(object["location"])},
|
||||||
{:tags, tags} <- {:tags, fetch_tags(object["tag"])} do
|
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
|
||||||
|
{:options, options} <- {:options, get_options(object)} do
|
||||||
picture_id =
|
picture_id =
|
||||||
with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0,
|
with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0,
|
||||||
%Picture{id: picture_id} <-
|
%Picture{id: picture_id} <-
|
||||||
|
@ -50,8 +51,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
entity = %{
|
||||||
%{
|
|
||||||
"title" => object["name"],
|
"title" => object["name"],
|
||||||
"description" => object["content"],
|
"description" => object["content"],
|
||||||
"organizer_actor_id" => actor_id,
|
"organizer_actor_id" => actor_id,
|
||||||
|
@ -62,13 +62,30 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
"uuid" => object["uuid"],
|
"uuid" => object["uuid"],
|
||||||
"tags" => tags,
|
"tags" => tags,
|
||||||
"physical_address_id" => address_id
|
"physical_address_id" => address_id
|
||||||
}}
|
}
|
||||||
|
|
||||||
|
{:ok, Map.put(entity, "options", options)}
|
||||||
else
|
else
|
||||||
err ->
|
err ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get only elements that we have in EventOptions
|
||||||
|
defp get_options(object) do
|
||||||
|
keys =
|
||||||
|
Mobilizon.Events.EventOptions
|
||||||
|
|> struct
|
||||||
|
|> Map.keys()
|
||||||
|
|> List.delete(:__struct__)
|
||||||
|
|> Enum.map(&Utils.camelize/1)
|
||||||
|
|
||||||
|
Enum.reduce(object, %{}, fn {key, value}, acc ->
|
||||||
|
(value && key in keys && Map.put(acc, Utils.underscore(key), value)) ||
|
||||||
|
acc
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp get_address(address_url) when is_bitstring(address_url) do
|
defp get_address(address_url) when is_bitstring(address_url) do
|
||||||
get_address(%{"id" => address_url})
|
get_address(%{"id" => address_url})
|
||||||
end
|
end
|
||||||
|
|
|
@ -298,7 +298,19 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||||
do: res,
|
do: res,
|
||||||
else: Map.put(res, "location", make_address_data(metadata.physical_address))
|
else: Map.put(res, "location", make_address_data(metadata.physical_address))
|
||||||
|
|
||||||
|
res =
|
||||||
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
|
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
|
||||||
|
|
||||||
|
if is_nil(metadata.options) do
|
||||||
|
res
|
||||||
|
else
|
||||||
|
options = struct(Mobilizon.Events.EventOptions, metadata.options) |> Map.from_struct()
|
||||||
|
|
||||||
|
Enum.reduce(options, res, fn {key, value}, acc ->
|
||||||
|
(value && Map.put(acc, camelize(key), value)) ||
|
||||||
|
acc
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_address_data(%Address{} = address) do
|
def make_address_data(%Address{} = address) do
|
||||||
|
@ -669,4 +681,21 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||||
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
|
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
|
||||||
:public_key.pem_encode([public_key])
|
:public_key.pem_encode([public_key])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def camelize(word) when is_atom(word) do
|
||||||
|
camelize(to_string(word))
|
||||||
|
end
|
||||||
|
|
||||||
|
def camelize(word) when is_bitstring(word) do
|
||||||
|
{first, rest} = String.split_at(Macro.camelize(word), 1)
|
||||||
|
String.downcase(first) <> rest
|
||||||
|
end
|
||||||
|
|
||||||
|
def underscore(word) when is_atom(word) do
|
||||||
|
underscore(to_string(word))
|
||||||
|
end
|
||||||
|
|
||||||
|
def underscore(word) when is_bitstring(word) do
|
||||||
|
Macro.underscore(word)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Mobilizon.Repo.Migrations.AddOptionsToEvent do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:events) do
|
||||||
|
add(:options, :map)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
120
schema.graphql
120
schema.graphql
|
@ -1,5 +1,5 @@
|
||||||
# source: http://localhost:4000/api
|
# source: http://localhost:4000/api
|
||||||
# timestamp: Thu Aug 22 2019 12:34:52 GMT+0200 (GMT+02:00)
|
# timestamp: Mon Sep 02 2019 16:41:17 GMT+0200 (GMT+02:00)
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: RootQueryType
|
query: RootQueryType
|
||||||
|
@ -245,6 +245,9 @@ type Event {
|
||||||
"""Online address of the event"""
|
"""Online address of the event"""
|
||||||
onlineAddress: OnlineAddress
|
onlineAddress: OnlineAddress
|
||||||
|
|
||||||
|
"""The event options"""
|
||||||
|
options: EventOptions
|
||||||
|
|
||||||
"""The event's organizer (as a person)"""
|
"""The event's organizer (as a person)"""
|
||||||
organizerActor: Actor
|
organizerActor: Actor
|
||||||
|
|
||||||
|
@ -291,6 +294,120 @@ type Event {
|
||||||
visibility: EventVisibility
|
visibility: EventVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""The list of possible options for the event's status"""
|
||||||
|
enum EventCommentModeration {
|
||||||
|
"""Anyone can comment under the event"""
|
||||||
|
ALLOW_ALL
|
||||||
|
|
||||||
|
"""No one can comment except for the admin"""
|
||||||
|
CLOSED
|
||||||
|
|
||||||
|
"""Every comment has to be moderated by the admin"""
|
||||||
|
MODERATED
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventOffer {
|
||||||
|
"""The price amount for this offer"""
|
||||||
|
price: Float
|
||||||
|
|
||||||
|
"""The currency for this price offer"""
|
||||||
|
priceCurrency: String
|
||||||
|
|
||||||
|
"""The URL to access to this offer"""
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input EventOfferInput {
|
||||||
|
"""The price amount for this offer"""
|
||||||
|
price: Float
|
||||||
|
|
||||||
|
"""The currency for this price offer"""
|
||||||
|
priceCurrency: String
|
||||||
|
|
||||||
|
"""The URL to access to this offer"""
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventOptions {
|
||||||
|
"""The list of special attendees"""
|
||||||
|
attendees: [String]
|
||||||
|
|
||||||
|
"""The policy on public comment moderation under the event"""
|
||||||
|
commentModeration: EventCommentModeration
|
||||||
|
|
||||||
|
"""The maximum attendee capacity for this event"""
|
||||||
|
maximumAttendeeCapacity: Int
|
||||||
|
|
||||||
|
"""The list of offers to show for this event"""
|
||||||
|
offers: [EventOffer]
|
||||||
|
|
||||||
|
"""The list of participation conditions to accept to join this event"""
|
||||||
|
participationConditions: [EventParticipationCondition]
|
||||||
|
|
||||||
|
"""The list of the event"""
|
||||||
|
program: String
|
||||||
|
|
||||||
|
"""The number of remaining seats for this event"""
|
||||||
|
remainingAttendeeCapacity: Int
|
||||||
|
|
||||||
|
"""Whether or not to show the participation price"""
|
||||||
|
showParticipationPrice: Boolean
|
||||||
|
|
||||||
|
"""Whether or not to show the number of remaining seats for this event"""
|
||||||
|
showRemainingAttendeeCapacity: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input EventOptionsInput {
|
||||||
|
"""The list of special attendees"""
|
||||||
|
attendees: [String]
|
||||||
|
|
||||||
|
"""The policy on public comment moderation under the event"""
|
||||||
|
commentModeration: EventCommentModeration
|
||||||
|
|
||||||
|
"""The maximum attendee capacity for this event"""
|
||||||
|
maximumAttendeeCapacity: Int
|
||||||
|
|
||||||
|
"""The list of offers to show for this event"""
|
||||||
|
offers: [EventOfferInput]
|
||||||
|
|
||||||
|
"""The list of participation conditions to accept to join this event"""
|
||||||
|
participationConditions: [EventParticipationConditionInput]
|
||||||
|
|
||||||
|
"""The list of the event"""
|
||||||
|
program: String
|
||||||
|
|
||||||
|
"""The number of remaining seats for this event"""
|
||||||
|
remainingAttendeeCapacity: Int
|
||||||
|
|
||||||
|
"""Whether or not to show the participation price"""
|
||||||
|
showParticipationPrice: Boolean
|
||||||
|
|
||||||
|
"""Whether or not to show the number of remaining seats for this event"""
|
||||||
|
showRemainingAttendeeCapacity: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventParticipationCondition {
|
||||||
|
"""The content for this condition"""
|
||||||
|
content: String
|
||||||
|
|
||||||
|
"""The title for this condition"""
|
||||||
|
title: String
|
||||||
|
|
||||||
|
"""The URL to access this condition"""
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input EventParticipationConditionInput {
|
||||||
|
"""The content for this condition"""
|
||||||
|
content: String
|
||||||
|
|
||||||
|
"""The title for this condition"""
|
||||||
|
title: String
|
||||||
|
|
||||||
|
"""The URL to access this condition"""
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
"""Search events result"""
|
"""Search events result"""
|
||||||
type Events {
|
type Events {
|
||||||
"""Event elements"""
|
"""Event elements"""
|
||||||
|
@ -710,6 +827,7 @@ type RootMutationType {
|
||||||
description: String!
|
description: String!
|
||||||
endsOn: DateTime
|
endsOn: DateTime
|
||||||
onlineAddress: String
|
onlineAddress: String
|
||||||
|
options: EventOptionsInput
|
||||||
organizerActorId: ID!
|
organizerActorId: ID!
|
||||||
phoneAddress: String
|
phoneAddress: String
|
||||||
physicalAddress: AddressInput
|
physicalAddress: AddressInput
|
||||||
|
|
|
@ -84,6 +84,48 @@ 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 with options", %{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}",
|
||||||
|
options: {
|
||||||
|
maximumAttendeeCapacity: 30,
|
||||||
|
showRemainingAttendeeCapacity: true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
title,
|
||||||
|
uuid,
|
||||||
|
options {
|
||||||
|
maximumAttendeeCapacity,
|
||||||
|
showRemainingAttendeeCapacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["errors"] == nil
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["createEvent"]["options"]["maximumAttendeeCapacity"] ==
|
||||||
|
30
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["createEvent"]["options"][
|
||||||
|
"showRemainingAttendeeCapacity"
|
||||||
|
] == true
|
||||||
|
end
|
||||||
|
|
||||||
test "create_event/3 creates an event with tags", %{conn: conn, actor: actor, user: user} do
|
test "create_event/3 creates an event with tags", %{conn: conn, actor: actor, user: user} do
|
||||||
mutation = """
|
mutation = """
|
||||||
mutation {
|
mutation {
|
||||||
|
|
Loading…
Reference in a new issue