Merge branch 'external-events' into 'main'

Add option to link an external registration provider for events

See merge request framasoft/mobilizon!1223
This commit is contained in:
Thomas Citharel 2022-05-04 07:01:53 +00:00
commit d85f708c37
12 changed files with 135 additions and 6 deletions

View File

@ -0,0 +1,27 @@
<template>
<div class="participation-button">
<a
class="button is-large is-primary"
type="button"
target="_blank"
:href="
event.externalParticipationUrl
? encodeURI(`${event.externalParticipationUrl}?uuid=${event.uuid}`)
: '#'
"
:disabled="!event.externalParticipationUrl"
>{{ $t("Go to booking") }}&nbsp;
<b-icon style="margin-left: 0" icon="open-in-new"
/></a>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "../../types/event.model";
@Component
export default class ExternalParticipationButton extends Vue {
@Prop({ required: true }) event!: IEvent;
}
</script>

View File

@ -21,6 +21,7 @@ const FULL_EVENT_FRAGMENT = gql`
status status
visibility visibility
joinOptions joinOptions
externalParticipationUrl
draft draft
language language
category category
@ -121,6 +122,7 @@ export const FETCH_EVENT_BASIC = gql`
id id
uuid uuid
joinOptions joinOptions
externalParticipationUrl
participantStats { participantStats {
going going
notApproved notApproved
@ -199,6 +201,7 @@ export const CREATE_EVENT = gql`
$status: EventStatus $status: EventStatus
$visibility: EventVisibility $visibility: EventVisibility
$joinOptions: EventJoinOptions $joinOptions: EventJoinOptions
$externalParticipationUrl: String
$draft: Boolean $draft: Boolean
$tags: [String] $tags: [String]
$picture: MediaInput $picture: MediaInput
@ -220,6 +223,7 @@ export const CREATE_EVENT = gql`
status: $status status: $status
visibility: $visibility visibility: $visibility
joinOptions: $joinOptions joinOptions: $joinOptions
externalParticipationUrl: $externalParticipationUrl
draft: $draft draft: $draft
tags: $tags tags: $tags
picture: $picture picture: $picture
@ -247,6 +251,7 @@ export const EDIT_EVENT = gql`
$status: EventStatus $status: EventStatus
$visibility: EventVisibility $visibility: EventVisibility
$joinOptions: EventJoinOptions $joinOptions: EventJoinOptions
$externalParticipationUrl: String
$draft: Boolean $draft: Boolean
$tags: [String] $tags: [String]
$picture: MediaInput $picture: MediaInput
@ -269,6 +274,7 @@ export const EDIT_EVENT = gql`
status: $status status: $status
visibility: $visibility visibility: $visibility
joinOptions: $joinOptions joinOptions: $joinOptions
externalParticipationUrl: $externalParticipationUrl
draft: $draft draft: $draft
tags: $tags tags: $tags
picture: $picture picture: $picture

View File

@ -64,6 +64,7 @@ export enum EventJoinOptions {
FREE = "FREE", FREE = "FREE",
RESTRICTED = "RESTRICTED", RESTRICTED = "RESTRICTED",
INVITE = "INVITE", INVITE = "INVITE",
EXTERNAL = "EXTERNAL",
} }
export enum EventVisibilityJoinOptions { export enum EventVisibilityJoinOptions {

View File

@ -42,6 +42,7 @@ interface IEventEditJSON {
status: EventStatus; status: EventStatus;
visibility: EventVisibility; visibility: EventVisibility;
joinOptions: EventJoinOptions; joinOptions: EventJoinOptions;
externalParticipationUrl: string | null;
draft: boolean; draft: boolean;
picture?: IMedia | { mediaId: string } | null; picture?: IMedia | { mediaId: string } | null;
attributedToId: string | null; attributedToId: string | null;
@ -71,6 +72,7 @@ export interface IEvent {
status: EventStatus; status: EventStatus;
visibility: EventVisibility; visibility: EventVisibility;
joinOptions: EventJoinOptions; joinOptions: EventJoinOptions;
externalParticipationUrl: string | null;
draft: boolean; draft: boolean;
picture: IMedia | null; picture: IMedia | null;
@ -131,6 +133,8 @@ export class EventModel implements IEvent {
joinOptions = EventJoinOptions.FREE; joinOptions = EventJoinOptions.FREE;
externalParticipationUrl: string | null = null;
status = EventStatus.CONFIRMED; status = EventStatus.CONFIRMED;
draft = true; draft = true;
@ -196,6 +200,7 @@ 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.externalParticipationUrl = hash.externalParticipationUrl;
this.draft = hash.draft; this.draft = hash.draft;
this.picture = hash.picture; this.picture = hash.picture;
@ -248,6 +253,7 @@ export function toEditJSON(event: IEditableEvent): IEventEditJSON {
category: event.category, category: event.category,
visibility: event.visibility, visibility: event.visibility,
joinOptions: event.joinOptions, joinOptions: event.joinOptions,
externalParticipationUrl: event.externalParticipationUrl,
draft: event.draft, draft: event.draft,
tags: event.tags.map((t) => t.title), tags: event.tags.map((t) => t.title),
onlineAddress: event.onlineAddress, onlineAddress: event.onlineAddress,

View File

@ -221,9 +221,33 @@
</b-radio> </b-radio>
</div>--> </div>-->
<div class="field">
<label class="label">{{ $t("External registration") }}</label>
<b-switch v-model="externalParticipation">
{{
$t("I want to manage the registration with an external provider.")
}}
</b-switch>
</div>
<div class="field" v-if="externalParticipation">
<b-field :label="$t('URL')">
<b-input
icon="link"
type="url"
v-model="event.externalParticipationUrl"
placeholder="URL"
/>
</b-field>
</div>
<div <div
class="field" class="field"
v-if="config && config.anonymous.participation.allowed" v-if="
config &&
config.anonymous.participation.allowed &&
!externalParticipation
"
> >
<label class="label">{{ $t("Anonymous participations") }}</label> <label class="label">{{ $t("Anonymous participations") }}</label>
<b-switch v-model="eventOptions.anonymousParticipation"> <b-switch v-model="eventOptions.anonymousParticipation">
@ -246,21 +270,21 @@
</b-switch> </b-switch>
</div> </div>
<div class="field"> <div class="field" v-if="!externalParticipation">
<label class="label">{{ $t("Participation approval") }}</label> <label class="label">{{ $t("Participation approval") }}</label>
<b-switch v-model="needsApproval">{{ <b-switch v-model="needsApproval">{{
$t("I want to approve every participation request") $t("I want to approve every participation request")
}}</b-switch> }}</b-switch>
</div> </div>
<div class="field"> <div class="field" v-if="!externalParticipation">
<label class="label">{{ $t("Number of places") }}</label> <label class="label">{{ $t("Number of places") }}</label>
<b-switch v-model="limitedPlaces">{{ <b-switch v-model="limitedPlaces">{{
$t("Limited number of places") $t("Limited number of places")
}}</b-switch> }}</b-switch>
</div> </div>
<div class="box" v-if="limitedPlaces"> <div class="box" v-if="limitedPlaces && !externalParticipation">
<b-field :label="$t('Number of places')" label-for="number-of-places"> <b-field :label="$t('Number of places')" label-for="number-of-places">
<b-numberinput <b-numberinput
controls-position="compact" controls-position="compact"
@ -1170,6 +1194,18 @@ export default class EditEvent extends Vue {
} }
} }
get externalParticipation(): boolean {
return this.event?.joinOptions == EventJoinOptions.EXTERNAL;
}
set externalParticipation(value: boolean) {
if (value === true) {
this.event.joinOptions = EventJoinOptions.EXTERNAL;
} else {
this.event.joinOptions = EventJoinOptions.FREE;
}
}
get checkTitleLength(): Array<string | undefined> { get checkTitleLength(): Array<string | undefined> {
return this.event.title.length > 80 return this.event.title.length > 80
? ["is-info", this.$t("The event title will be ellipsed.") as string] ? ["is-info", this.$t("The event title will be ellipsed.") as string]

10
js/src/views/Event/Event.vue Executable file → Normal file
View File

@ -94,7 +94,13 @@
</span> </span>
</div> </div>
<div class="column is-3-tablet"> <div class="column is-3-tablet">
<external-participation-button
v-if="event.joinOptions === EventJoinOptions.EXTERNAL"
:event="event"
:current-actor="currentActor"
/>
<participation-section <participation-section
v-else
:participation="participations[0]" :participation="participations[0]"
:event="event" :event="event"
:anonymousParticipation="anonymousParticipation" :anonymousParticipation="anonymousParticipation"
@ -120,7 +126,7 @@
<tag>{{ organizer.domain }}</tag> <tag>{{ organizer.domain }}</tag>
</a> </a>
</template> </template>
<p> <p v-if="event.joinOptions !== EventJoinOptions.EXTERNAL">
<router-link <router-link
class="participations-link" class="participations-link"
v-if="canManageEvent && event.draft === false" v-if="canManageEvent && event.draft === false"
@ -508,6 +514,7 @@ import { CREATE_REPORT } from "../../graphql/report";
import EventMixin from "../../mixins/event"; import EventMixin from "../../mixins/event";
import IdentityPicker from "../Account/IdentityPicker.vue"; import IdentityPicker from "../Account/IdentityPicker.vue";
import ParticipationSection from "../../components/Participation/ParticipationSection.vue"; import ParticipationSection from "../../components/Participation/ParticipationSection.vue";
import ExternalParticipationButton from "../../components/Event/ExternalParticipationButton.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import CommentTree from "../../components/Comment/CommentTree.vue"; import CommentTree from "../../components/Comment/CommentTree.vue";
import "intersection-observer"; import "intersection-observer";
@ -542,6 +549,7 @@ import { IUser } from "@/types/current-user.model";
ReportModal, ReportModal,
IdentityPicker, IdentityPicker,
ParticipationSection, ParticipationSection,
ExternalParticipationButton,
CommentTree, CommentTree,
Tag, Tag,
PopoverActorCard, PopoverActorCard,

View File

@ -78,6 +78,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
visibility: visibility, visibility: visibility,
join_options: Map.get(object, "joinMode", "free"), join_options: Map.get(object, "joinMode", "free"),
local: is_local?(object["id"]), local: is_local?(object["id"]),
external_participation_url: object["externalParticipationUrl"],
options: options, options: options,
metadata: metadata, metadata: metadata,
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
@ -129,6 +130,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"mediaType" => "text/html", "mediaType" => "text/html",
"startTime" => event.begins_on |> shift_tz(event.options.timezone) |> date_to_string(), "startTime" => event.begins_on |> shift_tz(event.options.timezone) |> date_to_string(),
"joinMode" => to_string(event.join_options), "joinMode" => to_string(event.join_options),
"externalParticipationUrl" => event.external_participation_url,
"endTime" => event.ends_on |> shift_tz(event.options.timezone) |> date_to_string(), "endTime" => event.ends_on |> shift_tz(event.options.timezone) |> date_to_string(),
"tag" => event.tags |> build_tags(), "tag" => event.tags |> build_tags(),
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity, "maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,

View File

@ -30,6 +30,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
field(:status, :event_status, description: "Status of the event") field(:status, :event_status, description: "Status of the event")
field(:visibility, :event_visibility, description: "The event's visibility") field(:visibility, :event_visibility, description: "The event's visibility")
field(:join_options, :event_join_options, description: "The event's visibility") field(:join_options, :event_join_options, description: "The event's visibility")
field(:external_participation_url, :string, description: "External URL for participation")
field(:picture, :media, field(:picture, :media,
description: "The event's picture", description: "The event's picture",
@ -123,6 +124,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
value(:free, description: "Anyone can join and is automatically accepted") value(:free, description: "Anyone can join and is automatically accepted")
value(:restricted, description: "Manual acceptation") value(:restricted, description: "Manual acceptation")
value(:invite, description: "Participants must be invited") value(:invite, description: "Participants must be invited")
value(:external, description: "External registration")
end end
@desc "The list of possible options for the event's status" @desc "The list of possible options for the event's status"
@ -379,6 +381,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "The event's options to join" description: "The event's options to join"
) )
arg(:external_participation_url, :string, description: "External URL for participation")
arg(:tags, list_of(:string), arg(:tags, list_of(:string),
default_value: [], default_value: [],
description: "The list of tags associated to the event" description: "The list of tags associated to the event"
@ -439,6 +443,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "The event's options to join" description: "The event's options to join"
) )
arg(:external_participation_url, :string, description: "External URL for participation")
arg(:tags, list_of(:string), description: "The list of tags associated to the event") arg(:tags, list_of(:string), description: "The list of tags associated to the event")
arg(:picture, :media_input, arg(:picture, :media_input,

View File

@ -47,6 +47,7 @@ defmodule Mobilizon.Events.Event do
draft: boolean, draft: boolean,
visibility: atom(), visibility: atom(),
join_options: atom(), join_options: atom(),
external_participation_url: String.t(),
publish_at: DateTime.t() | nil, publish_at: DateTime.t() | nil,
uuid: Ecto.UUID.t(), uuid: Ecto.UUID.t(),
online_address: String.t() | nil, online_address: String.t() | nil,
@ -81,6 +82,7 @@ defmodule Mobilizon.Events.Event do
:local, :local,
:visibility, :visibility,
:join_options, :join_options,
:external_participation_url,
:publish_at, :publish_at,
:online_address, :online_address,
:phone_address, :phone_address,
@ -105,6 +107,7 @@ defmodule Mobilizon.Events.Event do
field(:draft, :boolean, default: false) 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(:external_participation_url, :string)
field(:publish_at, :utc_datetime) field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate()) field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string) field(:online_address, :string)

View File

@ -46,7 +46,8 @@ defmodule Mobilizon.Events do
defenum(JoinOptions, :join_options, [ defenum(JoinOptions, :join_options, [
:free, :free,
:restricted, :restricted,
:invite :invite,
:external
]) ])
defenum(EventStatus, :event_status, [ defenum(EventStatus, :event_status, [

View File

@ -0,0 +1,21 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddExternalUrlForEvents do
use Ecto.Migration
alias Mobilizon.Events.JoinOptions
def change do
alter table(:events) do
add(:external_participation_url, :string)
end
execute("ALTER TABLE events ALTER COLUMN join_options TYPE VARCHAR USING join_options::text")
execute("ALTER TABLE events ALTER COLUMN join_options DROP DEFAULT")
JoinOptions.drop_type()
JoinOptions.create_type()
execute(
"ALTER TABLE events ALTER COLUMN join_options TYPE join_options USING join_options::join_options"
)
execute("ALTER TABLE events ALTER COLUMN join_options SET DEFAULT 'free'::join_options")
end
end

View File

@ -1551,6 +1551,9 @@ type RootMutationType {
"The event's options to join" "The event's options to join"
joinOptions: EventJoinOptions joinOptions: EventJoinOptions
"External URL for participation"
externalParticipationUrl: String
"The list of tags associated to the event" "The list of tags associated to the event"
tags: [String] tags: [String]
@ -1620,6 +1623,9 @@ type RootMutationType {
"The event's options to join" "The event's options to join"
joinOptions: EventJoinOptions joinOptions: EventJoinOptions
"External URL for participation"
externalParticipationUrl: String
"The list of tags associated to the event" "The list of tags associated to the event"
tags: [String] tags: [String]
@ -2962,6 +2968,9 @@ type Event implements ActivityObject & Interactable & ActionLogObject {
"The event's visibility" "The event's visibility"
joinOptions: EventJoinOptions joinOptions: EventJoinOptions
"External URL for participation"
externalParticipationUrl: String
"The event's picture" "The event's picture"
picture: Media picture: Media
@ -3150,6 +3159,9 @@ enum EventJoinOptions {
"Participants must be invited" "Participants must be invited"
INVITE INVITE
"External registration"
EXTERNAL
} }
type InstanceFeeds { type InstanceFeeds {