From 5999252e02b4439c7e084d981a2a1fecc650a66e Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Fri, 1 Sep 2023 12:14:36 +0200 Subject: [PATCH 1/7] ci: fix Gitlab CI exunit run by separating mix compile and tz_world.update Reference https://github.com/kimlai/tz_world/issues/33 Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 55396be7e..27029a5a4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -132,7 +132,9 @@ exunit: variables: MIX_ENV: test before_script: - - mix deps.get && mix tz_world.update + - mix deps.get + - mix compile + - mix tz_world.update - mix ecto.create - mix ecto.migrate script: From 84f62cd043d5cf5d186fea6f24a1a9dff5fc64ce Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Fri, 1 Sep 2023 12:17:27 +0200 Subject: [PATCH 2/7] fix(front): fix behavior of local toggle for profiles & groups view depending on domain value Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- js/src/views/Admin/GroupProfiles.vue | 8 ++++---- js/src/views/Admin/ProfilesView.vue | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/src/views/Admin/GroupProfiles.vue b/js/src/views/Admin/GroupProfiles.vue index dbd904eea..7b12c9cb1 100644 --- a/js/src/views/Admin/GroupProfiles.vue +++ b/js/src/views/Admin/GroupProfiles.vue @@ -129,11 +129,11 @@ const PROFILES_PER_PAGE = 10; const { restrictions } = useRestrictions(); -const preferredUsername = ref(""); -const name = ref(""); -const domain = ref(""); +const preferredUsername = useRouteQuery("preferredUsername", ""); +const name = useRouteQuery("name", ""); +const domain = useRouteQuery("domain", ""); -const local = useRouteQuery("local", true, booleanTransformer); +const local = useRouteQuery("local", domain.value === "", booleanTransformer); const suspended = useRouteQuery("suspended", false, booleanTransformer); const page = useRouteQuery("page", 1, integerTransformer); diff --git a/js/src/views/Admin/ProfilesView.vue b/js/src/views/Admin/ProfilesView.vue index fa754642c..27523f265 100644 --- a/js/src/views/Admin/ProfilesView.vue +++ b/js/src/views/Admin/ProfilesView.vue @@ -119,7 +119,7 @@ const preferredUsername = useRouteQuery("preferredUsername", ""); const name = useRouteQuery("name", ""); const domain = useRouteQuery("domain", ""); -const local = useRouteQuery("local", true, booleanTransformer); +const local = useRouteQuery("local", domain.value === "", booleanTransformer); const suspended = useRouteQuery("suspended", false, booleanTransformer); const page = useRouteQuery("page", 1, integerTransformer); From 4ce79f5136f42fbbc2a046bd4e604cb7e011e5f5 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Fri, 1 Sep 2023 14:08:04 +0200 Subject: [PATCH 3/7] chore(release): release 3.2.0-beta.2 Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- CHANGELOG.md | 10 ++++++++++ js/package.json | 2 +- mix.exs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8058e9310..94180ec57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.2.0-beta.2 (2023-09-01) + +Fixes a CI issue that prevented 3.2.0-beta.2 being released. + +### Bug Fixes + +* **front:** fix behavior of local toggle for profiles & groups view depending on domain value ([84f62cd](https://framagit.org/framasoft/mobilizon/commit/84f62cd043d5cf5d186fea6f24a1a9dff5fc64ce)) + + + ## 3.2.0-beta.1 (2023-09-01) ### Features diff --git a/js/package.json b/js/package.json index 7a082bf77..1603791fc 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "mobilizon", - "version": "3.2.0-beta.1", + "version": "3.2.0-beta.2", "private": true, "scripts": { "dev": "vite", diff --git a/mix.exs b/mix.exs index 14623353d..a2ee896da 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Mobilizon.Mixfile do use Mix.Project - @version "3.2.0-beta.1" + @version "3.2.0-beta.2" def project do [ From 3f60174877bbe05773b1d1b2ceb91749adec7ed7 Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Fri, 1 Sep 2023 14:06:44 +0000 Subject: [PATCH 4/7] improve group creation view --- js/src/graphql/group.ts | 8 ++ js/src/types/actor/group.model.ts | 7 +- js/src/views/Admin/GroupProfiles.vue | 2 +- js/src/views/Group/CreateView.vue | 145 +++++++++++++++++++++++++-- lib/graphql/schema/actors/group.ex | 4 + lib/mobilizon/actors/actor.ex | 3 +- 6 files changed, 156 insertions(+), 13 deletions(-) diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index 0b1d60874..b53ac350a 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -295,6 +295,10 @@ export const CREATE_GROUP = gql` $summary: String $avatar: MediaInput $banner: MediaInput + $physicalAddress: AddressInput + $visibility: GroupVisibility + $openness: Openness + $manuallyApprovesFollowers: Boolean ) { createGroup( preferredUsername: $preferredUsername @@ -302,6 +306,10 @@ export const CREATE_GROUP = gql` summary: $summary banner: $banner avatar: $avatar + physicalAddress: $physicalAddress + visibility: $visibility + openness: $openness + manuallyApprovesFollowers: $manuallyApprovesFollowers ) { ...ActorFragment banner { diff --git a/js/src/types/actor/group.model.ts b/js/src/types/actor/group.model.ts index 26e20449a..e96dc485c 100644 --- a/js/src/types/actor/group.model.ts +++ b/js/src/types/actor/group.model.ts @@ -5,8 +5,7 @@ import type { IResource } from "../resource"; import type { IEvent } from "../event.model"; import type { IDiscussion } from "../discussions"; import type { IPost } from "../post.model"; -import type { IAddress } from "../address.model"; -import { Address } from "../address.model"; +import { Address, type IAddress } from "../address.model"; import { ActorType, GroupVisibility, Openness } from "../enums"; import type { IMember } from "./member.model"; import type { ITodoList } from "../todolist"; @@ -53,11 +52,11 @@ export class Group extends Actor implements IGroup { visibility: GroupVisibility = GroupVisibility.PUBLIC; activity: Paginate<IActivity> = { elements: [], total: 0 }; - openness: Openness = Openness.INVITE_ONLY; + openness: Openness = Openness.MODERATED; physicalAddress: IAddress = new Address(); - manuallyApprovesFollowers = true; + manuallyApprovesFollowers = false; patch(hash: IGroup | Record<string, unknown>): void { Object.assign(this, hash); diff --git a/js/src/views/Admin/GroupProfiles.vue b/js/src/views/Admin/GroupProfiles.vue index 7b12c9cb1..21ab50f32 100644 --- a/js/src/views/Admin/GroupProfiles.vue +++ b/js/src/views/Admin/GroupProfiles.vue @@ -120,7 +120,7 @@ import { } from "vue-use-route-query"; import { useI18n } from "vue-i18n"; import { useHead } from "@vueuse/head"; -import { computed, ref } from "vue"; +import { computed } from "vue"; import { Paginate } from "@/types/paginate"; import { IGroup } from "@/types/actor"; import AccountGroup from "vue-material-design-icons/AccountGroup.vue"; diff --git a/js/src/views/Group/CreateView.vue b/js/src/views/Group/CreateView.vue index 6d231376b..d3d9aaed8 100644 --- a/js/src/views/Group/CreateView.vue +++ b/js/src/views/Group/CreateView.vue @@ -69,11 +69,25 @@ :message="summaryErrors[0]" :type="summaryErrors[1]" > - <o-input v-model="group.summary" type="textarea" id="group-summary" /> + <editor + v-if="currentActor" + id="group-summary" + mode="basic" + class="mb-3" + v-model="group.summary" + :maxSize="500" + :aria-label="$t('Group description body')" + :current-actor="currentActor" + /> </o-field> - <div> - <b>{{ t("Avatar") }}</b> + <full-address-auto-complete + :label="$t('Group address')" + v-model="group.physicalAddress" + /> + + <div class="field"> + <b class="field-label">{{ t("Avatar") }}</b> <picture-upload :textFallback="t('Avatar')" v-model="avatarFile" @@ -81,8 +95,8 @@ /> </div> - <div> - <b>{{ t("Banner") }}</b> + <div class="field"> + <b class="field-label">{{ t("Banner") }}</b> <picture-upload :textFallback="t('Banner')" v-model="bannerFile" @@ -90,7 +104,101 @@ /> </div> - <o-button variant="primary" native-type="submit"> + <fieldset> + <legend class="field-label !mb-0 mt-2"> + {{ t("Group visibility") }} + </legend> + <o-radio + v-model="group.visibility" + name="groupVisibility" + :native-value="GroupVisibility.PUBLIC" + > + {{ $t("Visible everywhere on the web") }}<br /> + <small>{{ + $t( + "The group will be publicly listed in search results and may be suggested in the explore section. Only public informations will be shown on it's page." + ) + }}</small> + </o-radio> + <o-radio + v-model="group.visibility" + name="groupVisibility" + :native-value="GroupVisibility.UNLISTED" + >{{ $t("Only accessible through link") }}<br /> + <small>{{ + $t( + "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines." + ) + }}</small> + </o-radio> + </fieldset> + <fieldset> + <legend class="mt-2"> + <span class="field-label !mb-0">{{ t("New members") }} </span> + <span> + {{ + t( + "Members will also access private sections like discussions, resources and restricted posts." + ) + }} + </span> + </legend> + <o-field> + <o-radio + v-model="group.openness" + name="groupOpenness" + :native-value="Openness.OPEN" + > + {{ $t("Anyone can join freely") }}<br /> + <small>{{ + $t( + "Anyone wanting to be a member from your group will be able to from your group page." + ) + }}</small> + </o-radio> + </o-field> + <o-field> + <o-radio + v-model="group.openness" + name="groupOpenness" + :native-value="Openness.MODERATED" + >{{ $t("Moderate new members") }}<br /> + <small>{{ + $t( + "Anyone can request being a member, but an administrator needs to approve the membership." + ) + }}</small> + </o-radio> + </o-field> + <o-field> + <o-radio + v-model="group.openness" + name="groupOpenness" + :native-value="Openness.INVITE_ONLY" + >{{ $t("Manually invite new members") }}<br /> + <small>{{ + $t( + "The only way for your group to get new members is if an admininistrator invites them." + ) + }}</small> + </o-radio> + </o-field> + </fieldset> + <fieldset> + <legend class="mt-2"> + <span class="field-label !mb-0"> + {{ t("Followers") }} + </span> + <span> + {{ t("Followers will receive new public events and posts.") }} + </span> + </legend> + <o-checkbox v-model="group.manuallyApprovesFollowers"> + {{ t("Manually approve new followers") }} + </o-checkbox> + </fieldset> + + <o-button variant="primary" native-type="submit" class="mt-3"> {{ t("Create my group") }} </o-button> </form> @@ -105,7 +213,14 @@ import PictureUpload from "../../components/PictureUpload.vue"; import { ErrorResponse } from "@/types/errors.model"; import { ServerParseError } from "@apollo/client/link/http"; import { useCurrentActorClient } from "@/composition/apollo/actor"; -import { computed, inject, reactive, ref, watch } from "vue"; +import { + computed, + defineAsyncComponent, + inject, + reactive, + ref, + watch, +} from "vue"; import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { useCreateGroup } from "@/composition/apollo/group"; @@ -116,6 +231,12 @@ import { } from "@/composition/config"; import { Notifier } from "@/plugins/notifier"; import { useHead } from "@vueuse/head"; +import { Openness, GroupVisibility } from "@/types/enums"; +import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue"; + +const Editor = defineAsyncComponent( + () => import("@/components/TextEditor.vue") +); const { currentActor } = useCurrentActorClient(); @@ -156,10 +277,20 @@ const buildVariables = computed(() => { let avatarObj = {}; let bannerObj = {}; + const cloneGroup = group.value; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete cloneGroup.physicalAddress.__typename; + delete cloneGroup.physicalAddress.pictureInfo; + const groupBasic = { preferredUsername: group.value.preferredUsername, name: group.value.name, summary: group.value.summary, + physicalAddress: cloneGroup.physicalAddress, + visibility: group.value.visibility, + openness: group.value.openness, + manuallyApprovesFollowers: group.value.manuallyApprovesFollowers, }; if (avatarFile.value) { diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index 3b0122a11..eed6b889b 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -305,6 +305,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do description: "Whether the group can be join freely, with approval or is invite-only." ) + arg(:manually_approves_followers, :boolean, + description: "Whether this group approves new followers manually" + ) + arg(:avatar, :media_input, description: "The avatar for the group, either as an object or directly the ID of an existing media" diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 36ec653b8..9db86aab7 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -146,7 +146,8 @@ defmodule Mobilizon.Actors.Actor do :domain, :summary, :visibility, - :openness + :openness, + :manually_approves_followers ] @group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs From 2de6937407743100daba1d397db4da32d4cb606b Mon Sep 17 00:00:00 2001 From: Luca Eichler <eic.luca@gmail.com> Date: Tue, 19 Oct 2021 15:56:18 +0200 Subject: [PATCH 5/7] feat: Add option to link an external registration provider for events Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- .../components/Event/EventActionSection.vue | 14 ++++++- .../Event/ExternalParticipationButton.vue | 30 ++++++++++++++ js/src/graphql/event.ts | 6 +++ js/src/i18n/en_US.json | 6 ++- js/src/i18n/fr_FR.json | 6 ++- js/src/types/enums.ts | 1 + js/src/types/event.model.ts | 6 +++ js/src/views/Event/EditView.vue | 41 +++++++++++++++++-- .../activity_stream/converter/event.ex | 2 + lib/graphql/schema/event.ex | 6 +++ lib/mobilizon/events/event.ex | 3 ++ lib/mobilizon/events/events.ex | 3 +- ...0901160000_add_external_url_for_events.exs | 33 +++++++++++++++ schema.graphql | 12 ++++++ 14 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 js/src/components/Event/ExternalParticipationButton.vue create mode 100644 priv/repo/migrations/20230901160000_add_external_url_for_events.exs diff --git a/js/src/components/Event/EventActionSection.vue b/js/src/components/Event/EventActionSection.vue index 7a5dff456..396efc615 100644 --- a/js/src/components/Event/EventActionSection.vue +++ b/js/src/components/Event/EventActionSection.vue @@ -1,7 +1,13 @@ <template> <div class=""> + <external-participation-button + v-if="event && event.joinOptions === EventJoinOptions.EXTERNAL" + :event="event" + :current-actor="currentActor" + /> + <participation-section - v-if="event && anonymousParticipationConfig" + v-else-if="event && anonymousParticipationConfig" :participation="participations[0]" :event="event" :anonymousParticipation="anonymousParticipation" @@ -15,7 +21,10 @@ @cancel-anonymous-participation="cancelAnonymousParticipation" /> <div class="flex flex-col gap-1 mt-1"> - <p class="inline-flex gap-2 ml-auto"> + <p + class="inline-flex gap-2 ml-auto" + v-if="event.joinOptions !== EventJoinOptions.EXTERNAL" + > <TicketConfirmationOutline /> <router-link class="participations-link" @@ -349,6 +358,7 @@ import { useMutation } from "@vue/apollo-composable"; import { useCreateReport } from "@/composition/apollo/report"; import { useDeleteEvent } from "@/composition/apollo/event"; import { useProgrammatic } from "@oruga-ui/oruga-next"; +import ExternalParticipationButton from "./ExternalParticipationButton.vue"; const ShareEventModal = defineAsyncComponent( () => import("@/components/Event/ShareEventModal.vue") diff --git a/js/src/components/Event/ExternalParticipationButton.vue b/js/src/components/Event/ExternalParticipationButton.vue new file mode 100644 index 000000000..72122d714 --- /dev/null +++ b/js/src/components/Event/ExternalParticipationButton.vue @@ -0,0 +1,30 @@ +<template> + <o-button + tag="a" + :href=" + event.externalParticipationUrl + ? encodeURI(`${event.externalParticipationUrl}?uuid=${event.uuid}`) + : '#' + " + rel="noopener ugc" + target="_blank" + :disabled="!event.externalParticipationUrl" + icon-right="OpenInNew" + > + {{ t("Go to booking") }} + </o-button> +</template> + +<script lang="ts" setup> +import { computed } from "vue"; +import { IEvent } from "../../types/event.model"; +import { useI18n } from "vue-i18n"; + +const { t } = useI18n({ useScope: "global" }); + +const props = defineProps<{ + event: IEvent; +}>(); + +const event = computed(() => props.event); +</script> diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 3e9aaae69..21f6a482c 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -21,6 +21,7 @@ const FULL_EVENT_FRAGMENT = gql` status visibility joinOptions + externalParticipationUrl draft language category @@ -121,6 +122,7 @@ export const FETCH_EVENT_BASIC = gql` id uuid joinOptions + externalParticipationUrl participantStats { going notApproved @@ -199,6 +201,7 @@ export const CREATE_EVENT = gql` $status: EventStatus $visibility: EventVisibility $joinOptions: EventJoinOptions + $externalParticipationUrl: String $draft: Boolean $tags: [String] $picture: MediaInput @@ -220,6 +223,7 @@ export const CREATE_EVENT = gql` status: $status visibility: $visibility joinOptions: $joinOptions + externalParticipationUrl: $externalParticipationUrl draft: $draft tags: $tags picture: $picture @@ -247,6 +251,7 @@ export const EDIT_EVENT = gql` $status: EventStatus $visibility: EventVisibility $joinOptions: EventJoinOptions + $externalParticipationUrl: String $draft: Boolean $tags: [String] $picture: MediaInput @@ -269,6 +274,7 @@ export const EDIT_EVENT = gql` status: $status visibility: $visibility joinOptions: $joinOptions + externalParticipationUrl: $externalParticipationUrl draft: $draft tags: $tags picture: $picture diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index fa50e35d2..304cb02b5 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1605,5 +1605,9 @@ "Reported by an unknown actor": "Reported by an unknown actor", "Reported at": "Reported at", "Updated at": "Updated at", - "Suspend the profile?": "Suspend the profile?" + "Suspend the profile?": "Suspend the profile?", + "Go to booking": "Go to booking", + "External registration": "External registration", + "I want to manage the registration with an external provider": "I want to manage the registration with an external provider", + "External provider URL": "External provider URL" } \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index bb7233e0d..7c4888a05 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1601,5 +1601,9 @@ "{username} was invited to {group}": "{username} a été invité à {group}", "{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée", "{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée", - "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" + "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", + "Go to booking": "Aller à la réservation", + "External registration": "Inscription externe", + "I want to manage the registration with an external provider": "Je souhaite gérer l'enregistrement auprès d'un fournisseur externe", + "External provider URL": "URL du fournisseur externe" } diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts index dc551f3f9..ed7e3ee0f 100644 --- a/js/src/types/enums.ts +++ b/js/src/types/enums.ts @@ -64,6 +64,7 @@ export enum EventJoinOptions { FREE = "FREE", RESTRICTED = "RESTRICTED", INVITE = "INVITE", + EXTERNAL = "EXTERNAL", } export enum EventVisibilityJoinOptions { diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 11216edc6..f9b520a85 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -43,6 +43,7 @@ interface IEventEditJSON { status: EventStatus; visibility: EventVisibility; joinOptions: EventJoinOptions; + externalParticipationUrl: string | null; draft: boolean; picture?: IMedia | { mediaId: string } | null; attributedToId: string | null; @@ -72,6 +73,7 @@ export interface IEvent { status: EventStatus; visibility: EventVisibility; joinOptions: EventJoinOptions; + externalParticipationUrl: string | null; draft: boolean; picture: IMedia | null; @@ -132,6 +134,8 @@ export class EventModel implements IEvent { joinOptions = EventJoinOptions.FREE; + externalParticipationUrl: string | null = null; + status = EventStatus.CONFIRMED; draft = true; @@ -197,6 +201,7 @@ export class EventModel implements IEvent { this.status = hash.status; this.visibility = hash.visibility; this.joinOptions = hash.joinOptions; + this.externalParticipationUrl = hash.externalParticipationUrl; this.draft = hash.draft; this.picture = hash.picture; @@ -248,6 +253,7 @@ export function toEditJSON(event: IEditableEvent): IEventEditJSON { category: event.category, visibility: event.visibility, joinOptions: event.joinOptions, + externalParticipationUrl: event.externalParticipationUrl, draft: event.draft, tags: event.tags.map((t) => t.title), onlineAddress: event.onlineAddress, diff --git a/js/src/views/Event/EditView.vue b/js/src/views/Event/EditView.vue index 608f8883b..44cd754b5 100644 --- a/js/src/views/Event/EditView.vue +++ b/js/src/views/Event/EditView.vue @@ -246,8 +246,25 @@ </o-radio> </div>--> + <o-field :label="t('External registration')"> + <o-switch v-model="externalParticipation"> + {{ + t("I want to manage the registration with an external provider") + }} + </o-switch> + </o-field> + + <o-field v-if="externalParticipation" :label="t('URL')"> + <o-input + icon="link" + type="url" + v-model="event.externalParticipationUrl" + :placeholder="t('External provider URL')" + /> + </o-field> + <o-field - v-if="anonymousParticipationConfig?.allowed" + v-if="anonymousParticipationConfig?.allowed && !externalParticipation" :label="t('Anonymous participations')" > <o-switch v-model="eventOptions.anonymousParticipation"> @@ -268,19 +285,22 @@ </o-switch> </o-field> - <o-field :label="t('Participation approval')"> + <o-field + :label="t('Participation approval')" + v-show="!externalParticipation" + > <o-switch v-model="needsApproval">{{ t("I want to approve every participation request") }}</o-switch> </o-field> - <o-field :label="t('Number of places')"> + <o-field :label="t('Number of places')" v-show="!externalParticipation"> <o-switch v-model="limitedPlaces">{{ t("Limited number of places") }}</o-switch> </o-field> - <div class="" v-if="limitedPlaces"> + <div class="" v-if="limitedPlaces && !externalParticipation"> <o-field :label="t('Number of places')" label-for="number-of-places"> <o-input type="number" @@ -1308,6 +1328,19 @@ const orderedCategories = computed(() => { if (!eventCategories.value) return undefined; return sortBy(eventCategories.value, ["label"]); }); + +const externalParticipation = computed({ + get() { + return event.value?.joinOptions === EventJoinOptions.EXTERNAL; + }, + set(newValue) { + if (newValue === true) { + event.value.joinOptions = EventJoinOptions.EXTERNAL; + } else { + event.value.joinOptions = EventJoinOptions.FREE; + } + }, +}); </script> <style lang="scss"> diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index e40aab157..d08c61db0 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -78,6 +78,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do visibility: visibility, join_options: Map.get(object, "joinMode", "free"), local: is_local?(object["id"]), + external_participation_url: object["externalParticipationUrl"], options: options, metadata: metadata, status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), @@ -129,6 +130,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do "mediaType" => "text/html", "startTime" => event.begins_on |> shift_tz(event.options.timezone) |> date_to_string(), "joinMode" => to_string(event.join_options), + "externalParticipationUrl" => event.external_participation_url, "endTime" => event.ends_on |> shift_tz(event.options.timezone) |> date_to_string(), "tag" => event.tags |> build_tags(), "maximumAttendeeCapacity" => event.options.maximum_attendee_capacity, diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index 42b435334..7ace49bb1 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -35,6 +35,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do field(:status, :event_status, description: "Status of the event") field(:visibility, :event_visibility, 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, description: "The event's picture", @@ -130,6 +131,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do value(:free, description: "Anyone can join and is automatically accepted") value(:restricted, description: "Manual acceptation") value(:invite, description: "Participants must be invited") + value(:external, description: "External registration") end @desc "The list of possible options for the event's status" @@ -398,6 +400,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do description: "The event's options to join" ) + arg(:external_participation_url, :string, description: "External URL for participation") + arg(:tags, list_of(:string), default_value: [], description: "The list of tags associated to the event" @@ -469,6 +473,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do 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(:picture, :media_input, diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 541c3d4b9..3d2f8ad7e 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -47,6 +47,7 @@ defmodule Mobilizon.Events.Event do draft: boolean, visibility: atom(), join_options: atom(), + external_participation_url: String.t(), publish_at: DateTime.t() | nil, uuid: Ecto.UUID.t(), online_address: String.t() | nil, @@ -81,6 +82,7 @@ defmodule Mobilizon.Events.Event do :local, :visibility, :join_options, + :external_participation_url, :publish_at, :online_address, :phone_address, @@ -105,6 +107,7 @@ defmodule Mobilizon.Events.Event do field(:draft, :boolean, default: false) field(:visibility, EventVisibility, default: :public) field(:join_options, JoinOptions, default: :free) + field(:external_participation_url, :string) field(:publish_at, :utc_datetime) field(:uuid, Ecto.UUID, default: Ecto.UUID.generate()) field(:online_address, :string) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index ebcdfa533..7653fda97 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -46,7 +46,8 @@ defmodule Mobilizon.Events do defenum(JoinOptions, :join_options, [ :free, :restricted, - :invite + :invite, + :external ]) defenum(EventStatus, :event_status, [ diff --git a/priv/repo/migrations/20230901160000_add_external_url_for_events.exs b/priv/repo/migrations/20230901160000_add_external_url_for_events.exs new file mode 100644 index 000000000..fe0f3e73b --- /dev/null +++ b/priv/repo/migrations/20230901160000_add_external_url_for_events.exs @@ -0,0 +1,33 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddExternalUrlForEvents do + use Ecto.Migration + alias Mobilizon.Events.JoinOptions + + def up do + alter table(:events) do + add(:external_participation_url, :string) + end + + reset_join_options_enum() + end + + def down do + alter table(:events) do + remove(:external_participation_url, :string) + end + + reset_join_options_enum() + end + + defp reset_join_options_enum do + 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 diff --git a/schema.graphql b/schema.graphql index 925df234f..fd4111819 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1551,6 +1551,9 @@ type RootMutationType { "The event's options to join" joinOptions: EventJoinOptions + "External URL for participation" + externalParticipationUrl: String + "The list of tags associated to the event" tags: [String] @@ -1620,6 +1623,9 @@ type RootMutationType { "The event's options to join" joinOptions: EventJoinOptions + "External URL for participation" + externalParticipationUrl: String + "The list of tags associated to the event" tags: [String] @@ -2956,6 +2962,9 @@ type Event implements ActivityObject & Interactable & ActionLogObject { "The event's visibility" joinOptions: EventJoinOptions + "External URL for participation" + externalParticipationUrl: String + "The event's picture" picture: Media @@ -3144,6 +3153,9 @@ enum EventJoinOptions { "Participants must be invited" INVITE + + "External registration" + EXTERNAL } type InstanceFeeds { From af670f39478b11465205fbea9b9268bab401bbb6 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Fri, 1 Sep 2023 17:38:11 +0200 Subject: [PATCH 6/7] fix(i18n): add missing translations Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- js/src/i18n/en_US.json | 3 ++- js/src/i18n/fr_FR.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 304cb02b5..312160de2 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1609,5 +1609,6 @@ "Go to booking": "Go to booking", "External registration": "External registration", "I want to manage the registration with an external provider": "I want to manage the registration with an external provider", - "External provider URL": "External provider URL" + "External provider URL": "External provider URL", + "Members will also access private sections like discussions, resources and restricted posts.": "Members will also access private sections like discussions, resources and restricted posts." } \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 7c4888a05..fb16e932d 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1605,5 +1605,6 @@ "Go to booking": "Aller à la réservation", "External registration": "Inscription externe", "I want to manage the registration with an external provider": "Je souhaite gérer l'enregistrement auprès d'un fournisseur externe", - "External provider URL": "URL du fournisseur externe" + "External provider URL": "URL du fournisseur externe", + "Members will also access private sections like discussions, resources and restricted posts.": "Les membres auront également accès aux section privées comme les discussions, les ressources et les billets restreints." } From f6611e8eb5a7e12dc0dc0c216b598e04144e07c6 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Fri, 1 Sep 2023 18:16:06 +0200 Subject: [PATCH 7/7] feat(back): add admin setting to disable external event feature Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- config/config.exs | 5 ++++- js/src/graphql/config.ts | 2 ++ js/src/types/config.model.ts | 1 + js/src/views/Event/EditView.vue | 6 +++++- lib/graphql/resolvers/config.ex | 1 + lib/graphql/resolvers/event.ex | 27 +++++++++++++++++++++++++++ lib/graphql/schema/config.ex | 4 ++++ lib/mobilizon/config.ex | 4 ++++ 8 files changed, 48 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index d2a0f9214..8cb7c203a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -41,7 +41,10 @@ config :mobilizon, :instance, email_reply_to: "noreply@localhost" config :mobilizon, :groups, enabled: true -config :mobilizon, :events, creation: true + +config :mobilizon, :events, + creation: true, + external: true config :mobilizon, :restrictions, only_admin_can_create_groups: false config :mobilizon, :restrictions, only_groups_can_create_events: false diff --git a/js/src/graphql/config.ts b/js/src/graphql/config.ts index 1022b7845..b7e4c7ba3 100644 --- a/js/src/graphql/config.ts +++ b/js/src/graphql/config.ts @@ -72,6 +72,7 @@ export const CONFIG = gql` features { groups eventCreation + eventExternal antispam } restrictions { @@ -370,6 +371,7 @@ export const FEATURES = gql` features { groups eventCreation + eventExternal antispam } } diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts index 8548fb13d..e34c39c0a 100644 --- a/js/src/types/config.model.ts +++ b/js/src/types/config.model.ts @@ -96,6 +96,7 @@ export interface IConfig { timezones: string[]; features: { eventCreation: boolean; + eventExternal: boolean; groups: boolean; antispam: boolean; }; diff --git a/js/src/views/Event/EditView.vue b/js/src/views/Event/EditView.vue index 44cd754b5..a83fb61b3 100644 --- a/js/src/views/Event/EditView.vue +++ b/js/src/views/Event/EditView.vue @@ -246,7 +246,10 @@ </o-radio> </div>--> - <o-field :label="t('External registration')"> + <o-field + :label="t('External registration')" + v-if="features?.eventExternal" + > <o-switch v-model="externalParticipation"> {{ t("I want to manage the registration with an external provider") @@ -260,6 +263,7 @@ type="url" v-model="event.externalParticipationUrl" :placeholder="t('External provider URL')" + required /> </o-field> diff --git a/lib/graphql/resolvers/config.ex b/lib/graphql/resolvers/config.ex index 1241872c3..b66afd1a5 100644 --- a/lib/graphql/resolvers/config.ex +++ b/lib/graphql/resolvers/config.ex @@ -145,6 +145,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do features: %{ groups: Config.instance_group_feature_enabled?(), event_creation: Config.instance_event_creation_enabled?(), + event_external: Config.instance_event_external_enabled?(), antispam: AntiSpam.service().ready?() }, restrictions: %{ diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index aaf18ef8c..16a86aa49 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -254,6 +254,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id), {:can_create_event, true} <- can_create_event(args), + {:event_external, true} <- edit_event_external_checker(args), {:organizer_group_member, true} <- {:organizer_group_member, is_organizer_group_member?(args)}, args_with_organizer <- @@ -281,6 +282,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do "Only groups can create events" )} + {:event_external, false} -> + {:error, + dgettext( + "errors", + "Providing external registration is not allowed" + )} + {:organizer_group_member, false} -> {:error, dgettext( @@ -322,6 +330,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do end end + @spec edit_event_external_checker(map()) :: {:event_external, boolean()} + defp edit_event_external_checker(args) do + if Config.instance_event_external_enabled?() do + {:event_external, true} + else + {:event_external, + Map.get(args, :join_options) != :external and + is_nil(Map.get(args, :external_participation_url))} + end + end + @doc """ Update an event """ @@ -340,6 +359,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do args <- extract_timezone(args, user.id), {:event_can_be_managed, true} <- {:event_can_be_managed, can_event_be_updated_by?(event, actor)}, + {:event_external, true} <- edit_event_external_checker(args), {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <- API.Events.update_event(args, event) do {:ok, event} @@ -351,6 +371,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do "This profile doesn't have permission to update an event on behalf of this group" )} + {:event_external, false} -> + {:error, + dgettext( + "errors", + "Providing external registration is not allowed" + )} + {:error, :event_not_found} -> {:error, dgettext("errors", "Event not found")} diff --git a/lib/graphql/schema/config.ex b/lib/graphql/schema/config.ex index cc42f1b1a..59579a6e3 100644 --- a/lib/graphql/schema/config.ex +++ b/lib/graphql/schema/config.ex @@ -314,6 +314,10 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do description: "Whether event creation is allowed on this instance" ) + field(:event_external, :boolean, + description: "Whether redirecting to external providers is authorized in event edition" + ) + field(:antispam, :boolean, description: "Whether anti-spam is activated on this instance") end diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index e27d7c9a2..92a29544e 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -357,6 +357,10 @@ defmodule Mobilizon.Config do def instance_event_creation_enabled?, do: :mobilizon |> Application.get_env(:events) |> Keyword.get(:creation) + @spec instance_event_external_enabled? :: boolean + def instance_event_external_enabled?, + do: :mobilizon |> Application.get_env(:events) |> Keyword.get(:external) + @spec instance_export_formats :: %{event_participants: list(String.t())} def instance_export_formats do %{