forked from potsda.mn/mobilizon
f0c8fa2525
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
1929 lines
56 KiB
Elixir
1929 lines
56 KiB
Elixir
defmodule Mobilizon.Events do
|
|
@moduledoc """
|
|
The Events context.
|
|
"""
|
|
|
|
import Geo.PostGIS
|
|
|
|
import Ecto.Query
|
|
import EctoEnum
|
|
|
|
import Mobilizon.Service.Guards
|
|
import Mobilizon.Storage.Ecto
|
|
import Mobilizon.Events.Utils, only: [calculate_notification_time: 1]
|
|
|
|
alias Ecto.{Changeset, Multi}
|
|
|
|
alias Mobilizon.Actors.{Actor, Follower}
|
|
alias Mobilizon.Addresses.Address
|
|
|
|
alias Mobilizon.Events.{
|
|
Event,
|
|
EventParticipantStats,
|
|
FeedToken,
|
|
Participant,
|
|
Session,
|
|
Tag,
|
|
TagRelation,
|
|
Track
|
|
}
|
|
|
|
alias Mobilizon.Federation.ActivityPub.Relay
|
|
alias Mobilizon.Service.Export.Cachable
|
|
alias Mobilizon.Service.Workers.BuildSearch
|
|
alias Mobilizon.Service.Workers.EventDelayedNotificationWorker
|
|
alias Mobilizon.Share
|
|
alias Mobilizon.Storage.{Page, Repo}
|
|
alias Mobilizon.Users.{Setting, User}
|
|
|
|
defenum(EventVisibility, :event_visibility, [
|
|
:public,
|
|
:unlisted,
|
|
:restricted,
|
|
:private
|
|
])
|
|
|
|
defenum(JoinOptions, :join_options, [
|
|
:free,
|
|
:restricted,
|
|
:invite
|
|
])
|
|
|
|
defenum(EventStatus, :event_status, [
|
|
:tentative,
|
|
:confirmed,
|
|
:cancelled
|
|
])
|
|
|
|
defenum(ParticipantRole, :participant_role, [
|
|
:not_approved,
|
|
:not_confirmed,
|
|
:rejected,
|
|
:participant,
|
|
:moderator,
|
|
:administrator,
|
|
:creator
|
|
])
|
|
|
|
@public_visibility [:public, :unlisted]
|
|
|
|
@event_preloads [
|
|
:organizer_actor,
|
|
:attributed_to,
|
|
:mentions,
|
|
:sessions,
|
|
:tracks,
|
|
:tags,
|
|
:comments,
|
|
:participants,
|
|
:physical_address,
|
|
:picture,
|
|
:contacts,
|
|
:media
|
|
]
|
|
|
|
@participant_preloads [:event, :actor]
|
|
|
|
@doc """
|
|
Gets a single event.
|
|
"""
|
|
@spec get_event(integer | String.t()) :: {:ok, Event.t()} | {:error, :event_not_found}
|
|
def get_event(id) do
|
|
case Repo.get(Event, id) do
|
|
%Event{} = event ->
|
|
{:ok, event}
|
|
|
|
nil ->
|
|
{:error, :event_not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a single event.
|
|
Raises `Ecto.NoResultsError` if the event does not exist.
|
|
"""
|
|
@spec get_event!(integer | String.t()) :: Event.t()
|
|
def get_event!(id), do: Repo.get!(Event, id)
|
|
|
|
@doc """
|
|
Gets a single event, with all associations loaded.
|
|
"""
|
|
@spec get_event_with_preload(integer | String.t()) ::
|
|
{:ok, Event.t()} | {:error, :event_not_found}
|
|
def get_event_with_preload(id) do
|
|
case Repo.get(Event, id) do
|
|
%Event{} = event ->
|
|
{:ok, Repo.preload(event, @event_preloads)}
|
|
|
|
nil ->
|
|
{:error, :event_not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a single event, with all associations loaded.
|
|
Raises `Ecto.NoResultsError` if the event does not exist.
|
|
"""
|
|
@spec get_event_with_preload!(integer | String.t()) :: Event.t()
|
|
def get_event_with_preload!(id) do
|
|
Event
|
|
|> Repo.get!(id)
|
|
|> Repo.preload(@event_preloads)
|
|
end
|
|
|
|
@doc """
|
|
Gets an event by its URL.
|
|
"""
|
|
@spec get_event_by_url(String.t()) :: Event.t() | nil
|
|
def get_event_by_url(url) do
|
|
url
|
|
|> event_by_url_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets an event by its URL.
|
|
Raises `Ecto.NoResultsError` if the event does not exist.
|
|
"""
|
|
@spec get_event_by_url!(String.t()) :: Event.t()
|
|
def get_event_by_url!(url) do
|
|
url
|
|
|> event_by_url_query()
|
|
|> preload_for_event()
|
|
|> Repo.one!()
|
|
end
|
|
|
|
@doc """
|
|
Gets an event by its URL, with all associations loaded.
|
|
Raises `Ecto.NoResultsError` if the event does not exist.
|
|
"""
|
|
@spec get_public_event_by_url_with_preload!(String.t()) :: Event.t()
|
|
def get_public_event_by_url_with_preload!(url) do
|
|
url
|
|
|> event_by_url_query()
|
|
|> filter_unlisted_and_public_visibility()
|
|
|> filter_draft()
|
|
|> preload_for_event()
|
|
|> Repo.one!()
|
|
end
|
|
|
|
@doc """
|
|
Gets a public event by its UUID, with all associations loaded.
|
|
"""
|
|
@spec get_public_event_by_uuid_with_preload(String.t()) :: Event.t() | nil
|
|
def get_public_event_by_uuid_with_preload(uuid) do
|
|
uuid
|
|
|> event_by_uuid_query()
|
|
|> filter_unlisted_and_public_visibility()
|
|
|> filter_draft()
|
|
|> preload_for_event()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@spec check_if_event_has_instance_follow(String.t(), integer()) :: boolean()
|
|
def check_if_event_has_instance_follow(event_uri, follower_actor_id) do
|
|
Share
|
|
|> join(:inner, [s], f in Follower, on: f.target_actor_id == s.actor_id)
|
|
|> where([s, f], f.actor_id == ^follower_actor_id and s.uri == ^event_uri)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
@doc """
|
|
Gets an event by its UUID, with all associations loaded.
|
|
"""
|
|
@spec get_event_by_uuid_with_preload(String.t()) :: Event.t() | nil
|
|
def get_event_by_uuid_with_preload(uuid) do
|
|
uuid
|
|
|> event_by_uuid_query()
|
|
|> preload_for_event()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets an actor's eventual upcoming public event.
|
|
"""
|
|
@spec get_upcoming_public_event_for_actor(Actor.t(), String.t() | nil) :: Event.t() | nil
|
|
def get_upcoming_public_event_for_actor(%Actor{id: actor_id}, not_event_uuid \\ nil) do
|
|
actor_id
|
|
|> upcoming_public_event_for_actor_query()
|
|
|> filter_public_visibility()
|
|
|> filter_not_event_uuid(not_event_uuid)
|
|
|> filter_draft()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Creates an event.
|
|
"""
|
|
@spec create_event(map) ::
|
|
{:ok, Event.t()}
|
|
| {:error, Changeset.t()}
|
|
| {:error, :update | :write, Changeset.t(), map()}
|
|
def create_event(attrs \\ %{}) do
|
|
with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs),
|
|
%Event{} = event <- Repo.preload(event, @event_preloads) do
|
|
unless event.draft do
|
|
BuildSearch.enqueue(:insert_search_event, %{"event_id" => event.id})
|
|
end
|
|
|
|
{:ok, event}
|
|
else
|
|
err -> err
|
|
end
|
|
end
|
|
|
|
# We start by inserting the event and then insert a first participant if the event is not a draft
|
|
@spec do_create_event(map) ::
|
|
{:ok, %{insert: Event.t(), write: Participant.t() | nil}}
|
|
| {:error, Changeset.t()}
|
|
| {:error, :update | :write, Changeset.t(), map()}
|
|
defp do_create_event(attrs) do
|
|
Multi.new()
|
|
|> Multi.insert(:insert, Event.changeset(%Event{}, attrs))
|
|
|> Multi.run(:write, fn _repo, %{insert: %Event{draft: draft} = event} ->
|
|
with {:is_draft, false} <- {:is_draft, draft},
|
|
{:ok, %Participant{} = participant} <-
|
|
create_participant(
|
|
%{
|
|
event_id: event.id,
|
|
role: :creator,
|
|
actor_id: event.organizer_actor_id
|
|
},
|
|
false
|
|
) do
|
|
{:ok, participant}
|
|
else
|
|
{:is_draft, true} -> {:ok, nil}
|
|
err -> err
|
|
end
|
|
end)
|
|
|> Repo.transaction()
|
|
end
|
|
|
|
@doc """
|
|
Updates an event.
|
|
|
|
We start by updating the event and then insert a first participant if the event is not a draft anymore
|
|
"""
|
|
@spec update_event(Event.t(), map) ::
|
|
{:ok, Event.t()}
|
|
| {:error, Changeset.t()}
|
|
| {:error, :update | :write, Changeset.t(), map()}
|
|
def update_event(%Event{draft: old_draft} = old_event, attrs) do
|
|
with %Event{} = old_event <- Repo.preload(old_event, @event_preloads),
|
|
%Changeset{changes: changes} = changeset <-
|
|
Event.update_changeset(old_event, attrs),
|
|
{:ok, %{update: %Event{} = new_event}} <-
|
|
Multi.new()
|
|
|> Multi.update(:update, changeset)
|
|
|> Multi.run(:write, fn _repo, %{update: %Event{draft: draft} = event} ->
|
|
with {:was_draft, true} <- {:was_draft, old_draft == true && draft == false},
|
|
{:ok, %Participant{} = participant} <-
|
|
create_participant(
|
|
%{
|
|
event_id: event.id,
|
|
role: :creator,
|
|
actor_id: event.organizer_actor_id
|
|
},
|
|
false
|
|
) do
|
|
{:ok, participant}
|
|
else
|
|
{:was_draft, false} -> {:ok, nil}
|
|
err -> err
|
|
end
|
|
end)
|
|
|> Repo.transaction(),
|
|
%Event{} = new_event <- Repo.preload(new_event, @event_preloads, force: true) do
|
|
Cachable.clear_all_caches(new_event)
|
|
|
|
unless new_event.draft do
|
|
%{
|
|
action: :notify_of_event_update,
|
|
event_uuid: new_event.uuid,
|
|
old_event: old_event |> Map.from_struct() |> Map.take(Event.__schema__(:fields)),
|
|
changes: build_changes(changes)
|
|
}
|
|
|> EventDelayedNotificationWorker.new(
|
|
scheduled_at: calculate_notification_time(new_event.begins_on),
|
|
replace: [:scheduled_at, :args],
|
|
unique: [period: 1, keys: [:event_uuid, :action]]
|
|
)
|
|
|> Oban.insert()
|
|
|
|
BuildSearch.enqueue(:update_search_event, %{"event_id" => new_event.id})
|
|
end
|
|
|
|
{:ok, new_event}
|
|
end
|
|
end
|
|
|
|
@spec build_changes(map()) :: map()
|
|
defp build_changes(changes) do
|
|
changes
|
|
|> maybe_add_field(:physical_address)
|
|
|> maybe_add_field(:options)
|
|
|> maybe_add_field(:metadata)
|
|
|> Map.drop(Event.__schema__(:associations) -- [:physical_address, :options, :metadata])
|
|
end
|
|
|
|
@spec maybe_add_field(map(), atom()) :: map()
|
|
defp maybe_add_field(changes, field) do
|
|
case Map.get(changes, field) do
|
|
%Ecto.Changeset{} = changeset ->
|
|
Map.put(
|
|
changes,
|
|
field,
|
|
changeset
|
|
|> Ecto.Changeset.apply_changes()
|
|
|> Map.from_struct()
|
|
|> Map.take(Address.__schema__(:fields))
|
|
)
|
|
|
|
_ ->
|
|
Map.drop(changes, [field])
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes an event.
|
|
"""
|
|
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Changeset.t()}
|
|
def delete_event(%Event{} = event) do
|
|
Cachable.clear_all_caches(event)
|
|
Repo.delete(event)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of events.
|
|
"""
|
|
@spec list_events(integer | nil, integer | nil, atom, atom, boolean) :: Page.t(Event.t())
|
|
def list_events(
|
|
page \\ nil,
|
|
limit \\ nil,
|
|
sort \\ :begins_on,
|
|
direction \\ :asc,
|
|
is_future \\ true
|
|
) do
|
|
Event
|
|
|> distinct([e], [{^direction, ^sort}, asc: e.id])
|
|
|> preload([:organizer_actor, :participants])
|
|
|> sort(sort, direction)
|
|
|> filter_future_events(is_future)
|
|
|> filter_public_visibility()
|
|
|> filter_draft()
|
|
|> filter_cancelled_events()
|
|
|> filter_local_or_from_followed_instances_events()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@spec stream_events_for_sitemap :: Enum.t()
|
|
def stream_events_for_sitemap do
|
|
Event
|
|
|> filter_public_visibility()
|
|
|> filter_draft()
|
|
|> filter_local()
|
|
|> Repo.stream()
|
|
end
|
|
|
|
@spec list_public_local_events(integer | nil, integer | nil) :: Page.t(Event.t())
|
|
def list_public_local_events(page \\ nil, limit \\ nil) do
|
|
Event
|
|
|> filter_public_visibility()
|
|
|> filter_draft()
|
|
|> filter_local()
|
|
|> preload_for_event()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of events with the same tags.
|
|
"""
|
|
@spec list_events_by_tags([Tag.t()], integer) :: [Event.t()]
|
|
def list_events_by_tags(tags, limit \\ 2) do
|
|
tags
|
|
|> Enum.map(& &1.id)
|
|
|> events_by_tags_query(limit)
|
|
|> filter_draft()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists public events for the actor, with all associations loaded.
|
|
"""
|
|
@spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t(Event.t())
|
|
def list_public_events_for_actor(actor, page \\ nil, limit \\ nil)
|
|
|
|
def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit),
|
|
do: list_organized_events_for_group(group, :public, nil, nil, page, limit)
|
|
|
|
def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do
|
|
actor_id
|
|
|> event_for_actor_query()
|
|
|> filter_public_visibility()
|
|
|> filter_draft()
|
|
|> preload_for_event()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
|
Page.t(Event.t())
|
|
def list_organized_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
|
actor_id
|
|
|> event_for_actor_query(desc: :begins_on)
|
|
|> preload_for_event()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@spec list_simple_organized_events_for_group(Actor.t(), integer | nil, integer | nil) ::
|
|
Page.t(Event.t())
|
|
def list_simple_organized_events_for_group(%Actor{} = actor, page \\ nil, limit \\ nil) do
|
|
list_organized_events_for_group(actor, :all, nil, nil, page, limit)
|
|
end
|
|
|
|
@spec list_organized_events_for_group(
|
|
Actor.t(),
|
|
atom(),
|
|
DateTime.t() | nil,
|
|
DateTime.t() | nil,
|
|
integer | nil,
|
|
integer | nil
|
|
) :: Page.t(Event.t())
|
|
def list_organized_events_for_group(
|
|
%Actor{id: group_id},
|
|
visibility \\ :public,
|
|
after_datetime \\ nil,
|
|
before_datetime \\ nil,
|
|
page \\ nil,
|
|
limit \\ nil
|
|
) do
|
|
group_id
|
|
|> event_for_group_query()
|
|
|> event_filter_visibility(visibility)
|
|
|> event_filter_begins_on(after_datetime, before_datetime)
|
|
|> preload_for_event()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@spec list_drafts_for_user(integer, integer | nil, integer | nil) :: [Event.t()]
|
|
def list_drafts_for_user(user_id, page \\ nil, limit \\ nil) do
|
|
Event
|
|
|> user_events_query(user_id)
|
|
|> filter_draft(true)
|
|
|> Page.paginate(page, limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec is_user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
|
|
def is_user_moderator_for_event?(user_id, event_id) do
|
|
Participant
|
|
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
|
|> where([p, _a], p.event_id == ^event_id)
|
|
|> where([_p, a], a.user_id == ^user_id)
|
|
|> where([p, _a], p.role == ^:creator)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
@doc """
|
|
Finds close events to coordinates.
|
|
Radius is in meters and defaults to 50km.
|
|
"""
|
|
@spec find_close_events(float(), float(), non_neg_integer()) :: [Event.t()]
|
|
def find_close_events(lon, lat, radius \\ 50_000) do
|
|
"SRID=#{4326};POINT(#{lon} #{lat})"
|
|
|> Geo.WKT.decode!()
|
|
|> close_events_query(radius)
|
|
|> filter_draft()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Counts local events.
|
|
"""
|
|
@spec count_local_events :: integer
|
|
def count_local_events do
|
|
count_local_events_query()
|
|
|> filter_unlisted_and_public_visibility()
|
|
|> filter_draft()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Counts all events.
|
|
"""
|
|
@spec count_events :: integer
|
|
def count_events do
|
|
count_events_query()
|
|
|> filter_unlisted_and_public_visibility()
|
|
|> filter_draft()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Builds a page struct for events by their name.
|
|
"""
|
|
@spec build_events_for_search(map(), integer | nil, integer | nil) :: Page.t(Event.t())
|
|
def build_events_for_search(%{term: term} = args, page \\ nil, limit \\ nil) do
|
|
term
|
|
|> normalize_search_string()
|
|
|> events_for_search_query()
|
|
|> events_for_begins_on(args)
|
|
|> events_for_ends_on(args)
|
|
|> events_for_category(args)
|
|
|> events_for_categories(args)
|
|
|> events_for_languages(args)
|
|
|> events_for_statuses(args)
|
|
|> events_for_tags(args)
|
|
|> maybe_join_address(args)
|
|
|> events_for_location(args)
|
|
|> events_for_bounding_box(args)
|
|
|> filter_online(args)
|
|
|> filter_draft()
|
|
|> filter_local_or_from_followed_instances_events()
|
|
|> filter_public_visibility()
|
|
|> event_order(Map.get(args, :sort_by, :match_desc))
|
|
|> Page.build_page(page, limit, :begins_on)
|
|
end
|
|
|
|
@doc """
|
|
Gets a single tag.
|
|
"""
|
|
@spec get_tag(integer | String.t()) :: Tag.t() | nil
|
|
def get_tag(id), do: Repo.get(Tag, id)
|
|
|
|
@doc """
|
|
Gets a single tag.
|
|
Raises `Ecto.NoResultsError` if the tag does not exist.
|
|
"""
|
|
@spec get_tag!(integer | String.t()) :: Tag.t()
|
|
def get_tag!(id), do: Repo.get!(Tag, id)
|
|
|
|
@doc """
|
|
Gets a tag by its slug.
|
|
"""
|
|
@spec get_tag_by_slug(String.t()) :: Tag.t() | nil
|
|
def get_tag_by_slug(slug) do
|
|
slug
|
|
|> tag_by_slug_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets a tag by its title.
|
|
"""
|
|
@spec get_tag_by_title(String.t()) :: Tag.t() | nil
|
|
def get_tag_by_title(slug) do
|
|
slug
|
|
|> tag_by_title_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Creates a tag.
|
|
"""
|
|
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
|
|
def create_tag(attrs \\ %{}) do
|
|
%Tag{}
|
|
|> Tag.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates a tag.
|
|
"""
|
|
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
|
|
def update_tag(%Tag{} = tag, attrs) do
|
|
tag
|
|
|> Tag.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a tag.
|
|
"""
|
|
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
|
|
def delete_tag(%Tag{} = tag), do: Repo.delete(tag)
|
|
|
|
@doc """
|
|
Returns the list of tags.
|
|
"""
|
|
@spec list_tags(String.t() | nil, integer | nil, integer | nil) :: Page.t(Tag.t())
|
|
def list_tags(filter \\ nil, page \\ nil, limit \\ nil) do
|
|
Tag
|
|
|> tag_filter(filter)
|
|
|> Page.paginate(page, limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of tags for the event.
|
|
"""
|
|
@spec list_tags_for_event(integer | String.t(), integer | nil, integer | nil) :: [Tag.t()]
|
|
def list_tags_for_event(event_id, page \\ nil, limit \\ nil) do
|
|
event_id
|
|
|> tags_for_event_query()
|
|
|> Page.paginate(page, limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Checks whether two tags are linked or not.
|
|
"""
|
|
@spec are_tags_linked?(Tag.t(), Tag.t()) :: boolean
|
|
def are_tags_linked?(%Tag{id: tag1_id}, %Tag{id: tag2_id}) do
|
|
tag_relation =
|
|
tag1_id
|
|
|> tags_linked_query(tag2_id)
|
|
|> Repo.one()
|
|
|
|
!!tag_relation
|
|
end
|
|
|
|
@doc """
|
|
Creates a relation between two tags.
|
|
"""
|
|
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Changeset.t()}
|
|
def create_tag_relation(attrs) do
|
|
%TagRelation{}
|
|
|> TagRelation.changeset(attrs)
|
|
|> Repo.insert(
|
|
conflict_target: [:tag_id, :link_id],
|
|
on_conflict: [inc: [weight: 1]]
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Removes a tag relation.
|
|
"""
|
|
@spec delete_tag_relation(TagRelation.t()) ::
|
|
{:ok, TagRelation.t()} | {:error, Changeset.t()}
|
|
def delete_tag_relation(%TagRelation{} = tag_relation) do
|
|
Repo.delete(tag_relation)
|
|
end
|
|
|
|
@doc """
|
|
Returns the tags neighbors for a given tag
|
|
|
|
We can't rely on the single many_to_many relation since we also want tags that
|
|
link to our tag, not just tags linked by this one.
|
|
|
|
The SQL query looks like this:
|
|
```sql
|
|
SELECT * FROM tags t
|
|
RIGHT JOIN (
|
|
SELECT weight, link_id AS id
|
|
FROM tag_relations t2
|
|
WHERE tag_id = 1
|
|
UNION ALL
|
|
SELECT tag_id AS id, weight
|
|
FROM tag_relations t2
|
|
WHERE link_id = 1
|
|
) tr
|
|
ON t.id = tr.id
|
|
ORDER BY tr.weight
|
|
DESC;
|
|
```
|
|
"""
|
|
@spec list_tag_neighbors(Tag.t(), integer, integer) :: [Tag.t()]
|
|
def list_tag_neighbors(%Tag{id: tag_id}, relation_minimum \\ 1, limit \\ 10) do
|
|
tag_id
|
|
|> tag_relation_subquery()
|
|
|> tag_relation_union_subquery(tag_id)
|
|
|> tag_neighbors_query(relation_minimum, limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a single participant.
|
|
|
|
## Examples
|
|
|
|
iex> get_participant(123)
|
|
%Participant{}
|
|
|
|
iex> get_participant(456)
|
|
nil
|
|
|
|
"""
|
|
@spec get_participant(integer) :: Participant.t() | nil
|
|
@spec get_participant(integer | String.t(), integer | String.t(), map()) ::
|
|
{:ok, Participant.t()} | {:error, :participant_not_found}
|
|
def get_participant(participant_id) do
|
|
Participant
|
|
|> where([p], p.id == ^participant_id)
|
|
|> preload([p], ^@participant_preloads)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets a single participation for an event and actor.
|
|
"""
|
|
def get_participant(event_id, actor_id, params \\ %{})
|
|
|
|
# This one if to check someone doesn't go to the same event twice
|
|
def get_participant(event_id, actor_id, %{email: email}) do
|
|
case Participant
|
|
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|
|
|> where([p], fragment("? ->>'email' = ?", p.metadata, ^email))
|
|
|> preload([p], ^@participant_preloads)
|
|
|> Repo.one() do
|
|
%Participant{} = participant ->
|
|
{:ok, participant}
|
|
|
|
nil ->
|
|
{:error, :participant_not_found}
|
|
end
|
|
end
|
|
|
|
# This one if for finding participants by their cancellation token when wanting to cancel a participation
|
|
def get_participant(event_id, actor_id, %{cancellation_token: cancellation_token}) do
|
|
case Participant
|
|
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|
|
|> where([p], fragment("? ->>'cancellation_token' = ?", p.metadata, ^cancellation_token))
|
|
|> preload([p], ^@participant_preloads)
|
|
|> Repo.one() do
|
|
%Participant{} = participant ->
|
|
{:ok, participant}
|
|
|
|
nil ->
|
|
{:error, :participant_not_found}
|
|
end
|
|
end
|
|
|
|
def get_participant(event_id, actor_id, %{}) do
|
|
case Participant
|
|
|> Repo.get_by(event_id: event_id, actor_id: actor_id)
|
|
|> Repo.preload(@participant_preloads) do
|
|
%Participant{} = participant ->
|
|
{:ok, participant}
|
|
|
|
nil ->
|
|
{:error, :participant_not_found}
|
|
end
|
|
end
|
|
|
|
@spec get_participant_by_confirmation_token(String.t()) :: Participant.t() | nil
|
|
def get_participant_by_confirmation_token(confirmation_token) do
|
|
Participant
|
|
|> where([p], fragment("? ->>'confirmation_token' = ?", p.metadata, ^confirmation_token))
|
|
|> preload([p], ^@participant_preloads)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@spec get_participant!(integer | String.t()) :: Participant.t()
|
|
def get_participant!(participant_id) do
|
|
Repo.get_by!(Participant, id: participant_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets a single participation for an event and actor.
|
|
|
|
Raises `Ecto.NoResultsError` if the Participant does not exist.
|
|
|
|
## Examples
|
|
|
|
iex> get_participant!(123, 19)
|
|
%Participant{}
|
|
|
|
iex> get_participant!(456, 5)
|
|
** (Ecto.NoResultsError)
|
|
|
|
"""
|
|
@spec get_participant!(integer | String.t(), integer | String.t()) :: Participant.t()
|
|
def get_participant!(event_id, actor_id) do
|
|
Repo.get_by!(Participant, event_id: event_id, actor_id: actor_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets a participant by its URL.
|
|
"""
|
|
@spec get_participant_by_url(String.t()) :: Participant.t() | nil
|
|
def get_participant_by_url(url) do
|
|
url
|
|
|> participant_by_url_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Returns the number of participations for all local events
|
|
"""
|
|
@spec count_confirmed_participants_for_local_events :: integer()
|
|
def count_confirmed_participants_for_local_events do
|
|
count_confirmed_participants_for_local_events_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of participants for an event.
|
|
Default behaviour is to not return :not_approved or :not_confirmed participants
|
|
"""
|
|
@spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
|
|
Page.t(Participant.t())
|
|
def list_participants_for_event(
|
|
id,
|
|
roles \\ [],
|
|
page \\ nil,
|
|
limit \\ nil
|
|
) do
|
|
id
|
|
|> participants_for_event_query(roles)
|
|
|> preload([:actor, :event])
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
|
def list_actors_participants_for_event(id) do
|
|
id
|
|
|> list_participant_actors_for_event_query
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of participations for an actor.
|
|
|
|
Default behaviour is to not return :not_approved participants
|
|
|
|
## Examples
|
|
|
|
iex> list_event_participations_for_user(5)
|
|
[%Participant{}, ...]
|
|
|
|
"""
|
|
@spec list_participations_for_user(
|
|
integer,
|
|
DateTime.t() | nil,
|
|
DateTime.t() | nil,
|
|
integer | nil,
|
|
integer | nil
|
|
) :: Page.t(Participant.t())
|
|
def list_participations_for_user(user_id, after_datetime, before_datetime, page, limit) do
|
|
user_id
|
|
|> list_participations_for_user_query()
|
|
|> participation_filter_begins_on(after_datetime, before_datetime)
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of organizers participants for an event.
|
|
|
|
## Examples
|
|
|
|
iex> list_organizers_participants_for_event(id)
|
|
[%Participant{role: :creator}, ...]
|
|
"""
|
|
@spec list_organizers_participants_for_event(
|
|
integer | String.t(),
|
|
integer | nil,
|
|
integer | nil
|
|
) ::
|
|
[Participant.t()]
|
|
def list_organizers_participants_for_event(event_id, page \\ nil, limit \\ nil) do
|
|
event_id
|
|
|> organizers_participants_for_event()
|
|
|> Page.paginate(page, limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of event participation requests for an actor.
|
|
"""
|
|
@spec list_requests_for_actor(Actor.t()) :: [Participant.t()]
|
|
def list_requests_for_actor(%Actor{id: actor_id}) do
|
|
actor_id
|
|
|> requests_for_actor_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of participations for an actor.
|
|
"""
|
|
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
|
Page.t(Participant.t())
|
|
def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
|
actor_id
|
|
|> event_participations_for_actor_query()
|
|
|> Page.build_page(page, limit)
|
|
end
|
|
|
|
@doc """
|
|
Counts participant participants (participants with no extra role)
|
|
"""
|
|
@spec count_participant_participants(integer | String.t()) :: integer
|
|
def count_participant_participants(event_id) do
|
|
event_id
|
|
|> count_participants_query()
|
|
|> filter_participant_role()
|
|
|> Repo.aggregate(:count, :id)
|
|
end
|
|
|
|
@doc """
|
|
Gets the default participant role depending on the event join options.
|
|
"""
|
|
@spec get_default_participant_role(Event.t()) :: :participant | :not_approved
|
|
def get_default_participant_role(%Event{join_options: :free}), do: :participant
|
|
def get_default_participant_role(%Event{join_options: _}), do: :not_approved
|
|
|
|
@doc """
|
|
Creates a participant.
|
|
"""
|
|
@spec create_participant(map) ::
|
|
{:ok, Participant.t()}
|
|
| {:error, :participant | :update_event_participation_stats, Changeset.t(), map()}
|
|
def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
|
|
with {:ok, %{participant: %Participant{} = participant}} <-
|
|
Multi.new()
|
|
|> Multi.insert(:participant, Participant.changeset(%Participant{}, attrs))
|
|
|> Multi.run(:update_event_participation_stats, fn _repo,
|
|
%{
|
|
participant:
|
|
%Participant{role: new_role} =
|
|
participant
|
|
} ->
|
|
update_participant_stats(
|
|
participant,
|
|
nil,
|
|
new_role,
|
|
update_event_participation_stats
|
|
)
|
|
end)
|
|
|> Repo.transaction() do
|
|
{:ok, Repo.preload(participant, [:event, :actor])}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates a participant.
|
|
"""
|
|
@spec update_participant(Participant.t(), map) ::
|
|
{:ok, Participant.t()}
|
|
| {:error, :participant | :update_event_participation_stats, Changeset.t(), map()}
|
|
def update_participant(%Participant{role: old_role} = participant, attrs) do
|
|
with {:ok, %{participant: %Participant{} = participant}} <-
|
|
Multi.new()
|
|
|> Multi.update(:participant, Participant.changeset(participant, attrs))
|
|
|> Multi.run(:update_event_participation_stats, fn _repo,
|
|
%{
|
|
participant:
|
|
%Participant{role: new_role} =
|
|
participant
|
|
} ->
|
|
update_participant_stats(participant, old_role, new_role)
|
|
end)
|
|
|> Repo.transaction() do
|
|
{:ok, Repo.preload(participant, [:event, :actor])}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes a participant.
|
|
"""
|
|
@spec delete_participant(Participant.t()) ::
|
|
{:ok, %{participant: Participant.t()}}
|
|
| {:error, :participant | :update_event_participation_stats, Changeset.t(), map()}
|
|
def delete_participant(%Participant{role: old_role} = participant) do
|
|
Multi.new()
|
|
|> Multi.delete(:participant, participant)
|
|
|> Multi.run(:update_event_participation_stats, fn _repo,
|
|
%{
|
|
participant: %Participant{} = participant
|
|
} ->
|
|
update_participant_stats(participant, old_role, nil)
|
|
end)
|
|
|> Repo.transaction()
|
|
end
|
|
|
|
defp update_participant_stats(
|
|
%Participant{
|
|
event_id: event_id
|
|
} = _participant,
|
|
old_role,
|
|
new_role,
|
|
update_event_participation_stats \\ true
|
|
) do
|
|
with {:update_event_participation_stats, true} <-
|
|
{:update_event_participation_stats, update_event_participation_stats},
|
|
{:ok, %Event{} = event} <- get_event_with_preload(event_id),
|
|
%EventParticipantStats{} = participant_stats <-
|
|
Map.get(event, :participant_stats),
|
|
%EventParticipantStats{} = participant_stats <-
|
|
do_update_participant_stats(participant_stats, old_role, new_role),
|
|
{:ok, %Event{} = event} <-
|
|
event
|
|
|> Event.update_changeset(%{
|
|
participant_stats: Map.from_struct(participant_stats)
|
|
})
|
|
|> Repo.update() do
|
|
{:ok, event}
|
|
else
|
|
{:update_event_participation_stats, false} ->
|
|
{:ok, nil}
|
|
|
|
{:error, :event_not_found} ->
|
|
{:error, :event_not_found}
|
|
|
|
err ->
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
defp do_update_participant_stats(participant_stats, old_role, new_role) do
|
|
participant_stats
|
|
|> decrease_participant_stats(old_role)
|
|
|> increase_participant_stats(new_role)
|
|
end
|
|
|
|
defp increase_participant_stats(participant_stats, nil), do: participant_stats
|
|
|
|
defp increase_participant_stats(participant_stats, role),
|
|
do: Map.update(participant_stats, role, 0, &(&1 + 1))
|
|
|
|
defp decrease_participant_stats(participant_stats, nil), do: participant_stats
|
|
|
|
defp decrease_participant_stats(participant_stats, role),
|
|
do: Map.update(participant_stats, role, 0, &(&1 - 1))
|
|
|
|
@doc """
|
|
Gets a single session.
|
|
Raises `Ecto.NoResultsError` if the session does not exist.
|
|
"""
|
|
@spec get_session!(integer | String.t()) :: Session.t()
|
|
def get_session!(id), do: Repo.get!(Session, id)
|
|
|
|
@doc """
|
|
Creates a session.
|
|
"""
|
|
@spec create_session(map) :: {:ok, Session.t()} | {:error, Changeset.t()}
|
|
def create_session(attrs \\ %{}) do
|
|
%Session{}
|
|
|> Session.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates a session.
|
|
"""
|
|
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Changeset.t()}
|
|
def update_session(%Session{} = session, attrs) do
|
|
session
|
|
|> Session.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a session.
|
|
"""
|
|
@spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Changeset.t()}
|
|
def delete_session(%Session{} = session), do: Repo.delete(session)
|
|
|
|
@doc """
|
|
Returns the list of sessions.
|
|
"""
|
|
@spec list_sessions :: [Session.t()]
|
|
def list_sessions, do: Repo.all(Session)
|
|
|
|
@doc """
|
|
Returns the list of sessions for the event.
|
|
"""
|
|
@spec list_sessions_for_event(Event.t()) :: [Session.t()]
|
|
def list_sessions_for_event(%Event{id: event_id}) do
|
|
event_id
|
|
|> sessions_for_event_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a single track.
|
|
Raises `Ecto.NoResultsError` if the track does not exist.
|
|
"""
|
|
@spec get_track!(integer | String.t()) :: Track.t()
|
|
def get_track!(id), do: Repo.get!(Track, id)
|
|
|
|
@doc """
|
|
Creates a track.
|
|
"""
|
|
@spec create_track(map) :: {:ok, Track.t()} | {:error, Changeset.t()}
|
|
def create_track(attrs \\ %{}) do
|
|
%Track{}
|
|
|> Track.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates a track.
|
|
"""
|
|
@spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Changeset.t()}
|
|
def update_track(%Track{} = track, attrs) do
|
|
track
|
|
|> Track.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a track.
|
|
"""
|
|
@spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Changeset.t()}
|
|
def delete_track(%Track{} = track), do: Repo.delete(track)
|
|
|
|
@doc """
|
|
Returns the list of tracks.
|
|
"""
|
|
@spec list_tracks :: [Track.t()]
|
|
def list_tracks, do: Repo.all(Track)
|
|
|
|
@doc """
|
|
Returns the list of sessions for the track.
|
|
"""
|
|
@spec list_sessions_for_track(Track.t()) :: [Session.t()]
|
|
def list_sessions_for_track(%Track{id: track_id}) do
|
|
track_id
|
|
|> sessions_for_track_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a single feed token.
|
|
"""
|
|
@spec get_feed_token(String.t()) :: FeedToken.t() | nil
|
|
def get_feed_token(token) do
|
|
token
|
|
|> feed_token_query()
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Creates a feed token.
|
|
"""
|
|
@spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
|
|
def create_feed_token(attrs \\ %{}) do
|
|
attrs = Map.put(attrs, :token, Ecto.UUID.generate())
|
|
|
|
%FeedToken{}
|
|
|> FeedToken.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a feed token.
|
|
"""
|
|
@spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
|
|
def delete_feed_token(%FeedToken{} = feed_token), do: Repo.delete(feed_token)
|
|
|
|
@doc """
|
|
Returns the list of feed tokens for an user.
|
|
"""
|
|
@spec list_feed_tokens_for_user(User.t()) :: [FeedTokens.t()]
|
|
def list_feed_tokens_for_user(%User{id: user_id}) do
|
|
user_id
|
|
|> feed_token_for_user_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of feed tokens for an actor.
|
|
"""
|
|
@spec list_feed_tokens_for_actor(Actor.t()) :: [FeedTokens.t()]
|
|
def list_feed_tokens_for_actor(%Actor{id: actor_id, domain: nil}) do
|
|
actor_id
|
|
|> feed_token_for_actor_query()
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec event_by_url_query(String.t()) :: Ecto.Query.t()
|
|
defp event_by_url_query(url) do
|
|
from(e in Event, where: e.url == ^url)
|
|
end
|
|
|
|
@spec event_by_uuid_query(String.t()) :: Ecto.Query.t()
|
|
defp event_by_uuid_query(uuid) do
|
|
from(e in Event, where: e.uuid == ^uuid)
|
|
end
|
|
|
|
@spec event_for_actor_query(integer | String.t()) :: Ecto.Query.t()
|
|
defp event_for_actor_query(actor_id, order_by \\ [desc: :id]) do
|
|
Event
|
|
|> where([e], e.organizer_actor_id == ^actor_id)
|
|
|> order_by([e], ^order_by)
|
|
end
|
|
|
|
@spec event_for_group_query(integer | String.t()) :: Ecto.Query.t()
|
|
defp event_for_group_query(group_id) do
|
|
from(
|
|
e in Event,
|
|
where: e.attributed_to_id == ^group_id,
|
|
order_by: [asc: :begins_on]
|
|
)
|
|
end
|
|
|
|
@spec upcoming_public_event_for_actor_query(integer | String.t()) :: Ecto.Query.t()
|
|
defp upcoming_public_event_for_actor_query(actor_id) do
|
|
from(
|
|
e in Event,
|
|
where:
|
|
e.organizer_actor_id == ^actor_id and
|
|
e.begins_on > ^DateTime.utc_now(),
|
|
order_by: [asc: :begins_on],
|
|
limit: 1,
|
|
preload: [
|
|
:organizer_actor,
|
|
:tags,
|
|
:participants,
|
|
:physical_address
|
|
]
|
|
)
|
|
end
|
|
|
|
@spec close_events_query(Geo.geometry(), number) :: Ecto.Query.t()
|
|
defp close_events_query(point, radius) do
|
|
from(
|
|
e in Event,
|
|
join: a in Address,
|
|
on: a.id == e.physical_address_id,
|
|
where: e.visibility == ^:public and st_dwithin_in_meters(^point, a.geom, ^radius),
|
|
preload: :organizer_actor
|
|
)
|
|
end
|
|
|
|
@spec user_events_query(Ecto.Queryable.t(), number()) :: Ecto.Query.t()
|
|
defp user_events_query(query, user_id) do
|
|
from(
|
|
e in query,
|
|
join: a in Actor,
|
|
on: a.id == e.organizer_actor_id,
|
|
where: a.user_id == ^user_id
|
|
)
|
|
end
|
|
|
|
defmacro matching_event_ids_and_ranks(search_string) do
|
|
quote do
|
|
fragment(
|
|
"""
|
|
SELECT event_search.id AS id,
|
|
ts_rank(
|
|
event_search.document, plainto_tsquery(unaccent(?))
|
|
) AS rank
|
|
FROM event_search
|
|
WHERE event_search.document @@ plainto_tsquery(unaccent(?))
|
|
OR event_search.title ILIKE ?
|
|
""",
|
|
^unquote(search_string),
|
|
^unquote(search_string),
|
|
^"%#{unquote(search_string)}%"
|
|
)
|
|
end
|
|
end
|
|
|
|
# @spec events_for_search_query(String.t()) :: Ecto.Query.t()
|
|
# defp events_for_search_query("") do
|
|
# Event
|
|
# |> join: rank in fragment("")
|
|
# end
|
|
|
|
defp events_for_search_query(search_string) do
|
|
from(event in Event,
|
|
join: id_and_rank in matching_event_ids_and_ranks(search_string),
|
|
on: id_and_rank.id == event.id
|
|
)
|
|
end
|
|
|
|
@spec events_for_begins_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_begins_on(query, args) do
|
|
begins_on = Map.get(args, :begins_on, DateTime.utc_now())
|
|
|
|
if is_nil(begins_on) do
|
|
query
|
|
else
|
|
where(
|
|
query,
|
|
[q],
|
|
(not is_nil(q.ends_on) and q.ends_on > ^begins_on) or
|
|
(is_nil(q.ends_on) and q.begins_on >= ^begins_on)
|
|
)
|
|
end
|
|
end
|
|
|
|
@spec events_for_ends_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_ends_on(query, args) do
|
|
ends_on = Map.get(args, :ends_on)
|
|
|
|
if is_nil(ends_on) do
|
|
query
|
|
else
|
|
where(
|
|
query,
|
|
[q],
|
|
(is_nil(q.ends_on) and q.begins_on <= ^ends_on) or
|
|
q.ends_on <= ^ends_on
|
|
)
|
|
end
|
|
end
|
|
|
|
@spec events_for_category(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_category(query, %{category: category}) when is_valid_string(category) do
|
|
where(query, [q], q.category == ^category)
|
|
end
|
|
|
|
defp events_for_category(query, _args), do: query
|
|
|
|
@spec events_for_categories(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_categories(query, %{category_one_of: category_one_of})
|
|
when length(category_one_of) > 0 do
|
|
where(query, [q], q.category in ^category_one_of)
|
|
end
|
|
|
|
defp events_for_categories(query, _args), do: query
|
|
|
|
defp events_for_languages(query, %{language_one_of: language_one_of})
|
|
when length(language_one_of) > 0 do
|
|
where(query, [q], q.language in ^language_one_of)
|
|
end
|
|
|
|
defp events_for_languages(query, _args), do: query
|
|
|
|
defp events_for_statuses(query, %{status_one_of: status_one_of})
|
|
when length(status_one_of) > 0 do
|
|
where(query, [q], q.status in ^status_one_of)
|
|
end
|
|
|
|
defp events_for_statuses(query, _args), do: query
|
|
|
|
@spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do
|
|
query
|
|
|> join(:inner, [q], te in "events_tags", on: q.id == te.event_id)
|
|
|> join(:inner, [q, ..., te], t in Tag, on: te.tag_id == t.id)
|
|
|> where([q, ..., t], t.title in ^String.split(tags, ",", trim: true))
|
|
end
|
|
|
|
defp events_for_tags(query, _args), do: query
|
|
|
|
# We add the inner join on address only if we're going to use it in
|
|
# events_for_location or events_for_bounding_box
|
|
# So we're sure it's only being added once
|
|
defp maybe_join_address(query, %{location: location, radius: radius})
|
|
when is_valid_string(location) and not is_nil(radius) do
|
|
join(query, :inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
|
|
end
|
|
|
|
defp maybe_join_address(query, %{bbox: bbox}) when is_valid_string(bbox) do
|
|
join(query, :inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
|
|
end
|
|
|
|
defp maybe_join_address(query, _), do: query
|
|
|
|
@spec events_for_location(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
|
defp events_for_location(query, %{radius: radius}) when is_nil(radius),
|
|
do: query
|
|
|
|
defp events_for_location(query, %{location: location, radius: radius})
|
|
when is_valid_string(location) and not is_nil(radius) do
|
|
with {lon, lat} <- Geohax.decode(location),
|
|
point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})"),
|
|
{{x_min, y_min}, {x_max, y_max}} <- search_box({lon, lat}, radius) do
|
|
where(
|
|
query,
|
|
[q, ..., a],
|
|
st_x(a.geom) > ^x_min and st_x(a.geom) < ^x_max and
|
|
st_y(a.geom) > ^y_min and st_y(a.geom) < ^y_max and
|
|
st_dwithin_in_meters(^point, a.geom, ^(radius * 1000))
|
|
)
|
|
else
|
|
_ -> query
|
|
end
|
|
end
|
|
|
|
defp events_for_location(query, _args), do: query
|
|
|
|
defp events_for_bounding_box(query, %{bbox: bbox}) when is_valid_string(bbox) do
|
|
[top_left, bottom_right] = String.split(bbox, ":")
|
|
[ymax, xmin] = String.split(top_left, ",")
|
|
[ymin, xmax] = String.split(bottom_right, ",")
|
|
|
|
where(
|
|
query,
|
|
[q, ..., a],
|
|
fragment(
|
|
"? @ ST_MakeEnvelope(?,?,?,?,?)",
|
|
a.geom,
|
|
^sanitize_bounding_box_params(xmin),
|
|
^sanitize_bounding_box_params(ymin),
|
|
^sanitize_bounding_box_params(xmax),
|
|
^sanitize_bounding_box_params(ymax),
|
|
"4326"
|
|
)
|
|
)
|
|
end
|
|
|
|
defp events_for_bounding_box(query, _args), do: query
|
|
|
|
@spec sanitize_bounding_box_params(String.t()) :: float()
|
|
defp sanitize_bounding_box_params(param) do
|
|
param
|
|
|> String.trim()
|
|
|> String.to_float()
|
|
|> Float.floor(10)
|
|
end
|
|
|
|
@spec search_box({float(), float()}, float()) :: {{float, float}, {float, float}}
|
|
defp search_box({lon0, lat0}, radius) do
|
|
km_per_lat_deg = 111.195
|
|
lat_amp = radius / km_per_lat_deg
|
|
km_per_lon_deg_at_lat = Haversine.distance({0.0, lat0}, {1.0, lat0}) / 1000
|
|
lon_amp = radius / km_per_lon_deg_at_lat
|
|
{{lon0 - lon_amp, lat0 - lat_amp}, {lon0 + lon_amp, lat0 + lat_amp}}
|
|
end
|
|
|
|
@spec filter_online(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
|
defp filter_online(query, %{type: :online}), do: is_online_fragment(query, true)
|
|
|
|
defp filter_online(query, %{type: :in_person}), do: is_online_fragment(query, false)
|
|
|
|
defp filter_online(query, _), do: query
|
|
|
|
@spec is_online_fragment(Ecto.Query.t(), boolean()) :: Ecto.Query.t()
|
|
defp is_online_fragment(query, value) do
|
|
where(query, [q], fragment("(?->>'is_online')::bool = ?", q.options, ^value))
|
|
end
|
|
|
|
@spec normalize_search_string(String.t()) :: String.t()
|
|
defp normalize_search_string(search_string) do
|
|
search_string
|
|
|> String.downcase()
|
|
|> String.replace(~r/\n/, " ")
|
|
|> String.replace(~r/\t/, " ")
|
|
|> String.replace(~r/\s{2,}/, " ")
|
|
|> String.trim()
|
|
end
|
|
|
|
@spec events_by_tags_query([integer], integer) :: Ecto.Query.t()
|
|
def events_by_tags_query(tags_ids, limit) do
|
|
from(
|
|
e in Event,
|
|
distinct: e.uuid,
|
|
join: te in "events_tags",
|
|
on: e.id == te.event_id,
|
|
where: e.begins_on > ^DateTime.utc_now(),
|
|
where: e.visibility == ^:public,
|
|
where: te.tag_id in ^tags_ids,
|
|
order_by: [asc: e.begins_on],
|
|
limit: ^limit
|
|
)
|
|
end
|
|
|
|
@spec count_local_events_query :: Ecto.Query.t()
|
|
defp count_local_events_query do
|
|
from(e in Event, select: count(e.id), where: e.local == ^true)
|
|
end
|
|
|
|
@spec count_events_query :: Ecto.Query.t()
|
|
defp count_events_query do
|
|
from(e in Event, select: count(e.id))
|
|
end
|
|
|
|
@spec tag_by_slug_query(String.t()) :: Ecto.Query.t()
|
|
defp tag_by_slug_query(slug) do
|
|
from(t in Tag, where: t.slug == ^slug)
|
|
end
|
|
|
|
@spec tag_by_title_query(String.t()) :: Ecto.Query.t()
|
|
defp tag_by_title_query(title) do
|
|
from(t in Tag, where: t.title == ^title, limit: 1)
|
|
end
|
|
|
|
@spec tag_filter(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t()
|
|
defp tag_filter(query, nil), do: query
|
|
defp tag_filter(query, ""), do: query
|
|
|
|
defp tag_filter(query, filter) when is_binary(filter) do
|
|
query
|
|
|> where([q], ilike(q.slug, ^"%#{filter}%"))
|
|
|> or_where([q], ilike(q.title, ^"%#{filter}%"))
|
|
end
|
|
|
|
@spec tags_for_event_query(integer) :: Ecto.Query.t()
|
|
defp tags_for_event_query(event_id) do
|
|
from(
|
|
t in Tag,
|
|
join: e in "events_tags",
|
|
on: t.id == e.tag_id,
|
|
where: e.event_id == ^event_id
|
|
)
|
|
end
|
|
|
|
@spec tags_linked_query(integer, integer) :: Ecto.Query.t()
|
|
defp tags_linked_query(tag1_id, tag2_id) do
|
|
from(
|
|
tr in TagRelation,
|
|
where:
|
|
tr.tag_id == ^min(tag1_id, tag2_id) and
|
|
tr.link_id == ^max(tag1_id, tag2_id)
|
|
)
|
|
end
|
|
|
|
@spec tag_relation_subquery(integer) :: Ecto.Query.t()
|
|
defp tag_relation_subquery(tag_id) do
|
|
from(
|
|
tr in TagRelation,
|
|
select: %{id: tr.tag_id, weight: tr.weight},
|
|
where: tr.link_id == ^tag_id
|
|
)
|
|
end
|
|
|
|
@spec tag_relation_union_subquery(Ecto.Queryable.t(), integer) :: Ecto.Query.t()
|
|
defp tag_relation_union_subquery(subquery, tag_id) do
|
|
from(
|
|
tr in TagRelation,
|
|
select: %{id: tr.link_id, weight: tr.weight},
|
|
union_all: ^subquery,
|
|
where: tr.tag_id == ^tag_id
|
|
)
|
|
end
|
|
|
|
@spec tag_neighbors_query(Ecto.Queryable.t(), integer, integer) :: Ecto.Query.t()
|
|
defp tag_neighbors_query(subquery, relation_minimum, limit) do
|
|
from(
|
|
t in Tag,
|
|
right_join: q in subquery(subquery),
|
|
on: [id: t.id],
|
|
where: q.weight >= ^relation_minimum,
|
|
limit: ^limit,
|
|
order_by: [desc: q.weight]
|
|
)
|
|
end
|
|
|
|
@spec participant_by_url_query(String.t()) :: Ecto.Query.t()
|
|
defp participant_by_url_query(url) do
|
|
from(
|
|
p in Participant,
|
|
where: p.url == ^url,
|
|
preload: [:actor, :event]
|
|
)
|
|
end
|
|
|
|
defp organizers_participants_for_event(event_id) do
|
|
from(
|
|
p in Participant,
|
|
where: p.event_id == ^event_id and p.role == ^:creator,
|
|
preload: [:actor]
|
|
)
|
|
end
|
|
|
|
@spec requests_for_actor_query(integer) :: Ecto.Query.t()
|
|
defp requests_for_actor_query(actor_id) do
|
|
from(p in Participant, where: p.actor_id == ^actor_id and p.role == ^:not_approved)
|
|
end
|
|
|
|
@spec count_participants_query(integer) :: Ecto.Query.t()
|
|
def count_participants_query(event_id) do
|
|
from(p in Participant, where: p.event_id == ^event_id)
|
|
end
|
|
|
|
@spec event_participations_for_actor_query(integer) :: Ecto.Query.t()
|
|
def event_participations_for_actor_query(actor_id) do
|
|
from(
|
|
p in Participant,
|
|
join: e in Event,
|
|
on: p.event_id == e.id,
|
|
where: p.actor_id == ^actor_id and p.role != ^:not_approved,
|
|
preload: [:event],
|
|
order_by: [desc: e.begins_on]
|
|
)
|
|
end
|
|
|
|
@spec sessions_for_event_query(integer) :: Ecto.Query.t()
|
|
defp sessions_for_event_query(event_id) do
|
|
from(
|
|
s in Session,
|
|
join: e in Event,
|
|
on: s.event_id == e.id,
|
|
where: e.id == ^event_id
|
|
)
|
|
end
|
|
|
|
@spec sessions_for_track_query(integer) :: Ecto.Query.t()
|
|
defp sessions_for_track_query(track_id) do
|
|
from(s in Session, where: s.track_id == ^track_id)
|
|
end
|
|
|
|
@spec count_confirmed_participants_for_local_events_query :: Ecto.Query.t()
|
|
defp count_confirmed_participants_for_local_events_query do
|
|
Participant
|
|
|> join(:inner, [p], e in Event, on: p.event_id == e.id)
|
|
|> where([p, e], e.local and p.role not in [^:not_approved, ^:not_confirmed, ^:rejected])
|
|
|> select([p], count(p.id))
|
|
end
|
|
|
|
@spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
|
|
defp list_participants_for_event_query(event_id) do
|
|
Participant
|
|
|> where([p], p.event_id == ^event_id)
|
|
end
|
|
|
|
@spec list_participant_actors_for_event_query(String.t()) :: Ecto.Query.t()
|
|
defp list_participant_actors_for_event_query(event_id) do
|
|
from(
|
|
a in Actor,
|
|
join: p in Participant,
|
|
on: p.actor_id == a.id,
|
|
where: p.event_id == ^event_id
|
|
)
|
|
end
|
|
|
|
@spec participants_for_event_query(String.t(), list(atom())) :: Ecto.Query.t()
|
|
def participants_for_event_query(id, roles \\ []) do
|
|
id
|
|
|> list_participants_for_event_query()
|
|
|> filter_role(roles)
|
|
|> order_by(asc: :role)
|
|
end
|
|
|
|
def participant_for_event_export_query(id, roles) do
|
|
id
|
|
|> participants_for_event_query(roles)
|
|
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
|
|> select([p, a], {p, a})
|
|
end
|
|
|
|
@doc """
|
|
List emails for local users (including anonymous ones) participating to an event
|
|
|
|
Returns {participation, actor, user, user_settings}
|
|
"""
|
|
@spec list_local_emails_user_participants_for_event_query(String.t()) :: Ecto.Query.t()
|
|
def list_local_emails_user_participants_for_event_query(event_id) do
|
|
Participant
|
|
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id and is_nil(a.domain))
|
|
|> join(:left, [_p, a], u in User, on: a.user_id == u.id)
|
|
|> join(:left, [_p, _a, u], s in Setting, on: s.user_id == u.id)
|
|
|> where(
|
|
[p],
|
|
p.event_id == ^event_id and p.role not in [^:not_approved, ^:not_confirmed, ^:rejected]
|
|
)
|
|
|> select([p, a, u, s], {p, a, u, s})
|
|
end
|
|
|
|
@doc """
|
|
List anonymous participations to an event
|
|
"""
|
|
@spec list_anonymous_participants_for_event(String.t()) :: [Participant.t()]
|
|
def list_anonymous_participants_for_event(event_id) do
|
|
Participant
|
|
|> where(
|
|
[p],
|
|
p.event_id == ^event_id and p.role not in [^:not_approved, ^:not_confirmed, ^:rejected]
|
|
)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def category_statistics do
|
|
Event
|
|
|> filter_future_events(true)
|
|
|> filter_public_visibility()
|
|
|> filter_draft()
|
|
|> filter_cancelled_events()
|
|
|> filter_local_or_from_followed_instances_events()
|
|
|> group_by([e], e.category)
|
|
|> select([e], {e.category, count(e.id)})
|
|
|> Repo.all()
|
|
end
|
|
|
|
@threshold_related_value 7
|
|
|
|
def related_events(%Event{
|
|
id: event_id,
|
|
tags: tags,
|
|
physical_address: physical_address,
|
|
category: category,
|
|
language: language
|
|
}) do
|
|
event_tags_ids = Enum.map(tags, & &1.id)
|
|
geom = if is_nil(physical_address), do: nil, else: physical_address.geom
|
|
|
|
Event
|
|
|> distinct([e], e.id)
|
|
|> join(:left, [e], et in "events_tags", on: e.id == et.event_id)
|
|
|> join(:left, [e], a in Address, on: e.physical_address_id == a.id)
|
|
|> where(
|
|
[e, et, a],
|
|
fragment(
|
|
"(? = ?)::int * 5 + (? = ALL(?))::int * 2 + (? = ?)::int + (?::geography is null or ST_DWithin(?::geography, ?::geography, ?::float))::int * 2 > ?",
|
|
e.language,
|
|
^language,
|
|
et.tag_id,
|
|
^event_tags_ids,
|
|
e.category,
|
|
^category,
|
|
^geom,
|
|
^geom,
|
|
a.geom,
|
|
5000,
|
|
@threshold_related_value
|
|
)
|
|
)
|
|
# |> where([e], e.begins_on > ^DateTime.utc_now())
|
|
|> where([e], e.id != ^event_id)
|
|
|> order_by(
|
|
[e, et, a],
|
|
fragment(
|
|
"(? = ?)::int * 5 + (? = ALL(?))::int * 2 + (? = ?)::int + (?::geography is null or ST_DWithin(?::geography, ?::geography, ?::float))::int * 2 DESC",
|
|
e.language,
|
|
^language,
|
|
et.tag_id,
|
|
^event_tags_ids,
|
|
e.category,
|
|
^category,
|
|
^geom,
|
|
^geom,
|
|
a.geom,
|
|
5000
|
|
)
|
|
)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec list_participations_for_user_query(integer()) :: Ecto.Query.t()
|
|
defp list_participations_for_user_query(user_id) do
|
|
from(
|
|
p in Participant,
|
|
join: e in Event,
|
|
join: a in Actor,
|
|
on: p.actor_id == a.id,
|
|
on: p.event_id == e.id,
|
|
where: a.user_id == ^user_id and p.role != ^:not_approved,
|
|
preload: [:event, :actor]
|
|
)
|
|
end
|
|
|
|
@spec feed_token_query(String.t()) :: Ecto.Query.t()
|
|
defp feed_token_query(token) do
|
|
from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user])
|
|
end
|
|
|
|
@spec feed_token_for_user_query(integer) :: Ecto.Query.t()
|
|
defp feed_token_for_user_query(user_id) do
|
|
from(tk in FeedToken, where: tk.user_id == ^user_id, preload: [:actor, :user])
|
|
end
|
|
|
|
@spec feed_token_for_actor_query(integer) :: Ecto.Query.t()
|
|
defp feed_token_for_actor_query(actor_id) do
|
|
from(tk in FeedToken, where: tk.actor_id == ^actor_id, preload: [:actor, :user])
|
|
end
|
|
|
|
@spec filter_public_visibility(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp filter_public_visibility(query) do
|
|
from(e in query, where: e.visibility == ^:public)
|
|
end
|
|
|
|
@spec filter_unlisted_and_public_visibility(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp filter_unlisted_and_public_visibility(query) do
|
|
from(q in query, where: q.visibility in ^@public_visibility)
|
|
end
|
|
|
|
@spec filter_not_event_uuid(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t()
|
|
defp filter_not_event_uuid(query, nil), do: query
|
|
|
|
defp filter_not_event_uuid(query, not_event_uuid) do
|
|
from(e in query, where: e.uuid != ^not_event_uuid)
|
|
end
|
|
|
|
@spec filter_draft(Ecto.Queryable.t(), boolean) :: Ecto.Query.t()
|
|
defp filter_draft(query, is_draft \\ false) do
|
|
from(e in query, where: e.draft == ^is_draft)
|
|
end
|
|
|
|
@spec filter_cancelled_events(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp filter_cancelled_events(query) do
|
|
from(e in query, where: e.status != ^:cancelled)
|
|
end
|
|
|
|
@spec filter_future_events(Ecto.Queryable.t(), boolean) :: Ecto.Query.t()
|
|
defp filter_future_events(query, true) do
|
|
from(q in query,
|
|
where: q.begins_on > ^DateTime.utc_now()
|
|
)
|
|
end
|
|
|
|
defp filter_future_events(query, false), do: query
|
|
|
|
@spec filter_local(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp filter_local(query) do
|
|
where(query, [q], q.local == true)
|
|
end
|
|
|
|
@spec filter_local_or_from_followed_instances_events(Ecto.Queryable.t()) ::
|
|
Ecto.Query.t()
|
|
defp filter_local_or_from_followed_instances_events(query) do
|
|
%Actor{id: follower_actor_id} = Relay.get_actor()
|
|
|
|
query
|
|
|> join(:left, [q], s in Share, on: s.uri == q.url)
|
|
|> join(:left, [_q, ..., s], f in Follower, on: f.target_actor_id == s.actor_id)
|
|
|> where(
|
|
[q, ..., s, f],
|
|
q.local == true or (f.actor_id == ^follower_actor_id and not is_nil(s.uri))
|
|
)
|
|
end
|
|
|
|
@spec filter_participant_role(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp filter_participant_role(query) do
|
|
filter_role(query, :participant)
|
|
end
|
|
|
|
@spec filter_role(Ecto.Queryable.t(), list(atom()) | atom()) :: Ecto.Query.t()
|
|
def filter_role(query, []), do: query
|
|
|
|
def filter_role(query, roles) when is_list(roles) do
|
|
where(query, [p], p.role in ^roles)
|
|
end
|
|
|
|
def filter_role(query, role) when is_atom(role) do
|
|
from(p in query, where: p.role == ^role)
|
|
end
|
|
|
|
@spec event_filter_visibility(Ecto.Queryable.t(), :public | :all) ::
|
|
Ecto.Queryable.t() | Ecto.Query.t()
|
|
defp event_filter_visibility(query, :all), do: query
|
|
|
|
defp event_filter_visibility(query, :public) do
|
|
where(query, visibility: ^:public, draft: false)
|
|
end
|
|
|
|
defp event_filter_begins_on(query, nil, nil),
|
|
do: event_order_begins_on_desc(query)
|
|
|
|
defp event_filter_begins_on(query, %DateTime{} = after_datetime, nil) do
|
|
query
|
|
|> where([e], e.begins_on > ^after_datetime)
|
|
|> event_order_begins_on_asc()
|
|
end
|
|
|
|
defp event_filter_begins_on(query, nil, %DateTime{} = before_datetime) do
|
|
query
|
|
|> where([e], e.begins_on < ^before_datetime)
|
|
|> event_order_begins_on_desc()
|
|
end
|
|
|
|
defp event_filter_begins_on(
|
|
query,
|
|
%DateTime{} = after_datetime,
|
|
%DateTime{} = before_datetime
|
|
) do
|
|
query
|
|
|> where([e], e.begins_on < ^before_datetime)
|
|
|> where([e], e.begins_on > ^after_datetime)
|
|
|> event_order_begins_on_asc()
|
|
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),
|
|
do: order_by(query, [e], asc: e.begins_on)
|
|
|
|
defp event_order_begins_on_desc(query),
|
|
do: order_by(query, [e], desc: e.begins_on)
|
|
|
|
defp participation_filter_begins_on(query, nil, nil),
|
|
do: participation_order_begins_on_desc(query)
|
|
|
|
defp participation_filter_begins_on(query, %DateTime{} = after_datetime, nil) do
|
|
query
|
|
|> where([_p, e, _a], e.begins_on > ^after_datetime)
|
|
|> participation_order_begins_on_asc()
|
|
end
|
|
|
|
defp participation_filter_begins_on(query, nil, %DateTime{} = before_datetime) do
|
|
query
|
|
|> where([_p, e, _a], e.begins_on < ^before_datetime)
|
|
|> participation_order_begins_on_desc()
|
|
end
|
|
|
|
defp participation_filter_begins_on(
|
|
query,
|
|
%DateTime{} = after_datetime,
|
|
%DateTime{} = before_datetime
|
|
) do
|
|
query
|
|
|> where([_p, e, _a], e.begins_on < ^before_datetime)
|
|
|> where([_p, e, _a], e.begins_on > ^after_datetime)
|
|
|> participation_order_begins_on_asc()
|
|
end
|
|
|
|
defp participation_order_begins_on_asc(query),
|
|
do: order_by(query, [_p, e, _a], asc: e.begins_on)
|
|
|
|
defp participation_order_begins_on_desc(query),
|
|
do: order_by(query, [_p, e, _a], desc: e.begins_on)
|
|
|
|
@spec preload_for_event(Ecto.Queryable.t()) :: Ecto.Query.t()
|
|
defp preload_for_event(query), do: preload(query, ^@event_preloads)
|
|
end
|