Merge branch 'fixes' into 'main'

Various fixes

See merge request framasoft/mobilizon!1262
This commit is contained in:
Thomas Citharel 2022-09-27 12:20:08 +00:00
commit cbce14c9d8
29 changed files with 533 additions and 312 deletions

View File

@ -144,26 +144,32 @@ vitest:
- js/junit.xml - js/junit.xml
expire_in: 30 days expire_in: 30 days
# cypress: e2e:
# stage: test stage: test
# services: services:
# - name: postgis/postgis:13.3 - name: postgis/postgis:14-3.2
# alias: postgres alias: postgres
# variables: variables:
# MIX_ENV=e2e MIX_ENV: "e2e"
# script: before_script:
# - mix ecto.create - mix deps.get
# - mix ecto.migrate - mix ecto.create
# - mix run priv/repo/e2e.seed.exs - mix ecto.migrate
# - mix phx.server & - mix run priv/repo/e2e.seed.exs
# - cd js - cd js && yarn run build && npx playwright install && cd ../
# - npx wait-on http://localhost:4000 - mix phx.digest
# - if [ -z "$CYPRESS_KEY" ]; then npx cypress run; else npx cypress run --record --parallel --key $CYPRESS_KEY; fi script:
# artifacts: - mix phx.server &
# expire_in: 2 day - cd js
# paths: - npx wait-on http://localhost:4000
# - js/tests/e2e/screenshots/**/*.png - npx playwright test --project $BROWSER
# - js/tests/e2e/videos/**/*.mp4 parallel:
matrix:
- BROWSER: ['firefox', 'chromium']
artifacts:
expire_in: 2 days
paths:
- js/playwright-report
pages: pages:
stage: deploy stage: deploy

View File

@ -19,19 +19,39 @@ config :mobilizon, Mobilizon.Web.Endpoint,
yarn: [cd: Path.expand("../js", __DIR__)] yarn: [cd: Path.expand("../js", __DIR__)]
] ]
require Logger config :vite_phx,
release_app: :mobilizon,
# Hard code :prod as an environment as :e2e will not be recongnized
environment: :prod,
vite_manifest: "priv/static/manifest.json",
phx_manifest: "priv/static/cache_manifest.json",
dev_server_address: "http://localhost:5173"
cond do config :mobilizon, :instance,
System.get_env("INSTANCE_CONFIG") && name: "E2E Testing instance",
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") -> description: "E2E is safety",
import_config System.get_env("INSTANCE_CONFIG") hostname: "mobilizon1.com",
registrations_open: true,
registration_email_denylist: ["gmail.com", "deny@tcit.fr"],
demo: false,
default_language: "en",
allow_relay: true,
federating: true,
email_from: "mobilizon@mobilizon1.com",
email_reply_to: nil,
enable_instance_feeds: true,
koena_connect_link: true,
extra_categories: [
%{
id: :something_else,
label: "Quelque chose d'autre"
}
]
System.get_env("DOCKER", "false") == "false" && File.exists?("./config/e2e.secret.exs") -> config :mobilizon, Mobilizon.Storage.Repo,
import_config "e2e.secret.exs" adapter: Ecto.Adapters.Postgres,
username: System.get_env("MOBILIZON_DATABASE_USERNAME", "mobilizon_e2e"),
System.get_env("DOCKER", "false") == "true" -> password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "mobilizon_e2e"),
Logger.info("Using environment configuration for Docker") database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon_e2e"),
hostname: System.get_env("MOBILIZON_DATABASE_HOST", "localhost"),
true -> port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432"
Logger.error("No configuration file found")
end

2
js/.gitignore vendored
View File

@ -2,8 +2,6 @@
node_modules node_modules
/dist /dist
/tests/e2e/videos/
/tests/e2e/screenshots/
/coverage /coverage
stats.html stats.html

View File

