feat(events): 1394 Long events, recurring events --> activities

This commit is contained in:
setop 2024-03-06 10:18:40 +01:00
commit 72d78ed8f4
22 changed files with 593 additions and 41 deletions

View File

@ -36,6 +36,7 @@ config :mobilizon, :instance,
unconfirmed_user_grace_period_hours: 48, unconfirmed_user_grace_period_hours: 48,
activity_expire_days: 365, activity_expire_days: 365,
activity_keep_number: 100, activity_keep_number: 100,
duration_of_long_event: 0,
enable_instance_feeds: true, enable_instance_feeds: true,
email_from: "noreply@localhost", email_from: "noreply@localhost",
email_reply_to: "noreply@localhost" email_reply_to: "noreply@localhost"

View File

@ -2,7 +2,8 @@ import Config
config :mobilizon, :instance, config :mobilizon, :instance,
name: "Test instance", name: "Test instance",
registrations_open: true registrations_open: true,
duration_of_long_event: 0
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.

View File

@ -94,6 +94,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
registrations_allowlist: Config.instance_registrations_allowlist?(), registrations_allowlist: Config.instance_registrations_allowlist?(),
contact: Config.contact(), contact: Config.contact(),
demo_mode: Config.instance_demo_mode?(), demo_mode: Config.instance_demo_mode?(),
long_events: Config.instance_long_events?(),
description: Config.instance_description(), description: Config.instance_description(),
long_description: Config.instance_long_description(), long_description: Config.instance_long_description(),
slogan: Config.instance_slogan(), slogan: Config.instance_slogan(),

View File

@ -31,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
) )
field(:demo_mode, :boolean, description: "Whether the demo mode is enabled") field(:demo_mode, :boolean, description: "Whether the demo mode is enabled")
field(:long_events, :boolean, description: "Whether the long events mode is enabled")
field(:country_code, :string, description: "The country code from the IP") field(:country_code, :string, description: "The country code from the IP")
field(:location, :lonlat, description: "The IP's location") field(:location, :lonlat, description: "The IP's location")
field(:geocoding, :geocoding, description: "The instance's geocoding settings") field(:geocoding, :geocoding, description: "The instance's geocoding settings")

View File

@ -273,6 +273,8 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "Radius around the location to search in" description: "Radius around the location to search in"
) )
arg(:longevents, :boolean, description: "if mention filter in or out long events")
arg(:bbox, :string, description: "The bbox to search events into") arg(:bbox, :string, description: "The bbox to search events into")
arg(:zoom, :integer, description: "The zoom level for searching events") arg(:zoom, :integer, description: "The zoom level for searching events")

View File

@ -202,6 +202,9 @@ defmodule Mobilizon.Config do
@spec instance_demo_mode? :: boolean @spec instance_demo_mode? :: boolean
def instance_demo_mode?, do: to_boolean(instance_config()[:demo]) def instance_demo_mode?, do: to_boolean(instance_config()[:demo])
@spec instance_long_events? :: boolean
def instance_long_events?, do: instance_config()[:duration_of_long_event] > 0
@spec instance_repository :: String.t() @spec instance_repository :: String.t()
def instance_repository, do: instance_config()[:repository] def instance_repository, do: instance_config()[:repository]

View File

