defmodule Mobilizon.Events.Event do @moduledoc """ Represents an event. """ use Ecto.Schema import Ecto.Changeset alias Ecto.Changeset alias Mobilizon.Actors.Actor alias Mobilizon.{Addresses, Events, Medias, Mention} alias Mobilizon.Addresses.Address alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{ EventMetadata, EventOptions, EventParticipantStats, EventStatus, EventVisibility, JoinOptions, Participant, Session, Tag, Track } alias Mobilizon.Medias.Media alias Mobilizon.Storage.Repo alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Router.Helpers, as: Routes @type t :: %__MODULE__{ id: integer(), url: String.t(), local: boolean, begins_on: DateTime.t(), slug: String.t(), description: String.t(), ends_on: DateTime.t(), title: String.t(), status: atom(), draft: boolean, visibility: atom(), join_options: atom(), publish_at: DateTime.t() | nil, uuid: Ecto.UUID.t(), online_address: String.t() | nil, phone_address: String.t(), category: String.t(), options: EventOptions.t(), organizer_actor: Actor.t(), attributed_to: Actor.t() | nil, physical_address: Address.t() | nil, picture: Media.t() | nil, media: [Media.t()], tracks: [Track.t()], sessions: [Session.t()], mentions: [Mention.t()], tags: [Tag.t()], participants: [Actor.t()], contacts: [Actor.t()], language: String.t(), metadata: [EventMetadata.t()] } @update_required_attrs [:title, :begins_on, :organizer_actor_id] @required_attrs @update_required_attrs ++ [:url, :uuid] @optional_attrs [ :slug, :description, :ends_on, :category, :status, :draft, :local, :visibility, :join_options, :publish_at, :online_address, :phone_address, :picture_id, :physical_address_id, :attributed_to_id, :language ] @attrs @required_attrs ++ @optional_attrs @update_attrs @update_required_attrs ++ @optional_attrs schema "events" do field(:url, :string) field(:local, :boolean, default: true) field(:begins_on, :utc_datetime) field(:slug, :string) field(:description, :string) field(:ends_on, :utc_datetime) field(:title, :string) field(:status, EventStatus, default: :confirmed) field(:draft, :boolean, default: false) field(:visibility, EventVisibility, default: :public) field(:join_options, JoinOptions, default: :free) field(:publish_at, :utc_datetime) field(:uuid, Ecto.UUID, default: Ecto.UUID.generate()) field(:online_address, :string) field(:phone_address, :string) field(:category, :string) field(:language, :string, default: "und") embeds_one(:options, EventOptions, on_replace: :delete) embeds_one(:participant_stats, EventParticipantStats, on_replace: :update) embeds_many(:metadata, EventMetadata, on_replace: :delete) belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) belongs_to(:physical_address, Address, on_replace: :nilify) belongs_to(:picture, Media, on_replace: :update) has_many(:tracks, Track) has_many(:sessions, Session) has_many(:mentions, Mention) has_many(:comments, Comment) many_to_many(:contacts, Actor, join_through: "event_contacts", on_replace: :delete) many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete) many_to_many(:participants, Actor, join_through: Participant) many_to_many(:media, Media, join_through: "events_medias", on_replace: :delete) timestamps(type: :utc_datetime) end @doc false @spec changeset(t | Ecto.Schema.t(), map) :: Changeset.t() def changeset(%__MODULE__{} = event, attrs) do attrs = Map.update(attrs, :uuid, Ecto.UUID.generate(), &(&1 || Ecto.UUID.generate())) attrs = Map.update(attrs, :url, Routes.page_url(Endpoint, :event, attrs.uuid), & &1) event |> cast(attrs, @attrs) |> common_changeset(attrs) |> put_creator_if_published(:create) |> validate_required(@required_attrs) end @doc false @spec update_changeset(t, map) :: Changeset.t() def update_changeset(%__MODULE__{} = event, attrs) do event |> cast(attrs, @update_attrs) |> common_changeset(attrs) |> put_creator_if_published(:update) |> validate_required(@update_required_attrs) end @spec common_changeset(Changeset.t(), map) :: Changeset.t() defp common_changeset(%Changeset{} = changeset, attrs) do changeset |> cast_embed(:options) |> cast_embed(:metadata) |> put_assoc(:contacts, Map.get(attrs, :contacts, [])) |> put_assoc(:media, Map.get(attrs, :media, [])) |> put_tags(attrs) |> put_address(attrs) |> put_picture(attrs) |> validate_lengths() |> validate_end_time() end @spec validate_lengths(Changeset.t()) :: Changeset.t() defp validate_lengths(%Changeset{} = changeset) do changeset |> validate_length(:title, min: 3, max: 200) |> validate_length(:online_address, min: 3, max: 2000) |> validate_length(:phone_address, min: 3, max: 200) |> validate_length(:category, min: 2, max: 100) |> validate_length(:slug, min: 3, max: 200) end defp validate_end_time(%Changeset{} = changeset) do case fetch_field(changeset, :begins_on) do {_, begins_on} -> validate_change(changeset, :ends_on, fn :ends_on, ends_on -> if DateTime.compare(begins_on, ends_on) == :gt, do: [ends_on: "ends_on cannot be set before begins_on"], else: [] end) :error -> changeset end end @doc """ Checks whether an event can be managed. """ @spec can_be_managed_by?(t, integer | String.t()) :: boolean def can_be_managed_by?(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id), do: organizer_actor_id == actor_id def can_be_managed_by?(_event, _actor), do: false @spec put_tags(Changeset.t(), map) :: Changeset.t() defp put_tags(%Changeset{} = changeset, %{tags: tags}) do put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1)) end defp put_tags(%Changeset{} = changeset, _), do: changeset @spec process_tag(map() | Tag.t()) :: Tag.t() | Ecto.Changeset.t() # We need a changeset instead of a raw struct because of slug which is generated in changeset defp process_tag(%{id: id} = _tag) do Events.get_tag(id) end defp process_tag(tag) do Tag.changeset(%Tag{}, tag) end # In case the provided addresses is an existing one @spec put_address(Changeset.t(), map) :: Changeset.t() defp put_address(%Changeset{} = changeset, %{physical_address: %{id: id} = _physical_address}) when not is_nil(id) do case Addresses.get_address(id) do %Address{} = address -> put_assoc(changeset, :physical_address, address) _ -> cast_assoc(changeset, :physical_address) end end # In case it's a new address but the origin_id is an existing one defp put_address(%Changeset{} = changeset, %{physical_address: %{origin_id: origin_id}}) when not is_nil(origin_id) do case Repo.get_by(Address, origin_id: origin_id) do %Address{} = address -> put_assoc(changeset, :physical_address, address) _ -> cast_assoc(changeset, :physical_address) end end # In case it's a new address without any origin_id (manual) defp put_address(%Changeset{} = changeset, _attrs) do cast_assoc(changeset, :physical_address) end # In case the provided picture is an existing one @spec put_picture(Changeset.t(), map) :: Changeset.t() defp put_picture(%Changeset{} = changeset, %{picture: %{media_id: id} = _picture}) do %Media{} = picture = Medias.get_media!(id) put_assoc(changeset, :picture, picture) end # In case it's a new picture defp put_picture(%Changeset{} = changeset, _attrs) do cast_assoc(changeset, :picture, with: &Media.changeset/2) end # Created or updated with draft parameter: don't publish defp put_creator_if_published( %Changeset{changes: %{draft: true}} = changeset, _action ) do put_embed(changeset, :participant_stats, %{creator: 0}) end # Created with any other value: publish defp put_creator_if_published( %Changeset{} = changeset, :create ) do changeset |> put_embed(:participant_stats, %{creator: 1}) end # Updated from draft false to true: publish defp put_creator_if_published( %Changeset{ data: %{draft: false}, changes: %{draft: true} } = changeset, :update ) do changeset |> put_embed(:participant_stats, %{creator: 1}) end defp put_creator_if_published(%Changeset{} = changeset, _), do: cast_embed(changeset, :participant_stats) @doc """ Whether we can show the event. Returns false if the organizer actor or group is suspended """ @spec show?(t) :: boolean() def show?(%__MODULE__{attributed_to: %Actor{suspended: true}}), do: false def show?(%__MODULE__{organizer_actor: %Actor{suspended: true}}), do: false def show?(%__MODULE__{}), do: true end