@ -1,41 +0,0 @@
# mobilizon
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Run your unit tests
```
yarn test:unit
```
### Run your end-to-end tests
```
yarn test:e2e
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -1,7 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js",
"projectId": "86dpkx",
"baseUrl": "http://localhost:4000",
"viewportWidth": 1920,
"viewportHeight": 1080
}

View File

@ -13,7 +13,7 @@ import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
testDir: "./tests/e2e", testDir: "./tests/e2e",
/* Maximum time one test can run for. */ /* Maximum time one test can run for. */
timeout: 30 * 1000, timeout: 10 * 1000,
expect: { expect: {
/** /**
* Maximum time expect() should wait for the condition to be met. * Maximum time expect() should wait for the condition to be met.
@ -36,7 +36,7 @@ const config: PlaywrightTestConfig = {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0, actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:4005", baseURL: "http://localhost:4000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry", trace: "on-first-retry",

View File

@ -57,13 +57,17 @@ import {
} from "vue"; } from "vue";
import { LocationType } from "@/types/user-location.model"; import { LocationType } from "@/types/user-location.model";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import { initializeCurrentActor } from "@/utils/identity"; import {
initializeCurrentActor,
NoIdentitiesException,
} from "@/utils/identity";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { Snackbar } from "@/plugins/snackbar"; import { Snackbar } from "@/plugins/snackbar";
import { Notifier } from "@/plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import RouteName from "@/router/name";
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG); const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
@ -130,7 +134,19 @@ interval.value = setInterval(async () => {
onBeforeMount(async () => { onBeforeMount(async () => {
if (initializeCurrentUser()) { if (initializeCurrentUser()) {
try {
await initializeCurrentActor(); await initializeCurrentActor();
} catch (err) {
if (err instanceof NoIdentitiesException) {
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: localStorage.getItem(AUTH_USER_EMAIL),
userAlreadyActivated: "true",
},
});
}
}
} }
}); });

View File

@ -26,13 +26,13 @@
variant="info" variant="info"
v-if="event.status === EventStatus.TENTATIVE" v-if="event.status === EventStatus.TENTATIVE"
> >
{{ $t("Tentative") }} {{ t("Tentative") }}
</mobilizon-tag> </mobilizon-tag>
<mobilizon-tag <mobilizon-tag
variant="danger" variant="danger"
v-if="event.status === EventStatus.CANCELLED" v-if="event.status === EventStatus.CANCELLED"
> >
{{ $t("Cancelled") }} {{ t("Cancelled") }}
</mobilizon-tag> </mobilizon-tag>
<router-link <router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }" :to="{ name: RouteName.TAG, params: { tag: tag.title } }"
@ -100,24 +100,40 @@
v-else-if="event.options && event.options.isOnline" v-else-if="event.options && event.options.isOnline"
> >
<Video /> <Video />
<span class="ltr:pl-2 rtl:pr-2">{{ $t("Online") }}</span> <span class="ltr:pl-2 rtl:pr-2">{{ t("Online") }}</span>
</div> </div>
<div <div
class="mt-1 no-underline gap-1 items-center hidden" class="mt-1 no-underline gap-1 items-center hidden"
:class="{ 'sm:flex': mode === 'row' }" :class="{ 'sm:flex': mode === 'row' }"
v-if="event.tags || event.status !== EventStatus.CONFIRMED" v-if="
event.tags ||
event.status !== EventStatus.CONFIRMED ||
event.participantStats?.participant > 0
"
> >
<mobilizon-tag
variant="info"
v-if="event.participantStats?.participant > 0"
>
{{
t(
"{count} participants",
event.participantStats?.participant,
{ count: event.participantStats?.participant }
)
}}
</mobilizon-tag>
<mobilizon-tag <mobilizon-tag
variant="info" variant="info"
v-if="event.status === EventStatus.TENTATIVE" v-if="event.status === EventStatus.TENTATIVE"
> >
{{ $t("Tentative") }} {{ t("Tentative") }}
</mobilizon-tag> </mobilizon-tag>
<mobilizon-tag <mobilizon-tag
variant="danger" variant="danger"
v-if="event.status === EventStatus.CANCELLED" v-if="event.status === EventStatus.CANCELLED"
> >
{{ $t("Cancelled") }} {{ t("Cancelled") }}
</mobilizon-tag> </mobilizon-tag>
<router-link <router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }" :to="{ name: RouteName.TAG, params: { tag: tag.title } }"
@ -156,6 +172,9 @@ import Video from "vue-material-design-icons/Video.vue";
import { formatDateTimeForEvent } from "@/utils/datetime"; import { 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";
const { t } = useI18n({ useScope: "global" });
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@ -406,11 +406,11 @@ watch(identities, () => {
// If we don't have any identities, the user has validated their account, // If we don't have any identities, the user has validated their account,
// is logging for the first time but didn't create an identity somehow // is logging for the first time but didn't create an identity somehow
if (identities.value && identities.value.length === 0) { if (identities.value && identities.value.length === 0) {
console.debug( console.warn(
"We have no identities listed for current user", "We have no identities listed for current user",
identities.value identities.value
); );
console.debug("Pushing route to REGISTER_PROFILE"); console.info("Pushing route to REGISTER_PROFILE");
router.push({ router.push({
name: RouteName.REGISTER_PROFILE, name: RouteName.REGISTER_PROFILE,
params: { params: {

View File

@ -1,6 +1,6 @@
<template> <template>
<span <span
class="rounded-md my-1 truncate text-sm text-violet-title px-2 py-1" class="rounded-md truncate text-sm text-violet-title px-2 py-1"
:class="[ :class="[
typeClasses, typeClasses,
capitalize, capitalize,

View File

@ -66,15 +66,7 @@ export async function updateLocale(locale: string) {
})); }));
} }
export function registerAccount( export function registerAccount() {
variables: {
preferredUsername: string;
name: string;
summary: string;
email: string;
},
userAlreadyActivated: boolean
) {
return useMutation< return useMutation<
{ registerPerson: IPerson }, { registerPerson: IPerson },
{ {
@ -84,12 +76,12 @@ export function registerAccount(
email: string; email: string;
} }
>(REGISTER_PERSON, () => ({ >(REGISTER_PERSON, () => ({
variables,
update: ( update: (
store: ApolloCache<{ registerPerson: IPerson }>, store: ApolloCache<{ registerPerson: IPerson }>,
{ data: localData }: FetchResult { data: localData }: FetchResult,
{ context }
) => { ) => {
if (userAlreadyActivated) { if (context?.userAlreadyActivated) {
const identitiesData = store.readQuery<{ identities: IPerson[] }>({ const identitiesData = store.readQuery<{ identities: IPerson[] }>({
query: IDENTITIES, query: IDENTITIES,
}); });

View File

@ -38,6 +38,9 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
$eventPage: Int $eventPage: Int
$groupPage: Int $groupPage: Int
$limit: Int $limit: Int
$sortByEvents: SearchEventSortOptions
$sortByGroups: SearchGroupSortOptions
$boostLanguages: [String]
) { ) {
searchEvents( searchEvents(
location: $location location: $location
@ -55,6 +58,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
zoom: $zoom zoom: $zoom
page: $eventPage page: $eventPage
limit: $limit limit: $limit
sortBy: $sortByEvents
boostLanguages: $boostLanguages
) { ) {
total total
elements { elements {
@ -80,6 +85,9 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
attributedTo { attributedTo {
...ActorFragment ...ActorFragment
} }
participantStats {
participant
}
options { options {
isOnline isOnline
} }
@ -96,6 +104,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
zoom: $zoom zoom: $zoom
page: $groupPage page: $groupPage
limit: $limit limit: $limit
sortBy: $sortByGroups
boostLanguages: $boostLanguages
) { ) {
total total
elements { elements {

View File

@ -28,7 +28,7 @@ export const userRoutes: RouteRecordRaw[] = [
beforeEnter: beforeRegisterGuard, beforeEnter: beforeRegisterGuard,
}, },
{ {
path: "/register/profile", path: "/register/profile/:email/:userAlreadyActivated?",
name: UserRouteName.REGISTER_PROFILE, name: UserRouteName.REGISTER_PROFILE,
component: (): Promise<any> => import("@/views/Account/RegisterView.vue"), component: (): Promise<any> => import("@/views/Account/RegisterView.vue"),
// We can only pass string values through params, therefore // We can only pass string values through params, therefore

View File

@ -11,7 +11,7 @@ import { computed, watch } from "vue";
export class NoIdentitiesException extends Error {} export class NoIdentitiesException extends Error {}
export function saveActorData(obj: IPerson): void { function saveActorData(obj: IPerson): void {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`); localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
} }