@ -16,6 +16,7 @@ defmodule Mobilizon.Events do
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Config
alias Mobilizon.Events.{ alias Mobilizon.Events.{
Event, Event,
@ -571,6 +572,7 @@ defmodule Mobilizon.Events do
|> events_for_search_query() |> events_for_search_query()
|> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now())) |> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now()))
|> events_for_ends_on(Map.get(args, :ends_on)) |> events_for_ends_on(Map.get(args, :ends_on))
|> events_for_longevents(args)
|> events_for_category(args) |> events_for_category(args)
|> events_for_categories(args) |> events_for_categories(args)
|> events_for_languages(args) |> events_for_languages(args)
@ -1377,6 +1379,38 @@ defmodule Mobilizon.Events do
end end
end end
@spec events_for_longevents(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
defp events_for_longevents(query, args) do
duration = Config.get([:instance, :duration_of_long_event], 0)
if duration <= 0 do
query
else
longevents = Map.get(args, :longevents)
case longevents do
nil ->
query
true ->
where(
query,
[q],
not is_nil(q.ends_on) and
q.ends_on > fragment("? + '1 days'::interval * ?", q.begins_on, ^duration)
)
false ->
where(
query,
[q],
is_nil(q.ends_on) or
q.ends_on <= fragment("? + '1 days'::interval * ?", q.begins_on, ^duration)
)
end
end
end
@spec events_for_category(Ecto.Queryable.t(), map()) :: Ecto.Query.t() @spec events_for_category(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
defp events_for_category(query, %{category: category}) when is_valid_string(category) do defp events_for_category(query, %{category: category}) when is_valid_string(category) do
where(query, [q], q.category == ^category) where(query, [q], q.category == ^category)

View File

@ -187,6 +187,9 @@ type Config {
"Whether the demo mode is enabled" "Whether the demo mode is enabled"
demoMode: Boolean demoMode: Boolean
"Whether the long events mode is enabled"
longEvents: Boolean
"The country code from the IP" "The country code from the IP"
countryCode: String countryCode: String
@ -2242,6 +2245,9 @@ type RootQueryType {
"Filter events by their end date" "Filter events by their end date"
endsOn: DateTime endsOn: DateTime
"Filter for long events in function of configuration parameter 'duration_of_long_event'"
longevents: Boolean
): Events ): Events
"Interact with an URI" "Interact with an URI"

View File

@ -12,6 +12,31 @@
class="rounded-lg" class="rounded-lg"
:class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }" :class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }"
> >
<div
class="-mt-3 h-0 mb-3 ltr:ml-0 rtl:mr-0 block relative z-10"
:class="{
'sm:hidden': mode === 'row',
'calendar-simple': !isDifferentBeginsEndsDate,
'calendar-double': isDifferentBeginsEndsDate,
}"
>
<date-calendar-icon
:small="true"
v-if="!mergedOptions.hideDate"
:date="event.beginsOn.toString()"
/>
<MenuDown
:small="true"
class="left-3 relative"
v-if="!mergedOptions.hideDate && isDifferentBeginsEndsDate"
/>
<date-calendar-icon
:small="true"
v-if="!mergedOptions.hideDate && isDifferentBeginsEndsDate"
:date="event.endsOn?.toString()"
/>
</div>
<figure class="block relative pt-40"> <figure class="block relative pt-40">
<lazy-image-wrapper <lazy-image-wrapper
:picture="event.picture" :picture="event.picture"
@ -48,21 +73,20 @@
</div> </div>
<div class="p-2 flex-auto" :class="{ 'sm:flex-1': mode === 'row' }"> <div class="p-2 flex-auto" :class="{ 'sm:flex-1': mode === 'row' }">
<div class="relative flex flex-col h-full"> <div class="relative flex flex-col h-full">
<div
class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start"
:class="{ 'sm:hidden': mode === 'row' }"
>
<date-calendar-icon
:small="true"
v-if="!mergedOptions.hideDate"
:date="event.beginsOn.toString()"
/>
</div>
<span <span
class="text-gray-700 dark:text-white font-semibold hidden" class="text-gray-700 dark:text-white font-semibold hidden"
:class="{ 'sm:block': mode === 'row' }" :class="{ 'sm:block': mode === 'row' }"
v-if="!isDifferentBeginsEndsDate"
>{{ formatDateTimeWithCurrentLocale }}</span >{{ formatDateTimeWithCurrentLocale }}</span
> >
<span
class="text-gray-700 dark:text-white font-semibold hidden"
:class="{ 'sm:block': mode === 'row' }"
v-if="isDifferentBeginsEndsDate"
>{{ formatBeginsOnDateWithCurrentLocale }}
<ArrowRightThin :small="true" style="display: ruby" />
{{ formatEndsOnDateWithCurrentLocale }}</span
>
<div class="w-full flex flex-col justify-between h-full"> <div class="w-full flex flex-col justify-between h-full">
<h2 <h2
class="mt-0 mb-2 text-2xl line-clamp-3 font-bold text-violet-3 dark:text-white" class="mt-0 mb-2 text-2xl line-clamp-3 font-bold text-violet-3 dark:text-white"
@ -152,6 +176,16 @@
</div> </div>
</LinkOrRouterLink> </LinkOrRouterLink>
</template> </template>
<style scoped>
.calendar-simple {
bottom: -117px;
left: 5px;
}
.calendar-double {
bottom: -45px;
left: 5px;
}
</style>
<script lang="ts" setup> <script lang="ts" setup>
import { import {
@ -161,6 +195,8 @@ import {
organizerAvatarUrl, organizerAvatarUrl,
} from "@/types/event.model"; } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import ArrowRightThin from "vue-material-design-icons/ArrowRightThin.vue";
import MenuDown from "vue-material-design-icons/MenuDown.vue";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue"; import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import { EventStatus } from "@/types/enums"; import { EventStatus } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@ -170,7 +206,7 @@ import { computed, inject } from "vue";
import MobilizonTag from "@/components/TagElement.vue"; import MobilizonTag from "@/components/TagElement.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Video from "vue-material-design-icons/Video.vue"; import Video from "vue-material-design-icons/Video.vue";
import { formatDateTimeForEvent } from "@/utils/datetime"; import { formatDateForEvent, formatDateTimeForEvent } from "@/utils/datetime";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue"; import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -212,6 +248,28 @@ const actorAvatarURL = computed<string | null>(() =>
const dateFnsLocale = inject<Locale>("dateFnsLocale"); const dateFnsLocale = inject<Locale>("dateFnsLocale");
const isDifferentBeginsEndsDate = computed(() => {
if (!dateFnsLocale) return;
const beginsOnStr = formatDateForEvent(
new Date(props.event.beginsOn),
dateFnsLocale
);
const endsOnStr = props.event.endsOn
? formatDateForEvent(new Date(props.event.endsOn), dateFnsLocale)
: null;
return endsOnStr && endsOnStr != beginsOnStr;
});
const formatBeginsOnDateWithCurrentLocale = computed(() => {
if (!dateFnsLocale) return;
return formatDateForEvent(new Date(props.event.beginsOn), dateFnsLocale);
});
const formatEndsOnDateWithCurrentLocale = computed(() => {
if (!dateFnsLocale) return;
return formatDateForEvent(new Date(props.event.endsOn), dateFnsLocale);
});
const formatDateTimeWithCurrentLocale = computed(() => { const formatDateTimeWithCurrentLocale = computed(() => {
if (!dateFnsLocale) return; if (!dateFnsLocale) return;
return formatDateTimeForEvent(new Date(props.event.beginsOn), dateFnsLocale); return formatDateTimeForEvent(new Date(props.event.beginsOn), dateFnsLocale);

View File

@ -173,6 +173,8 @@ const icons: Record<string, () => Promise<any>> = {
import( import(
`../../../node_modules/vue-material-design-icons/CalendarRemove.vue` `../../../node_modules/vue-material-design-icons/CalendarRemove.vue`
), ),
CalendarStar: () =>
import(`../../../node_modules/vue-material-design-icons/CalendarStar.vue`),
FileDocumentEdit: () => FileDocumentEdit: () =>
import( import(
`../../../node_modules/vue-material-design-icons/FileDocumentEdit.vue` `../../../node_modules/vue-material-design-icons/FileDocumentEdit.vue`

View File

@ -5,6 +5,7 @@ import {
ANONYMOUS_PARTICIPATION_CONFIG, ANONYMOUS_PARTICIPATION_CONFIG,
ANONYMOUS_REPORTS_CONFIG, ANONYMOUS_REPORTS_CONFIG,
DEMO_MODE, DEMO_MODE,
LONG_EVENTS,
EVENT_CATEGORIES, EVENT_CATEGORIES,
EVENT_PARTICIPANTS, EVENT_PARTICIPANTS,
FEATURES, FEATURES,
@ -188,6 +189,15 @@ export function useIsDemoMode() {
return { isDemoMode, error, loading }; return { isDemoMode, error, loading };
} }
export function useIsLongEvents() {
const { result, error, loading } = useQuery<{
config: Pick<IConfig, "longEvents">;
}>(LONG_EVENTS);
const islongEvents = computed(() => result.value?.config.longEvents);
return { islongEvents, error, loading };
}
export function useAnalytics() { export function useAnalytics() {
const { result, error, loading } = useQuery<{ const { result, error, loading } = useQuery<{
config: Pick<IConfig, "analytics">; config: Pick<IConfig, "analytics">;

View File

@ -10,6 +10,7 @@ export const CONFIG = gql`
registrationsOpen registrationsOpen
registrationsAllowlist registrationsAllowlist
demoMode demoMode
longEvents
countryCode countryCode
languages languages
eventCategories { eventCategories {
@ -425,6 +426,14 @@ export const DEMO_MODE = gql`
} }
`; `;
export const LONG_EVENTS = gql`
query LongEvents {
config {
longEvents
}
}
`;
export const ANALYTICS = gql` export const ANALYTICS = gql`
query Analytics { query Analytics {
config { config {

View File

@ -33,6 +33,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
$searchTarget: SearchTarget $searchTarget: SearchTarget
$beginsOn: DateTime $beginsOn: DateTime
$endsOn: DateTime $endsOn: DateTime
$longevents: Boolean
$bbox: String $bbox: String
$zoom: Int $zoom: Int
$eventPage: Int $eventPage: Int
@ -54,6 +55,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
searchTarget: $searchTarget searchTarget: $searchTarget
beginsOn: $beginsOn beginsOn: $beginsOn
endsOn: $endsOn endsOn: $endsOn
longevents: $longevents
bbox: $bbox bbox: $bbox
zoom: $zoom zoom: $zoom
page: $eventPage page: $eventPage
@ -67,6 +69,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
title title
uuid uuid
beginsOn beginsOn
endsOn
picture { picture {
id id
url url
@ -152,6 +155,7 @@ export const SEARCH_EVENTS = gql`
$endsOn: DateTime $endsOn: DateTime
$eventPage: Int $eventPage: Int
$limit: Int $limit: Int
$longevents: Boolean
) { ) {
searchEvents( searchEvents(
location: $location location: $location
@ -164,6 +168,7 @@ export const SEARCH_EVENTS = gql`
endsOn: $endsOn endsOn: $endsOn
page: $eventPage page: $eventPage
limit: $limit limit: $limit
longevents: $longevents
) { ) {
total total
elements { elements {

View File

@ -204,6 +204,7 @@
"No events found": "No events found", "No events found": "No events found",
"No group found": "No group found", "No group found": "No group found",
"No groups found": "No groups found", "No groups found": "No groups found",
"No activities found": "No activities found",
"No instance follows your instance yet.": "No instance follows your instance yet.", "No instance follows your instance yet.": "No instance follows your instance yet.",
"No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances", "No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances",
"No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances", "No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances",
@ -1394,6 +1395,7 @@
"Reported content": "Reported content", "Reported content": "Reported content",
"No results found": "No results found", "No results found": "No results found",
"{eventsCount} events found": "No events found|One event found|{eventsCount} events found", "{eventsCount} events found": "No events found|One event found|{eventsCount} events found",
"{eventsCount} activities found": "No activities found|One activity found|{eventsCount} activities found",
"{groupsCount} groups found": "No groups found|One group found|{groupsCount} groups found", "{groupsCount} groups found": "No groups found|One group found|{groupsCount} groups found",
"{resultsCount} results found": "No results found|On result found|{resultsCount} results found", "{resultsCount} results found": "No results found|On result found|{resultsCount} results found",
"Loading map": "Loading map", "Loading map": "Loading map",

View File

@ -716,6 +716,7 @@
"No end date": "Pas de date de fin", "No end date": "Pas de date de fin",
"No event found at this address": "Aucun événement trouvé à cette addresse", "No event found at this address": "Aucun événement trouvé à cette addresse",
"No events found": "Aucun événement trouvé", "No events found": "Aucun événement trouvé",
"No activities found": "Aucun activité trouvé",
"No events found for {search}": "Aucun événement trouvé pour {search}", "No events found for {search}": "Aucun événement trouvé pour {search}",
"No follower matches the filters": "Aucun·e abonné·e ne correspond aux filtres", "No follower matches the filters": "Aucun·e abonné·e ne correspond aux filtres",
"No group found": "Aucun groupe trouvé", "No group found": "Aucun groupe trouvé",
@ -1544,6 +1545,7 @@
"{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s", "{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s",
"{count} requests waiting": "Une demande en attente|{count} demandes en attente", "{count} requests waiting": "Une demande en attente|{count} demandes en attente",
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés", "{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
"{eventsCount} activities found": "Aucune activité trouvé|Une activité trouvé|{eventsCount} activités trouvés",
"{folder} - Resources": "{folder} - Ressources", "{folder} - Resources": "{folder} - Ressources",
"{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés", "{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés",
"{group} activity timeline": "Timeline de l'activité de {group}", "{group} activity timeline": "Timeline de l'activité de {group}",

View File

@ -41,6 +41,7 @@ export interface IConfig {
registrationsOpen: boolean; registrationsOpen: boolean;
registrationsAllowlist: boolean; registrationsAllowlist: boolean;
demoMode: boolean; demoMode: boolean;
longEvents: boolean;
countryCode: string; countryCode: string;
eventCategories: { id: string; label: string }[]; eventCategories: { id: string; label: string }[];
languages: string[]; languages: string[];

View File

@ -134,6 +134,8 @@ export enum SearchTabs {
export enum ContentType { export enum ContentType {
ALL = "ALL", ALL = "ALL",
EVENTS = "EVENTS", EVENTS = "EVENTS",
SHORTEVENTS = "SHORTEVENTS",
LONGEVENTS = "LONGEVENTS",
GROUPS = "GROUPS", GROUPS = "GROUPS",
} }

View File

@ -69,10 +69,15 @@ function formatDateTimeForEvent(dateTime: Date, locale: Locale): string {
return format(dateTime, "PPp", { locale }); return format(dateTime, "PPp", { locale });
} }
function formatDateForEvent(dateTime: Date, locale: Locale): string {
return format(dateTime, "PP", { locale });
}
export { export {
localeMonthNames, localeMonthNames,
localeShortWeekDayNames, localeShortWeekDayNames,
formatBytes, formatBytes,
roundToNearestMinute, roundToNearestMinute,
formatDateTimeForEvent, formatDateTimeForEvent,
formatDateForEvent,
}; };

View File

@ -45,6 +45,16 @@
:size="24" :size="24"
/> />
<Calendar
v-if="content.contentType === ContentType.SHORTEVENTS"
:size="24"
/>
<CalendarStar
v-if="content.contentType === ContentType.LONGEVENTS"
:size="24"
/>
<AccountMultiple <AccountMultiple
v-if="content.contentType === ContentType.GROUPS" v-if="content.contentType === ContentType.GROUPS"
:size="24" :size="24"
@ -443,8 +453,15 @@
class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2" class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2"
> >
<p v-if="totalCount === 0"> <p v-if="totalCount === 0">
<span v-if="contentType === ContentType.EVENTS">{{ <span
t("No events found") v-if="
contentType === ContentType.EVENTS ||
contentType === ContentType.SHORTEVENTS
"
>{{ t("No events found") }}</span
>
<span v-else-if="contentType === ContentType.LONGEVENTS">{{
t("No activities found")
}}</span> }}</span>
<span v-else-if="contentType === ContentType.GROUPS">{{ <span v-else-if="contentType === ContentType.GROUPS">{{
t("No groups found") t("No groups found")
@ -452,7 +469,12 @@
<span v-else>{{ t("No results found") }}</span> <span v-else>{{ t("No results found") }}</span>
</p> </p>
<p v-else> <p v-else>
<span v-if="contentType === 'EVENTS'"> <span
v-if="
contentType === ContentType.EVENTS ||
contentType === ContentType.SHORTEVENTS
"
>
{{ {{
t( t(
"{eventsCount} events found", "{eventsCount} events found",
@ -461,7 +483,16 @@
) )
}} }}
</span> </span>
<span v-else-if="contentType === 'GROUPS'"> <span v-else-if="contentType === ContentType.LONGEVENTS">
{{
t(
"{eventsCount} activities found",
{ eventsCount: searchEvents?.total },
searchEvents?.total ?? 0
)
}}
</span>
<span v-else-if="contentType === ContentType.GROUPS">
{{ {{
t( t(
"{groupsCount} groups found", "{groupsCount} groups found",
@ -597,7 +628,13 @@
:aria-current-label="t('Current page')" :aria-current-label="t('Current page')"
/> />
</template> </template>
<template v-else-if="contentType === ContentType.EVENTS"> <template
v-else-if="
contentType === ContentType.EVENTS ||
contentType === ContentType.SHORTEVENTS ||
contentType === ContentType.LONGEVENTS
"
>
<template v-if="searchLoading"> <template v-if="searchLoading">
<SkeletonEventResultList v-for="i in 8" :key="i" /> <SkeletonEventResultList v-for="i in 8" :key="i" />
</template> </template>
@ -625,13 +662,23 @@
> >
</o-pagination> </o-pagination>
</template> </template>
<EmptyContent v-else-if="searchLoading === false" icon="calendar"> <EmptyContent
v-else-if="searchLoading === false"
:icon="
contentType === ContentType.LONGEVENTS
? 'calendar-star'
: 'calendar'
"
>
<span v-if="searchIsUrl"> <span v-if="searchIsUrl">
{{ t("No event found at this address") }} {{ t("No event found at this address") }}
</span> </span>
<span v-else-if="!search"> <span v-else-if="!search && contentType !== ContentType.LONGEVENTS">
{{ t("No events found") }} {{ t("No events found") }}
</span> </span>
<span v-else-if="!search && contentType === ContentType.LONGEVENTS">
{{ t("No activities found") }}
</span>
<i18n-t keypath="No events found for {search}" tag="span" v-else> <i18n-t keypath="No events found for {search}" tag="span" v-else>
<template #search> <template #search>
<b>{{ search }}</b> <b>{{ search }}</b>
@ -694,7 +741,7 @@
icon="account-multiple" icon="account-multiple"
> >
<span v-if="!search"> <span v-if="!search">
{{ t("No events found") }} {{ t("No groups found") }}
</span> </span>
<i18n-t keypath="No groups found for {search}" tag="span" v-else> <i18n-t keypath="No groups found for {search}" tag="span" v-else>
<template #search> <template #search>
@ -767,6 +814,7 @@ import {
booleanTransformer, booleanTransformer,
} from "vue-use-route-query"; } from "vue-use-route-query";
import Calendar from "vue-material-design-icons/Calendar.vue"; import Calendar from "vue-material-design-icons/Calendar.vue";
import CalendarStar from "vue-material-design-icons/CalendarStar.vue";
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue"; import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
import Magnify from "vue-material-design-icons/Magnify.vue"; import Magnify from "vue-material-design-icons/Magnify.vue";
@ -778,6 +826,7 @@ import langs from "@/i18n/langs.json";
import { import {
useEventCategories, useEventCategories,
useFeatures, useFeatures,
useIsLongEvents,
useSearchConfig, useSearchConfig,
} from "@/composition/apollo/config"; } from "@/composition/apollo/config";
import { coordsToGeoHash } from "@/utils/location"; import { coordsToGeoHash } from "@/utils/location";
@ -904,6 +953,7 @@ const GROUP_PAGE_LIMIT = 16;
const { features } = useFeatures(); const { features } = useFeatures();
const { eventCategories } = useEventCategories(); const { eventCategories } = useEventCategories();
const { islongEvents } = useIsLongEvents();
const orderedCategories = computed(() => { const orderedCategories = computed(() => {
if (!eventCategories.value) return []; if (!eventCategories.value) return [];
@ -1017,20 +1067,41 @@ const searchIsUrl = computed((): boolean => {
}); });
const contentTypeMapping = computed(() => { const contentTypeMapping = computed(() => {
return [ if (islongEvents.value) {
{ return [
contentType: "ALL", {
label: t("Everything"), contentType: "ALL",
}, label: t("Everything"),
{ },
contentType: "EVENTS", {
label: t("Events"), contentType: "SHORTEVENTS",
}, label: t("Events"),
{ },
contentType: "GROUPS", {
label: t("Groups"), contentType: "LONGEVENTS",
}, label: t("Activities"),
]; },
{
contentType: "GROUPS",
label: t("Groups"),
},
];
} else {
return [
{
contentType: "ALL",
label: t("Everything"),
},
{
contentType: "EVENTS",
label: t("Events"),
},
{
contentType: "GROUPS",
label: t("Groups"),
},
];
}
}); });
const eventDistance = computed(() => { const eventDistance = computed(() => {
@ -1138,6 +1209,16 @@ const geoHashLocation = computed(() =>
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3))); const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
const longEvents = computed(() => {
if (contentType.value === ContentType.SHORTEVENTS) {
return false;
} else if (contentType.value === ContentType.LONGEVENTS) {
return true;
} else {
return null;
}
});
const totalCount = computed(() => { const totalCount = computed(() => {
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0); return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
}); });
@ -1150,7 +1231,11 @@ const sortOptions = computed(() => {
}, },
]; ];
if (contentType.value == ContentType.EVENTS) { if (
contentType.value === ContentType.EVENTS ||
contentType.value === ContentType.SHORTEVENTS ||
contentType.value === ContentType.LONGEVENTS
) {
options.push( options.push(
{ {
key: SortValues.START_TIME_ASC, key: SortValues.START_TIME_ASC,
@ -1171,7 +1256,7 @@ const sortOptions = computed(() => {
); );
} }
if (contentType.value == ContentType.GROUPS) { if (contentType.value === ContentType.GROUPS) {
options.push({ options.push({
key: SortValues.MEMBER_COUNT_DESC, key: SortValues.MEMBER_COUNT_DESC,
label: t("Number of members"), label: t("Number of members"),
@ -1282,6 +1367,12 @@ watch(
case ContentType.EVENTS: case ContentType.EVENTS:
eventPage.value = 1; eventPage.value = 1;
break; break;
case ContentType.SHORTEVENTS:
eventPage.value = 1;
break;
case ContentType.LONGEVENTS:
eventPage.value = 1;
break;
case ContentType.GROUPS: case ContentType.GROUPS:
groupPage.value = 1; groupPage.value = 1;
break; break;
@ -1298,6 +1389,7 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
location: geoHashLocation.value, location: geoHashLocation.value,
beginsOn: start.value, beginsOn: start.value,
endsOn: end.value, endsOn: end.value,
longevents: longEvents.value,
radius: geoHashLocation.value ? radius.value : undefined, radius: geoHashLocation.value ? radius.value : undefined,
eventPage: eventPage:
contentType.value === ContentType.ALL ? page.value : eventPage.value, contentType.value === ContentType.ALL ? page.value : eventPage.value,

View File

@ -2,13 +2,24 @@ defmodule Mobilizon.GraphQL.Resolvers.ConfigTest do
use Mobilizon.Web.ConnCase use Mobilizon.Web.ConnCase
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
describe "Resolver: Get config" do describe "Resolver: Get config" do
test "get_config/3 returns the instance config", context do test "get_config/3 returns the instance config", context do
Cachex.clear("full_config") Cachex.clear("full_config")
Mobilizon.Config.clear_config_cache() Mobilizon.Config.clear_config_cache()
Config.put([:instance, :name], "Test instance")
Config.put([:instance, :registrations_open], true)
Config.put([:instance, :demo], false)
Config.put([:instance, :duration_of_long_event], 0)
Config.put(
[:instance, :description],
"Change this to a proper description of your instance"
)
Config.put([:instance, :federating], true)
query = """ query = """
{ {
@ -48,5 +59,99 @@ defmodule Mobilizon.GraphQL.Resolvers.ConfigTest do
{:ok, %Actor{id: actor_id}} = Actors.get_or_create_internal_actor("anonymous") {:ok, %Actor{id: actor_id}} = Actors.get_or_create_internal_actor("anonymous")
assert res["data"]["config"]["anonymous"]["actor_id"] == to_string(actor_id) assert res["data"]["config"]["anonymous"]["actor_id"] == to_string(actor_id)
end end
test "get_config/3 returns the instance config default", context do
Cachex.clear("full_config")
Mobilizon.Config.clear_config_cache()
Config.put([:instance, :name], "Test instance")
Config.put([:instance, :registrations_open], true)
Config.put([:instance, :demo], false)
Config.put([:instance, :duration_of_long_event], 0)
Config.put(
[:instance, :description],
"Change this to a proper description of your instance"
)
Config.put([:instance, :federating], true)
query = """
{
config {
name,
registrationsOpen,
registrations_allowlist,
contact,
demo_mode,
long_events,
description,
long_description,
slogan,
languages,
timezones,
rules,
version,
federating
}
}
"""
res =
context.conn
|> AbsintheHelpers.graphql_query(query: query)
assert res["data"]["config"]["name"] == "Test instance"
assert res["data"]["config"]["registrationsOpen"] == true
assert res["data"]["config"]["registrations_allowlist"] == false
assert res["data"]["config"]["contact"] == nil
assert res["data"]["config"]["demo_mode"] == false
assert res["data"]["config"]["long_events"] == false
assert res["data"]["config"]["description"] ==
"Change this to a proper description of your instance"
assert res["data"]["config"]["long_description"] == nil
assert res["data"]["config"]["slogan"] == nil
assert res["data"]["config"]["languages"] == []
assert length(res["data"]["config"]["timezones"]) == 596
assert res["data"]["config"]["rules"] == nil
assert String.slice(res["data"]["config"]["version"], 0, 5) == "4.0.2"
assert res["data"]["config"]["federating"] == true
end
test "get_config/3 returns the instance config changed", context do
Cachex.clear("full_config")
Mobilizon.Config.clear_config_cache()
Config.put([:instance, :name], "My instance")
Config.put([:instance, :registrations_open], false)
Config.put([:instance, :demo], true)
Config.put([:instance, :duration_of_long_event], 30)
Config.put([:instance, :description], "My description")
Config.put([:instance, :federating], false)
query = """
{
config {
name,
registrationsOpen,
demo_mode,
long_events,
description,
federating
}
}
"""
res =
context.conn
|> AbsintheHelpers.graphql_query(query: query)
assert res["data"]["config"]["name"] == "My instance"
assert res["data"]["config"]["registrationsOpen"] == false
assert res["data"]["config"]["demo_mode"] == true
assert res["data"]["config"]["long_events"] == true
assert res["data"]["config"]["description"] == "My description"
assert res["data"]["config"]["federating"] == false
end
end end
end end

View File

@ -4,11 +4,11 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Service.Workers
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
alias Mobilizon.Service.Workers
setup %{conn: conn} do setup %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -18,8 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
describe "search events/3" do describe "search events/3" do
@search_events_query """ @search_events_query """
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $searchTarget: SearchTarget) { query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $longevents:Boolean, $searchTarget: SearchTarget) {
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, searchTarget: $searchTarget) { searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, longevents: $longevents, searchTarget: $searchTarget) {
total, total,
elements { elements {
id id
@ -149,6 +149,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
end end
test "finds events by begins_on and ends_on", %{conn: conn} do test "finds events by begins_on and ends_on", %{conn: conn} do
Config.put([:instance, :duration_of_long_event], 0)
now = DateTime.utc_now() now = DateTime.utc_now()
# TODO # TODO
@ -183,6 +184,214 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
event.uuid event.uuid
end end
test "finds 4 events : long event disabled", %{conn: conn} do
Config.put([:instance, :duration_of_long_event], 0)
now = DateTime.utc_now()
event1 =
insert(:event,
title: "Cours 10j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 14)
)
event2 =
insert(:event,
title: "Long 29j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 33)
)
event3 =
insert(:event,
title: "Long 31j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 35)
)
event4 =
insert(:event,
title: "Long 40j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 44)
)
Workers.BuildSearch.insert_search_event(event1)
Workers.BuildSearch.insert_search_event(event2)
Workers.BuildSearch.insert_search_event(event3)
Workers.BuildSearch.insert_search_event(event4)
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: false}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 4
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event1.uuid,
event2.uuid,
event3.uuid,
event4.uuid
]
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: true}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 4
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event1.uuid,
event2.uuid,
event3.uuid,
event4.uuid
]
end
test "finds 4 events : long event enabled 30 days", %{conn: conn} do
Config.put([:instance, :duration_of_long_event], 30)
now = DateTime.utc_now()
event1 =
insert(:event,
title: "Cours 10j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 15)
)
event2 =
insert(:event,
title: "Long 30j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 35)
)
event3 =
insert(:event,
title: "Long 31j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 36)
)
event4 =
insert(:event,
title: "Long 40j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 45)
)
Workers.BuildSearch.insert_search_event(event1)
Workers.BuildSearch.insert_search_event(event2)
Workers.BuildSearch.insert_search_event(event3)
Workers.BuildSearch.insert_search_event(event4)
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: false}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 2
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event1.uuid,
event2.uuid
]
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: true}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 2
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event3.uuid,
event4.uuid
]
end
test "finds 4 events : long event enabled 15 days", %{conn: conn} do
Config.put([:instance, :duration_of_long_event], 15)
now = DateTime.utc_now()
event1 =
insert(:event,
title: "Cours 10j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 15)
)
event2 =
insert(:event,
title: "Long 30j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 35)
)
event3 =
insert(:event,
title: "Long 31j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 36)
)
event4 =
insert(:event,
title: "Long 40j",
begins_on: DateTime.add(now, 3600 * 24 * 5),
ends_on: DateTime.add(now, 3600 * 24 * 45)
)
Workers.BuildSearch.insert_search_event(event1)
Workers.BuildSearch.insert_search_event(event2)
Workers.BuildSearch.insert_search_event(event3)
Workers.BuildSearch.insert_search_event(event4)
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: false}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 1
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event1.uuid
]
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{longevents: true}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 3
assert res["data"]["searchEvents"]["elements"]
|> Enum.map(& &1["uuid"]) == [
event2.uuid,
event3.uuid,
event4.uuid
]
end
test "finds events with multiple criteria", %{conn: conn} do test "finds events with multiple criteria", %{conn: conn} do
{lon, lat} = {45.75, 4.85} {lon, lat} = {45.75, 4.85}
point = %Geo.Point{coordinates: {lon, lat}, srid: 4326} point = %Geo.Point{coordinates: {lon, lat}, srid: 4326}

View File

@ -49,6 +49,7 @@ export const configMock = {
}, },
countryCode: "fr", countryCode: "fr",
demoMode: false, demoMode: false,
longEvents: false,
description: "Mobilizon.fr est l'instance Mobilizon de Framasoft.", description: "Mobilizon.fr est l'instance Mobilizon de Framasoft.",
features: { features: {
__typename: "Features", __typename: "Features",