View File

@ -1,38 +1,42 @@
<template> <template>
<section class="container mx-auto"> <section class="container mx-auto max-w-screen-sm">
<div class="">
<div class="">
<h1 class="text-2xl" v-if="userAlreadyActivated"> <h1 class="text-2xl" v-if="userAlreadyActivated">
{{ $t("Congratulations, your account is now created!") }} {{ t("Congratulations, your account is now created!") }}
</h1> </h1>
<h1 class="text-2xl" v-else> <h1 class="text-2xl" v-else>
{{ {{
$t("Register an account on {instanceName}!", { t("Register an account on {instanceName}!", {
instanceName, instanceName,
}) })
}} }}
</h1> </h1>
<p class="prose dark:prose-invert" v-if="userAlreadyActivated"> <p class="prose dark:prose-invert" v-if="userAlreadyActivated">
{{ $t("Now, create your first profile:") }} {{ t("Now, create your first profile:") }}
</p> </p>
<form v-if="!validationSent" @submit.prevent="submit"> <form v-if="!validationSent" @submit.prevent="submit">
<o-field :label="$t('Displayed nickname')"> <o-notification variant="danger" v-if="errors.extra">
{{ errors.extra }}
</o-notification>
<o-field :label="t('Displayed nickname')" labelFor="identityName">
<o-input <o-input
aria-required="true" aria-required="true"
required required
v-model="identity.name" v-model="identity.name"
id="identityName"
@input="autoUpdateUsername" @input="autoUpdateUsername"
/> />
</o-field> </o-field>
<o-field <o-field
:label="$t('Username')" :label="t('Username')"
:type="errors.preferred_username ? 'is-danger' : null" :variant="errors.preferred_username ? 'danger' : null"
:message="errors.preferred_username" :message="errors.preferred_username"
labelFor="identityPreferredUsername"
> >
<o-field <o-field
:message=" :message="
$t( t(
'Only alphanumeric lowercased characters and underscores are supported.' 'Only alphanumeric lowercased characters and underscores are supported.'
) )
" "
@ -41,52 +45,54 @@
aria-required="true" aria-required="true"
required required
expanded expanded
id="identityPreferredUsername"
v-model="identity.preferredUsername" v-model="identity.preferredUsername"
:validation-message=" :validation-message="
identity.preferredUsername identity.preferredUsername
? $t( ? t(
'Only alphanumeric lowercased characters and underscores are supported.' 'Only alphanumeric lowercased characters and underscores are supported.'
) )
: null : null
" "
/> />
<p class="control"> <p class="control">
<span class="button is-static">@{{ host }}</span> <span class="button">@{{ host }}</span>
</p> </p>
</o-field> </o-field>
</o-field> </o-field>
<p class="description"> <p class="prose dark:prose-invert">
{{ {{
$t( t(
"This identifier is unique to your profile. It allows others to find you." "This identifier is unique to your profile. It allows others to find you."
) )
}} }}
</p> </p>
<o-field :label="$t('Short bio')"> <o-field :label="t('Short bio')" labelFor="identitySummary">
<o-input <o-input
type="textarea" type="textarea"
maxlength="100" maxlength="100"
rows="2" rows="2"
id="identitySummary"
v-model="identity.summary" v-model="identity.summary"
/> />
</o-field> </o-field>
<p class="prose dark:prose-invert"> <p class="prose dark:prose-invert">
{{ {{
$t( t(
"You will be able to add an avatar and set other options in your account settings." "You will be able to add an avatar and set other options in your account settings."
) )
}} }}
</p> </p>
<p class="control has-text-centered"> <p class="text-center">
<o-button <o-button
variant="primary" variant="primary"
size="large" size="large"
native-type="submit" native-type="submit"
:disabled="sendingValidation" :disabled="sendingValidation"
>{{ $t("Create my profile") }}</o-button >{{ t("Create my profile") }}</o-button
> >
</p> </p>
</form> </form>
@ -95,7 +101,7 @@
<o-notification variant="success" :closable="false"> <o-notification variant="success" :closable="false">
<h2 class="title"> <h2 class="title">
{{ {{
$t("Your account is nearly ready, {username}", { t("Your account is nearly ready, {username}", {
username: identity.name ?? identity.preferredUsername, username: identity.name ?? identity.preferredUsername,
}) })
}} }}
@ -107,15 +113,16 @@
</i18n-t> </i18n-t>
<p> <p>
{{ {{
$t( t(
"Before you can login, you need to click on the link inside it to validate your account." "Before you can login, you need to click on the link inside it to validate your account."
) )
}} }}
</p> </p>
<o-button tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Back to homepage")
}}</o-button>
</o-notification> </o-notification>
</div> </div>
</div>
</div>
</section> </section>
</template> </template>
@ -173,13 +180,7 @@ const autoUpdateUsername = () => {
identity.value.preferredUsername = convertToUsername(identity.value.name); identity.value.preferredUsername = convertToUsername(identity.value.name);
}; };
const submit = async (): Promise<void> => { const { onDone, onError, mutate } = registerAccount();
sendingValidation.value = true;
errors.value = {};
const { onDone, onError } = registerAccount(
{ email: props.email, ...identity.value },
props.userAlreadyActivated
);
onDone(async ({ data }) => { onDone(async ({ data }) => {
validationSent.value = true; validationSent.value = true;
@ -195,7 +196,11 @@ const submit = async (): Promise<void> => {
onError((err) => { onError((err) => {
errors.value = err.graphQLErrors.reduce( errors.value = err.graphQLErrors.reduce(
(acc: { [key: string]: string }, error: any) => { (acc: { [key: string]: string }, error: any) => {
acc[error.details || error.field] = error.message; acc[error.details ?? error.field ?? "extra"] = Array.isArray(
error.message
)
? (error.message as string[]).join(",")
: error.message;
return acc; return acc;
}, },
{} {}
@ -204,30 +209,13 @@ const submit = async (): Promise<void> => {
console.error("Errors while registering person", errors); console.error("Errors while registering person", errors);
sendingValidation.value = false; sendingValidation.value = false;
}); });
const submit = async (): Promise<void> => {
sendingValidation.value = true;
errors.value = {};
mutate(
{ email: props.email, ...identity.value },
{ context: { userAlreadyActivated: props.userAlreadyActivated } }
);
}; };
</script> </script>
<style lang="scss" scoped>
.avatar-enter-active {
transition: opacity 1s ease;
}
.avatar-enter,
.avatar-leave-to {
opacity: 0;
}
.avatar-leave {
display: none;
}
.container .columns {
margin: 1rem auto 3rem;
}
p.description {
font-size: 0.9rem;
margin-bottom: 15px;
margin-top: -10px;
}
</style>

View File

@ -365,7 +365,12 @@ onMounted(() => {
const router = useRouter(); const router = useRouter();
watch(loggedUser, (loggedUserValue) => { watch(loggedUser, (loggedUserValue) => {
if (loggedUserValue?.id && loggedUserValue?.settings === null) { if (
loggedUserValue?.id &&
loggedUserValue?.settings === null &&
loggedUserValue.defaultActor?.id
) {
console.info("No user settings, going to onboarding", loggedUserValue);
router.push({ router.push({
name: RouteName.WELCOME_SCREEN, name: RouteName.WELCOME_SCREEN,
params: { step: "1" }, params: { step: "1" },

View File

@ -2,7 +2,7 @@
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<SearchFields <SearchFields
class="md:ml-10 mr-2" class="md:ml-10 mr-2"
v-model:search="searchDebounced" v-model:search="search"
v-model:location="location" v-model:location="location"
:locationDefaultText="locationName" :locationDefaultText="locationName"
/> />
@ -736,13 +736,26 @@ enum ViewMode {
MAP = "map", MAP = "map",
} }
enum EventSortValues {
MATCH_DESC = "MATCH_DESC",
START_TIME_DESC = "START_TIME_DESC",
CREATED_AT_DESC = "CREATED_AT_DESC",
CREATED_AT_ASC = "CREATED_AT_ASC",
PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
}
enum GroupSortValues {
MATCH_DESC = "MATCH_DESC",
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
}
enum SortValues { enum SortValues {
MATCH_DESC = "-match", MATCH_DESC = "MATCH_DESC",
START_TIME_DESC = "-startTime", START_TIME_DESC = "START_TIME_DESC",
CREATED_AT_DESC = "-createdAt", CREATED_AT_DESC = "CREATED_AT_DESC",
CREATED_AT_ASC = "createdAt", CREATED_AT_ASC = "CREATED_AT_ASC",
PARTICIPANT_COUNT_DESC = "-participantCount", PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
MEMBER_COUNT_DESC = "-memberCount", MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
} }
const arrayTransformer: RouteQueryTransformer<string[]> = { const arrayTransformer: RouteQueryTransformer<string[]> = {
@ -1121,6 +1134,27 @@ watch(isOnline, (newIsOnline) => {
} }
}); });
const sortByForType = (
value: SortValues,
allowed: typeof EventSortValues | typeof GroupSortValues
): SortValues | undefined => {
return Object.values(allowed).includes(value) ? value : undefined;
};
const boostLanguagesQuery = computed((): string[] => {
const languages = new Set<string>();
for (const completeLanguage of navigator.languages) {
const language = completeLanguage.split("-")[0];
if (Object.keys(langs).find((langKey) => langKey === language)) {
languages.add(language);
}
}
return Array.from(languages);
});
const { result: searchElementsResult, loading: searchLoading } = useQuery<{ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
searchEvents: Paginate<TypeNamed<IEvent>>; searchEvents: Paginate<TypeNamed<IEvent>>;
searchGroups: Paginate<TypeNamed<IGroup>>; searchGroups: Paginate<TypeNamed<IGroup>>;
@ -1139,7 +1173,10 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
statusOneOf: statusOneOf.value, statusOneOf: statusOneOf.value,
languageOneOf: languageOneOf.value, languageOneOf: languageOneOf.value,
searchTarget: searchTarget.value, searchTarget: searchTarget.value,
bbox: bbox.value, bbox: mode.value === ViewMode.MAP ? bbox.value : undefined,
zoom: zoom.value, zoom: zoom.value,
sortByEvents: sortByForType(sortBy.value, EventSortValues),
sortByGroups: sortByForType(sortBy.value, GroupSortValues),
boostLanguages: boostLanguagesQuery.value,
})); }));
</script> </script>

View File

@ -91,7 +91,7 @@
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<o-field <o-field
:label="t('Email')" :label="t('Email')"
:type="errorEmailType" :variant="errorEmailType"
:message="errorEmailMessage" :message="errorEmailMessage"
label-for="email" label-for="email"
> >

View File

@ -25,17 +25,17 @@ test("Login has everything we need", async ({ page }) => {
await forgotPasswordLink.click(); await forgotPasswordLink.click();
await page.waitForURL("/password-reset/send"); await page.waitForURL("/password-reset/send");
await expect(page.url()).toContain("/password-reset/send"); expect(page.url()).toContain("/password-reset/send");
await page.goBack(); await page.goBack();
await reAskInstructionsLink.click(); await reAskInstructionsLink.click();
await page.waitForURL("/resend-instructions"); await page.waitForURL("/resend-instructions");
await expect(page.url()).toContain("/resend-instructions"); expect(page.url()).toContain("/resend-instructions");
await page.goBack(); await page.goBack();
await registerLink.click(); await registerLink.click();
await page.waitForURL("/register/user"); await page.waitForURL("/register/user");
await expect(page.url()).toContain("/register/user"); expect(page.url()).toContain("/register/user");
await page.goBack(); await page.goBack();
}); });
@ -59,8 +59,8 @@ test("Login rejects unknown users properly", async ({ page }) => {
test("Tries to login with valid credentials", async ({ page, context }) => { test("Tries to login with valid credentials", async ({ page, context }) => {
await page.goto("/login"); await page.goto("/login");
await page.locator("#email").fill("user@provider.org"); await page.locator("#email").fill("user@email.com");
await page.locator("#password").fill("valid_passw0rd"); await page.locator("#password").fill("some password");
const loginButton = page.locator("form button", { hasText: "Login" }); const loginButton = page.locator("form button", { hasText: "Login" });
@ -68,6 +68,103 @@ test("Tries to login with valid credentials", async ({ page, context }) => {
await loginButton.click(); await loginButton.click();
await page.waitForURL("/"); await page.waitForURL("/");
await expect(new URL(page.url()).pathname).toBe("/"); expect(new URL(page.url()).pathname).toBe("/");
expect((await context.storageState()).origins[0].localStorage).toBe("toto"); const localStorage = (
await context.storageState()
).origins[0].localStorage.reduce((acc: Record<string, string>, elem) => {
acc[elem.name] = elem.value;
return acc;
}, {});
expect(localStorage["auth-user-role"]).toBe("USER");
expect(localStorage["auth-access-token"]).toBeDefined();
expect(localStorage["auth-refresh-token"]).toBeDefined();
expect(localStorage["auth-user-email"]).toBe("user@email.com");
// Changes each run
// expect(localStorage["auth-user-id"]).toBe("3");
// Doesn't work in Chrome for some reason
// expect(localStorage['auth-user-actor-id']).toBe('2');
});
test("Tries to login with valid credentials but unconfirmed account", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#email").fill("unconfirmed@email.com");
await page.locator("#password").fill("some password");
await page.keyboard.press("Enter");
await expect(page.locator(".notification-danger")).toHaveText(
"User not found"
);
});
test("Tries to login with valid credentials, confirmed account but no profile", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#email").fill("confirmed@email.com");
await page.locator("#password").fill("some password");
await page.keyboard.press("Enter");
await page.waitForURL("/register/profile/confirmed@email.com/true");
expect(page.url()).toContain("/register/profile/confirmed@email.com/true");
await expect(page.locator("p.prose").first()).toHaveText(
"Now, create your first profile:"
);
const displayNameField = page.locator("form > .field").first();
await expect(displayNameField.locator("label")).toHaveText(
"Displayed nickname"
);
const displayNameInput = displayNameField.locator("input");
await displayNameInput.fill("Duplicate");
const usernameField = page.locator("form > .field").nth(1);
await expect(usernameField.locator("label")).toHaveText("Username");
const usernameFieldInput = usernameField.locator("input");
await usernameFieldInput.fill("test_user");
const descriptionField = page.locator("form > .field").nth(2);
await expect(descriptionField.locator("label")).toHaveText("Short bio");
await descriptionField
.locator("textarea")
.fill("This shouln't work because it's using a dupublicated username");
const submitButton = page.locator('button[type="submit"]', {
hasText: "Create my profile",
});
await submitButton.click();
await expect(page.locator("p.field-message-danger")).toHaveText(
"This username is already taken."
);
await displayNameInput.fill("");
await displayNameInput.fill("Not");
await usernameFieldInput.fill("");
await usernameFieldInput.fill("test_user_2");
await submitButton.click();
// cy.get("form .field input").first(0).clear().type("test_user_2");
// cy.get("form .field input").eq(1).type("Not");
// cy.get("form .field textarea").clear().type("This will now work");
// cy.get("form").submit();
// cy.get(".navbar-link span.icon i").should(
// "have.class",
// "mdi-account-circle"
// );
await page.waitForURL("/");
expect(page.url()).toContain("/");
await expect(page.locator(".notification-info")).toHaveText(
"Welcome to Mobilizon, Not!"
);
await expect(
page.locator("button#user-menu-button span:not(.sr-only)")
).toHaveClass("material-design-icon account-circle-icon");
}); });

View File

@ -30,7 +30,7 @@ exports[`PostListItem > renders post list item with tags 1`] = `
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\"> <div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3> <h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-6ca7cc69=\\"\\">Dec 2, 2020</span></p> <p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
<div class=\\"flex flex-wrap gap-y-0 gap-x-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon tag-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z\\"><!--v-if--></path></svg></span><span class=\\"rounded-md my-1 truncate text-sm text-violet-title px-2 py-1 bg-purple-3 dark:text-violet-3\\" data-v-6955ca87=\\"\\" data-v-6ca7cc69=\\"\\">A tag</span></div> <div class=\\"flex flex-wrap gap-y-0 gap-x-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon tag-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z\\"><!--v-if--></path></svg></span><span class=\\"rounded-md truncate text-sm text-violet-title px-2 py-1 bg-purple-3 dark:text-violet-3\\" data-v-6955ca87=\\"\\" data-v-6ca7cc69=\\"\\">A tag</span></div>
<!--v-if--> <!--v-if-->
</div> </div>
</a>" </a>"

View File

@ -1,6 +1,6 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.{Actors, Instances}
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay} alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
@ -204,7 +204,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional) def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
when type != :Person do when type != :Person do
case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do
{:ok, %Follower{} = follower} -> {:ok, %Follower{actor: %Actor{type: actor_type}} = follower} ->
# We refresh the instance materialized view to make sure the instance page will be available
# when the admin clicks on the email link and access it
if actor_type == :Application do
Instances.refresh()
end
FollowMailer.send_notification_to_admins(follower) FollowMailer.send_notification_to_admins(follower)
follower_as_data = Convertible.model_to_as(follower) follower_as_data = Convertible.model_to_as(follower)
approve_if_manually_approves_followers(follower, follower_as_data) approve_if_manually_approves_followers(follower, follower_as_data)

View File

@ -62,7 +62,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
Map.merge(map, %{actor: actor, id: id, inserted_at: inserted_at}) Map.merge(map, %{actor: actor, id: id, inserted_at: inserted_at})
end end
@spec transform_action_log(module(), atom(), ActionLog.t()) :: map() @spec transform_action_log(module(), atom(), ActionLog.t()) :: map() | nil
defp transform_action_log( defp transform_action_log(
Report, Report,
:update, :update,
@ -132,6 +132,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
} }
end end
defp transform_action_log(_, _, _), do: nil
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct # Changes are stored as %{"key" => "value"} so we need to convert them back as struct
@spec convert_changes_to_struct(module(), map()) :: struct() @spec convert_changes_to_struct(module(), map()) :: struct()
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
@ -464,6 +466,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, :unauthenticated} {:error, :unauthenticated}
end end
@spec get_instance(any, map(), Absinthe.Resolution.t()) ::
{:error, :unauthenticated | :unauthorized | :not_found}
| {:ok, Mobilizon.Instances.Instance.t()}
def get_instance(_parent, %{domain: domain}, %{ def get_instance(_parent, %{domain: domain}, %{
context: %{current_user: %User{role: role}} context: %{current_user: %User{role: role}}
}) })
@ -482,7 +487,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
followed_status: follow_status(local_relay, remote_relay) followed_status: follow_status(local_relay, remote_relay)
} }
{:ok, Map.merge(Instances.instance(domain), result)} case Instances.instance(domain) do
nil -> {:error, :not_found}
instance -> {:ok, Map.merge(instance, result)}
end
end end
def get_instance(_parent, _args, %{context: %{current_user: %User{}}}) do def get_instance(_parent, _args, %{context: %{current_user: %User{}}}) do

View File

@ -183,7 +183,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
stats.participant + stats.moderator + stats.administrator + stats.creator stats.participant + stats.moderator + stats.administrator + stats.creator
)} )}
else else
{:ok, %{participant: stats.participant}} {:ok, %EventParticipantStats{participant: stats.participant}}
end end
end end

View File

@ -25,6 +25,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
field(:category, :event_category, description: "The event's category") field(:category, :event_category, description: "The event's category")
field(:options, :event_options, description: "The event options") field(:options, :event_options, description: "The event options")
field(:participant_stats, :participant_stats,
description: "Statistics on the event's participants"
)
resolve_type(fn resolve_type(fn
%Event{}, _ -> %Event{}, _ ->
:event :event
@ -54,6 +58,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
field(:tags, list_of(:tag), description: "The event's tags") field(:tags, list_of(:tag), description: "The event's tags")
field(:category, :event_category, description: "The event's category") field(:category, :event_category, description: "The event's category")
field(:options, :event_options, description: "The event options") field(:options, :event_options, description: "The event options")
field(:participant_stats, :participant_stats,
description: "Statistics on the event's participants"
)
end end
interface :group_search_result do interface :group_search_result do
@ -152,6 +160,19 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
value(:global, description: "Search using the global fediverse search") value(:global, description: "Search using the global fediverse search")
end end
enum :search_group_sort_options do
value(:match_desc, description: "The pertinence of the result")
value(:member_count_desc, description: "The members count of the group")
end
enum :search_event_sort_options do
value(:match_desc, description: "The pertinence of the result")
value(:start_time_desc, description: "The start date of the result")
value(:created_at_desc, description: "When the event was published")
value(:created_at_asc, description: "When the event was published")
value(:participant_count_desc, description: "With the most participants")
end
object :search_queries do object :search_queries do
@desc "Search persons" @desc "Search persons"
field :search_persons, :persons do field :search_persons, :persons do
@ -184,6 +205,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "The list of languages this event can be in" description: "The list of languages this event can be in"
) )
arg(:boost_languages, list_of(:string),
description: "The user's languages that can benefit from a boost in search results"
)
arg(:search_target, :search_target, arg(:search_target, :search_target,
default_value: :internal, default_value: :internal,
description: "The target of the search (internal or global)" description: "The target of the search (internal or global)"
@ -195,6 +220,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:page, :integer, default_value: 1, description: "Result page") arg(:page, :integer, default_value: 1, description: "Result page")
arg(:limit, :integer, default_value: 10, description: "Results limit per page") arg(:limit, :integer, default_value: 10, description: "Results limit per page")
arg(:sort_by, :search_group_sort_options,
default_value: :match_desc,
description: "How to sort search results"
)
resolve(&Search.search_groups/3) resolve(&Search.search_groups/3)
end end
@ -218,6 +248,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "The list of languages this event can be in" description: "The list of languages this event can be in"
) )
arg(:boost_languages, list_of(:string),
description: "The user's languages that can benefit from a boost in search results"
)
arg(:search_target, :search_target, arg(:search_target, :search_target,
default_value: :internal, default_value: :internal,
description: "The target of the search (internal or global)" description: "The target of the search (internal or global)"
@ -236,6 +270,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:begins_on, :datetime, description: "Filter events by their start date") arg(:begins_on, :datetime, description: "Filter events by their start date")
arg(:ends_on, :datetime, description: "Filter events by their end date") arg(:ends_on, :datetime, description: "Filter events by their end date")
arg(:sort_by, :search_event_sort_options,
default_value: :match_desc,
description: "How to sort search results"
)
resolve(&Search.search_events/3) resolve(&Search.search_events/3)
end end

View File

@ -541,7 +541,7 @@ defmodule Mobilizon.Events do
|> filter_draft() |> filter_draft()
|> filter_local_or_from_followed_instances_events() |> filter_local_or_from_followed_instances_events()
|> filter_public_visibility() |> filter_public_visibility()
|> event_order_begins_on_asc() |> event_order(Map.get(args, :sort_by, :match_desc))
|> Page.build_page(page, limit, :begins_on) |> Page.build_page(page, limit, :begins_on)
end end
@ -1272,15 +1272,14 @@ defmodule Mobilizon.Events do
end end
end end
@spec events_for_search_query(String.t()) :: Ecto.Query.t() # @spec events_for_search_query(String.t()) :: Ecto.Query.t()
defp events_for_search_query("") do # defp events_for_search_query("") do
Event # Event
|> distinct([e], asc: e.begins_on, asc: e.id) # |> join: rank in fragment("")
end # end
defp events_for_search_query(search_string) do defp events_for_search_query(search_string) do
from(event in Event, from(event in Event,
distinct: [asc: event.begins_on, asc: event.id],
join: id_and_rank in matching_event_ids_and_ranks(search_string), join: id_and_rank in matching_event_ids_and_ranks(search_string),
on: id_and_rank.id == event.id on: id_and_rank.id == event.id
) )
@ -1820,6 +1819,18 @@ defmodule Mobilizon.Events do
|> event_order_begins_on_asc() |> event_order_begins_on_asc()
end end
defp event_order(query, :match_desc),
do: order_by(query, [e, f], desc: f.rank, asc: e.begins_on)
defp event_order(query, :start_time_desc), do: order_by(query, [e], asc: e.begins_on)
defp event_order(query, :created_at_desc), do: order_by(query, [e], desc: e.publish_at)
defp event_order(query, :created_at_asc), do: order_by(query, [e], asc: e.publish_at)
defp event_order(query, :participant_count_desc),
do: order_by(query, [e], fragment("participant_stats->>'participant' DESC"))
defp event_order(query, _), do: query
defp event_order_begins_on_asc(query), defp event_order_begins_on_asc(query),
do: order_by(query, [e], asc: e.begins_on) do: order_by(query, [e], asc: e.begins_on)

View File

@ -67,7 +67,7 @@ defmodule Mobilizon.Instances do
} }
end end
@spec instance(String.t()) :: Instance.t() @spec instance(String.t()) :: Instance.t() | nil
def instance(domain) do def instance(domain) do
Instance Instance
|> where(domain: ^domain) |> where(domain: ^domain)

View File

@ -13,7 +13,7 @@ defmodule Mobilizon.Service.GlobalSearch.EventResult do
:category, :category,
:tags, :tags,
:organizer_actor, :organizer_actor,
:participants, :participant_stats,
:physical_address :physical_address
] ]
end end

View File

@ -15,6 +15,15 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
@search_events_api "/api/v1/search/events" @search_events_api "/api/v1/search/events"
@search_groups_api "/api/v1/search/groups" @search_groups_api "/api/v1/search/groups"
@sort_by_options %{
match_desc: "-match",
start_time_desc: "-startTime",
created_at_desc: "-createdAt",
created_at_asc: "createdAt",
participant_count_desc: "-participantCount",
member_count_desc: "-memberCount"
}
@behaviour Provider @behaviour Provider
@impl Provider @impl Provider
@ -39,9 +48,11 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
end), end),
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil), distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit], count: options[:limit],
start: (options[:page] - 1) * options[:limit], start: (Keyword.get(options, :page, 1) - 1) * Keyword.get(options, :limit, 16),
latlon: to_lat_lon(options[:location]), latlon: to_lat_lon(options[:location]),
bbox: options[:bbox] bbox: options[:bbox],
sortBy: Map.get(@sort_by_options, options[:sort_by]),
boostLanguages: options[:boost_languages]
) )
|> Keyword.take([ |> Keyword.take([
:search, :search,
@ -56,7 +67,8 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
:statusOneOf, :statusOneOf,
:bbox, :bbox,
:start, :start,
:count :count,
:sortBy
]) ])
|> Keyword.reject(fn {_key, val} -> is_nil(val) end) |> Keyword.reject(fn {_key, val} -> is_nil(val) end)
@ -85,21 +97,25 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
|> Keyword.merge( |> Keyword.merge(
term: options[:search], term: options[:search],
languageOneOf: options[:language_one_of], languageOneOf: options[:language_one_of],
boostLanguages: options[:boost_languages],
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil), distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit], count: options[:limit],
start: (options[:page] - 1) * options[:limit], start: (options[:page] - 1) * options[:limit],
latlon: to_lat_lon(options[:location]), latlon: to_lat_lon(options[:location]),
bbox: options[:bbox] bbox: options[:bbox],
sortBy: Map.get(@sort_by_options, options[:sort_by])
) )
|> Keyword.take([ |> Keyword.take([
:search, :search,
:languageOneOf,
:boostLanguages, :boostLanguages,
:latlon, :latlon,
:distance, :distance,
:sort, :sort,
:start, :start,
:count, :count,
:bbox :bbox,
:sortBy
]) ])
|> Keyword.reject(fn {_key, val} -> is_nil(val) end) |> Keyword.reject(fn {_key, val} -> is_nil(val) end)
@ -179,6 +195,7 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
avatar: organizer_actor_avatar avatar: organizer_actor_avatar
}, },
physical_address: address, physical_address: address,
participant_stats: %{participant: data["participantCount"]},
tags: tags:
Enum.map(data["tags"], fn tag -> Enum.map(data["tags"], fn tag ->
tag = String.trim_leading(tag, "#") tag = String.trim_leading(tag, "#")

View File

@ -3,7 +3,7 @@ defmodule EndToEndSeed do
def delete_user(email) do def delete_user(email) do
with {:ok, user} <- Users.get_user_by_email(email) do with {:ok, user} <- Users.get_user_by_email(email) do
Users.delete_user(user) Users.delete_user(user, reserve_email: false)
end end
end end
end end