Refactor Core things, including Ecto handling, ActivityPub & Transmogrifier modules

* Data doesn't need anymore to be converted to ActivityStream format to
be saved (this was taken from Pleroma and not at all a good idea here)
* Everything saved when creating an event is inserted into PostgreSQL in
a single transaction
This commit is contained in:
Thomas Citharel 2019-10-25 17:43:37 +02:00
parent 814cfbc8eb
commit cc820d6b63
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
69 changed files with 1881 additions and 1424 deletions

View File

@ -14,7 +14,7 @@ export default {
data() {
return {
participation: {
event: {
event: {
title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5,
uuid: 'some uuid',
@ -31,7 +31,7 @@ export default {
},
participantStats: {
approved: 1,
unapproved: 2
notApproved: 2
}
},
actor: {
@ -75,20 +75,20 @@ export default {
</span>
<span class="column is-narrow participant-stats">
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.participants, total: participation.event.options.maximumAttendeeCapacity }) }}
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.participant, total: participation.event.options.maximumAttendeeCapacity }) }}
<!-- <b-progress-->
<!-- v-if="participation.event.options.maximumAttendeeCapacity > 0"-->
<!-- size="is-medium"-->
<!-- :value="participation.event.participantStats.participants * 100 / participation.event.options.maximumAttendeeCapacity">-->
<!-- :value="participation.event.participantStats.participant * 100 / participation.event.options.maximumAttendeeCapacity">-->
<!-- </b-progress>-->
</span>
<span v-else>
{{ $tc('{count} participants', participation.event.participantStats.participants, { count: participation.event.participantStats.participants })}}
{{ $tc('{count} participants', participation.event.participantStats.participant, { count: participation.event.participantStats.participant })}}
</span>
<span
v-if="participation.event.participantStats.unapproved > 0">
v-if="participation.event.participantStats.notApproved > 0">
<b-button type="is-text" @click="gotToWithCheck(participation, { name: RouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } })">
{{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}}
{{ $tc('{count} requests waiting', participation.event.participantStats.notApproved, { count: participation.event.participantStats.notApproved })}}
</b-button>
</span>
</span>

View File

@ -113,9 +113,8 @@ query LoggedUserParticipations($afterDateTime: DateTime, $beforeDateTime: DateTi
}
},
participantStats {
approved,
unapproved,
participants
notApproved
participant
},
options {
maximumAttendeeCapacity
@ -161,8 +160,8 @@ export const LOGGED_USER_DRAFTS = gql`
}
},
participantStats {
approved,
unapproved
going,
notApproved
},
options {
maximumAttendeeCapacity

View File

@ -102,9 +102,9 @@ export const FETCH_EVENT = gql`
# name,
# },
participantStats {
approved,
unapproved,
participants
going,
notApproved,
participant
},
tags {
${tagsQuery}
@ -259,9 +259,9 @@ export const CREATE_EVENT = gql`
id,
},
participantStats {
approved,
unapproved,
participants
going,
notApproved,
participant
},
tags {
${tagsQuery}
@ -344,9 +344,9 @@ export const EDIT_EVENT = gql`
id,
},
participantStats {
approved,
unapproved,
participants
going,
notApproved,
participant
},
tags {
${tagsQuery}
@ -410,10 +410,10 @@ export const PARTICIPANTS = gql`
${participantQuery}
},
participantStats {
approved,
unapproved,
going,
notApproved,
rejected,
participants
participant
}
}
}

View File

@ -8,10 +8,10 @@ import { IPerson } from '@/types/actor';
@Component
export default class EventMixin extends mixins(Vue) {
async openDeleteEventModal (event: IEvent, currentActor: IPerson) {
const participantsLength = event.participantStats.approved;
const participantsLength = event.participantStats.participant;
const prefix = participantsLength
? this.$tc('There are {participants} participants.', event.participantStats.approved, {
participants: event.participantStats.approved,
? this.$tc('There are {participants} participants.', event.participantStats.participant, {
participants: event.participantStats.participant,
})
: '';

View File

@ -94,10 +94,13 @@ export enum CommentModeration {
}
export interface IEventParticipantStats {
approved: number;
unapproved: number;
notApproved: number;
rejected: number;
participants: number;
participant: number;
creator: number;
moderator: number;
administrator: number;
going: number;
}
export interface IEvent {
@ -192,7 +195,7 @@ export class EventModel implements IEvent {
publishAt = new Date();
participantStats = { approved: 0, unapproved: 0, rejected: 0, participants: 0 };
participantStats = { notApproved: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
participants: IParticipant[] = [];
relatedEvents: IEvent[] = [];

View File

@ -16,18 +16,18 @@ import {ParticipantRole} from "@/types/event.model";
<h1 class="title">{{ event.title }}</h1>
<span>
<router-link v-if="actorIsOrganizer" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
<small v-if="event.participantStats.approved > 0 && !actorIsParticipant">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
<small v-if="event.participantStats.going > 0 && !actorIsParticipant">
{{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
</small>
<small v-else-if="event.participantStats.approved > 0 && actorIsParticipant">
{{ $tc('You and one other person are going to this event', event.participantStats.participants, { approved: event.participantStats.participants }) }}
<small v-else-if="event.participantStats.going > 0 && actorIsParticipant">
{{ $tc('You and one other person are going to this event', event.participantStats.participant, { approved: event.participantStats.participant }) }}
</small>
</router-link>
<small v-if="event.participantStats.approved > 0 && !actorIsParticipant && !actorIsOrganizer">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
<small v-if="event.participantStats.going > 0 && !actorIsParticipant && !actorIsOrganizer">
{{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
</small>
<small v-else-if="event.participantStats.approved > 0 && actorIsParticipant && !actorIsOrganizer">
{{ $tc('You and one other person are going to this event', event.participantStats.participants, { approved: event.participantStats.participants }) }}
<small v-else-if="event.participantStats.going > 0 && actorIsParticipant && !actorIsOrganizer">
{{ $tc('You and one other person are going to this event', event.participantStats.participant, { approved: event.participantStats.participant }) }}
</small>
<small v-if="event.options.maximumAttendeeCapacity">
{{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }}
@ -443,10 +443,10 @@ export default class Event extends EventMixin {
}
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved + 1;
event.participantStats.notApproved = event.participantStats.notApproved + 1;
} else {
event.participantStats.approved = event.participantStats.approved + 1;
event.participantStats.participants = event.participantStats.participants + 1;
event.participantStats.going = event.participantStats.going + 1;
event.participantStats.participant = event.participantStats.participant + 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
@ -514,10 +514,10 @@ export default class Event extends EventMixin {
return;
}
if (participation.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved - 1;
event.participantStats.notApproved = event.participantStats.notApproved - 1;
} else {
event.participantStats.approved = event.participantStats.approved - 1;
event.participantStats.participants = event.participantStats.participants - 1;
event.participantStats.going = event.participantStats.going - 1;
event.participantStats.participant = event.participantStats.participant - 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
},
@ -591,11 +591,11 @@ export default class Event extends EventMixin {
get eventCapacityOK(): boolean {
if (!this.event.options.maximumAttendeeCapacity) return true;
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participants;
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
}
get numberOfPlacesStillAvailable(): number {
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participants;
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
}
urlToHostname(url: string): string|null {

View File

@ -4,7 +4,7 @@
<b-tab-item>
<template slot="header">
<b-icon icon="account-multiple"></b-icon>
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.approved }} </b-tag> </span>
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
</template>
<template>
<section v-if="participantsAndCreators.length > 0">
@ -22,10 +22,10 @@
</section>
</template>
</b-tab-item>
<b-tab-item :disabled="participantStats.unapproved === 0">
<b-tab-item :disabled="participantStats.notApproved === 0">
<template slot="header">
<b-icon icon="account-multiple-plus"></b-icon>
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.unapproved }} </b-tag> </span>
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
</template>
<template>
<section v-if="queue.length > 0">
@ -182,7 +182,7 @@ export default class Participants extends Vue {
@Watch('participantStats', { deep: true })
watchParticipantStats(stats: IEventParticipantStats) {
if (!stats) return;
if ((stats.unapproved === 0 && this.activeTab === 1) || stats.rejected === 0 && this.activeTab === 2 ) {
if ((stats.notApproved === 0 && this.activeTab === 1) || stats.rejected === 0 && this.activeTab === 2 ) {
this.activeTab = 0;
}
}
@ -223,9 +223,9 @@ export default class Participants extends Vue {
if (data) {
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
this.rejected = this.rejected.filter(participant => participant.id !== data.updateParticipation.id);
this.event.participantStats.approved += 1;
this.event.participantStats.going += 1;
if (participant.role === ParticipantRole.NOT_APPROVED) {
this.event.participantStats.unapproved -= 1;
this.event.participantStats.notApproved -= 1;
}
if (participant.role === ParticipantRole.REJECTED) {
this.event.participantStats.rejected -= 1;
@ -253,11 +253,11 @@ export default class Participants extends Vue {
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
this.event.participantStats.rejected += 1;
if (participant.role === ParticipantRole.PARTICIPANT) {
this.event.participantStats.participants -= 1;
this.event.participantStats.approved -= 1;
this.event.participantStats.participant -= 1;
this.event.participantStats.going -= 1;
}
if (participant.role === ParticipantRole.NOT_APPROVED) {
this.event.participantStats.unapproved -= 1;
this.event.participantStats.notApproved -= 1;
}
participant.role = ParticipantRole.REJECTED;
this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id);

View File

@ -0,0 +1,67 @@
defmodule Mix.Tasks.Mobilizon.MoveParticipantStats do
@moduledoc """
Temporary task to move participant stats in the events table
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Storage.Repo
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Events.ParticipantRole
import Ecto.Query
require Logger
@shortdoc "Move participant stats to events table"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts(
"\nStarting inserting participants stats into #{nb_events} events, this can take a while…\n"
)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([%Event{url: url} = event | events], nb_events) do
with roles <- ParticipantRole.__enum_map__(),
counts <-
Enum.reduce(roles, %{}, fn role, acc ->
Map.put(acc, role, count_participants(event, role))
end),
{:ok, _} <-
Events.update_event(event, %{
participant_stats: counts
}) do
Logger.debug("Added participants stats to event #{url}")
else
{:error, res} ->
Logger.error("Error while adding participants stats to event #{url} : #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([], nb_events) do
IO.puts("\nFinished inserting participant stats for #{nb_events} events!\n")
end
defp count_participants(%Event{id: event_id}, role) when is_atom(role) do
event_id
|> Events.count_participants_query()
|> Events.filter_role(role)
|> Repo.aggregate(:count, :id)
end
end

View File

@ -0,0 +1,49 @@
defmodule Mix.Tasks.Mobilizon.SetupSearch do
@moduledoc """
Temporary task to insert search data from existing events
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Service.Search
alias Mobilizon.Storage.Repo
alias Mobilizon.Events.Event
import Ecto.Query
require Logger
@shortdoc "Insert search data"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts("\nStarting setting up search for #{nb_events} events, this can take a while…\n")
insert_search_event(events, nb_events)
end
defp insert_search_event([%Event{url: url} = event | events], nb_events) do
case Search.insert_search_event(event) do
{:ok, _} ->
Logger.debug("Added event #{url} to the search")
{:error, res} ->
Logger.error("Error while adding event #{url} to the search: #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_search_event(events, nb_events)
end
defp insert_search_event([], nb_events) do
IO.puts("\nFinished setting up search for #{nb_events} events!\n")
end
end

View File

@ -5,18 +5,20 @@ defmodule Mix.Tasks.Mobilizon.Toot do
use Mix.Task
alias MobilizonWeb.API
alias MobilizonWeb.API.Comments
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
require Logger
@shortdoc "Toot to an user"
def run([from, content]) do
def run([from, text]) do
Mix.Task.run("app.start")
case API.Comments.create_comment(from, content) do
{:ok, _, _} ->
Mix.shell().info("Tooted")
with {:local_actor, %Actor{} = actor} <- {:local_actor, Actors.get_local_actor_by_name(from)},
{:ok, _, _} <- Comments.create_comment(%{actor: actor, text: text}) do
Mix.shell().info("Tooted")
else
{:local_actor, _, _} ->
Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist")

View File

@ -13,6 +13,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Media.File
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@ -46,6 +47,7 @@ defmodule Mobilizon.Actors.Actor do
created_reports: [Report.t()],
subject_reports: [Report.t()],
report_notes: [Note.t()],
mentions: [Mention.t()],
memberships: [t]
}
@ -139,6 +141,7 @@ defmodule Mobilizon.Actors.Actor do
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
has_many(:mentions, Mention)
many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps()

View File

@ -88,7 +88,10 @@ defmodule Mobilizon.Actors do
"""
@spec get_actor_by_url(String.t(), boolean) ::
{:ok, Actor.t()} | {:error, :actor_not_found}
def get_actor_by_url(url, preload \\ false) do
def get_actor_by_url(url, preload \\ false)
def get_actor_by_url(nil, _preload), do: {:error, :actor_not_found}
def get_actor_by_url(url, preload) do
case Repo.get_by(Actor, url: url) do
nil ->
{:error, :actor_not_found}

View File

@ -65,8 +65,7 @@ defmodule Mobilizon.Addresses.Address do
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
uuid = Ecto.UUID.generate()
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}")
put_change(changeset, :url, url)
end

View File

@ -8,7 +8,8 @@ defmodule Mobilizon.Events.Comment do
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, CommentVisibility, Event}
alias Mobilizon.Events.{Comment, CommentVisibility, Event, Tag}
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@ -22,6 +23,8 @@ defmodule Mobilizon.Events.Comment do
actor: Actor.t(),
attributed_to: Actor.t(),
event: Event.t(),
tags: [Tag.t()],
mentions: [Mention.t()],
in_reply_to_comment: t,
origin_comment: t
}
@ -42,6 +45,8 @@ defmodule Mobilizon.Events.Comment do
belongs_to(:event, Event, foreign_key: :event_id)
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
timestamps(type: :utc_datetime)
end
@ -57,16 +62,45 @@ defmodule Mobilizon.Events.Comment do
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do
uuid = attrs["uuid"] || Ecto.UUID.generate()
url = attrs["url"] || generate_url(uuid)
uuid = Map.get(attrs, :uuid) || Ecto.UUID.generate()
url = Map.get(attrs, :url) || generate_url(uuid)
comment
|> cast(attrs, @attrs)
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> put_tags(attrs)
|> put_mentions(attrs)
|> validate_required(@required_attrs)
end
@spec generate_url(String.t()) :: String.t()
defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid)
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}),
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(changeset, %{tags: tags}),
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(changeset, _), do: changeset
@spec put_mentions(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_mentions(changeset, %{"mentions" => mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, %{mentions: mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(tag) do
Tag.changeset(%Tag{}, tag)
end
defp process_mention(tag) do
Mention.changeset(%Mention{}, tag)
end
end

View File

@ -6,22 +6,31 @@ defmodule Mobilizon.Events.Event do
use Ecto.Schema
import Ecto.Changeset
alias Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Addresses
alias Mobilizon.Events.{
EventOptions,
EventStatus,
EventVisibility,
JoinOptions,
EventParticipantStats,
Participant,
Session,
Tag,
Track
}
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@type t :: %__MODULE__{
url: String.t(),
@ -47,30 +56,15 @@ defmodule Mobilizon.Events.Event do
picture: Picture.t(),
tracks: [Track.t()],
sessions: [Session.t()],
mentions: [Mention.t()],
tags: [Tag.t()],
participants: [Actor.t()]
}
@required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid]
@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,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
]
@attrs @required_attrs ++ @optional_attrs
@update_required_attrs @required_attrs
@update_optional_attrs [
:slug,
:description,
:ends_on,
@ -85,7 +79,9 @@ defmodule Mobilizon.Events.Event do
:picture_id,
:physical_address_id
]
@update_attrs @update_required_attrs ++ @update_optional_attrs
@attrs @required_attrs ++ @optional_attrs
@update_attrs @update_required_attrs ++ @optional_attrs
schema "events" do
field(:url, :string)
@ -105,13 +101,15 @@ defmodule Mobilizon.Events.Event do
field(:phone_address, :string)
field(:category, :string)
embeds_one(:options, EventOptions, on_replace: :update)
embeds_one(:options, EventOptions, on_replace: :delete)
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
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)
belongs_to(:picture, Picture)
belongs_to(:physical_address, Address, on_replace: :update)
belongs_to(:picture, Picture, on_replace: :update)
has_many(:tracks, Track)
has_many(:sessions, Session)
has_many(:mentions, Mention)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
@ -119,28 +117,41 @@ defmodule Mobilizon.Events.Event do
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
@spec changeset(t, map) :: Changeset.t()
def changeset(%__MODULE__{} = event, attrs) do
attrs = Map.update(attrs, :uuid, Ecto.UUID.generate(), & &1)
attrs = Map.update(attrs, :url, Routes.page_url(Endpoint, :event, attrs.uuid), & &1)
event
|> cast(attrs, @attrs)
|> cast_embed(:options)
|> common_changeset(attrs)
|> put_creator_if_published(:create)
|> validate_required(@required_attrs)
|> validate_lengths()
end
@doc false
@spec update_changeset(t, map) :: Ecto.Changeset.t()
@spec update_changeset(t, map) :: Changeset.t()
def update_changeset(%__MODULE__{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, @update_attrs)
|> cast_embed(:options)
|> put_tags(attrs)
|> cast(attrs, @update_attrs)
|> common_changeset(attrs)
|> put_creator_if_published(:update)
|> validate_required(@update_required_attrs)
|> validate_lengths()
end
@spec validate_lengths(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_lengths(%Ecto.Changeset{} = changeset) do
@spec common_changeset(Changeset.t(), map) :: Changeset.t()
defp common_changeset(%Changeset{} = changeset, attrs) do
changeset
|> cast_embed(:options)
|> put_tags(attrs)
|> put_address(attrs)
|> put_picture(attrs)
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)
@ -161,7 +172,80 @@ defmodule Mobilizon.Events.Event do
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
@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))
defp put_tags(%Changeset{} = changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
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}) do
case Addresses.get_address!(id) do
%Address{} = address ->
put_assoc(changeset, :physical_address, address)
_ ->
changeset
end
end
# In case it's a new address
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: %{picture_id: id} = _picture}) do
case Media.get_picture!(id) do
%Picture{} = picture ->
put_assoc(changeset, :picture, picture)
_ ->
changeset
end
end
# In case it's a new picture
defp put_picture(%Changeset{} = changeset, _attrs) do
cast_assoc(changeset, :picture)
end
# Created or updated with draft parameter: don't publish
defp put_creator_if_published(
%Changeset{changes: %{draft: true}} = changeset,
_action
) do
cast_embed(changeset, :participant_stats)
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)
end

View File

@ -0,0 +1,44 @@
defmodule Mobilizon.Events.EventParticipantStats do
@moduledoc """
Participation stats on event
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
not_approved: integer(),
rejected: integer(),
participant: integer(),
moderator: integer(),
administrator: integer(),
creator: integer()
}
@attrs [
:not_approved,
:rejected,
:participant,
:moderator,
:administrator,
:moderator,
:creator
]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:not_approved, :integer, default: 0)
field(:rejected, :integer, default: 0)
field(:participant, :integer, default: 0)
field(:moderator, :integer, default: 0)
field(:administrator, :integer, default: 0)
field(:creator, :integer, default: 0)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event_options, attrs) do
cast(event_options, attrs, @attrs)
end
end

View File

@ -7,6 +7,7 @@ defmodule Mobilizon.Events do
import Ecto.Query
import EctoEnum
alias Ecto.{Multi, Changeset}
import Mobilizon.Storage.Ecto
@ -17,6 +18,7 @@ defmodule Mobilizon.Events do
alias Mobilizon.Events.{
Comment,
Event,
EventParticipantStats,
FeedToken,
Participant,
Session,
@ -82,6 +84,8 @@ defmodule Mobilizon.Events do
@event_preloads [
:organizer_actor,
:attributed_to,
:mentions,
:sessions,
:tracks,
:tags,
@ -90,7 +94,7 @@ defmodule Mobilizon.Events do
:picture
]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment, :tags, :mentions]
@doc """
Gets a single event.
@ -235,81 +239,103 @@ defmodule Mobilizon.Events do
|> Repo.one()
end
def get_or_create_event(%{"url" => url} = attrs) do
case Repo.get_by(Event, url: url) do
%Event{} = event -> {:ok, Repo.preload(event, @event_preloads)}
nil -> create_event(attrs)
end
end
@doc """
Creates an event.
"""
@spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
@spec create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def create_event(attrs \\ %{}) do
with {:ok, %Event{draft: false} = event} <- do_create_event(attrs),
{:ok, %Participant{} = _participant} <-
create_participant(%{
actor_id: event.organizer_actor_id,
role: :creator,
event_id: event.id
}) do
with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs),
%Event{} = event <- Repo.preload(event, @event_preloads) do
Task.start(fn -> Search.insert_search_event(event) end)
{:ok, event}
else
# We don't create a creator participant if the event is a draft
{:ok, %Event{draft: true} = event} -> {:ok, event}
err -> err
end
end
@spec do_create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
# 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, Event.t()} | {:error, Changeset.t()}
defp do_create_event(attrs) do
with {:ok, %Event{} = event} <-
%Event{}
|> Event.changeset(attrs)
|> Ecto.Changeset.put_assoc(:tags, Map.get(attrs, "tags", []))
|> Repo.insert(),
%Event{} = event <-
Repo.preload(event, [:tags, :organizer_actor, :physical_address, :picture]) do
{:ok, event}
end
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, Ecto.Changeset.t()}
def update_event(
%Event{draft: old_draft_status, id: event_id, organizer_actor_id: organizer_actor_id} =
old_event,
attrs
) do
with %Ecto.Changeset{changes: changes} = changeset <-
old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do
with {:ok, %Event{draft: new_draft_status} = new_event} <- Repo.update(changeset) do
# If the event is no longer a draft
if old_draft_status == true && new_draft_status == false do
{:ok, %Participant{} = _participant} =
create_participant(%{
event_id: event_id,
role: :creator,
actor_id: organizer_actor_id
})
end
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def update_event(%Event{} = old_event, attrs) do
with %Changeset{changes: changes} = changeset <-
Event.update_changeset(Repo.preload(old_event, :tags), attrs),
{:ok, %{update: %Event{} = new_event}} <-
Multi.new()
|> Multi.update(
:update,
changeset
)
|> Multi.run(:write, fn _repo, %{update: %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() do
Cachex.del(:ics, "event_#{new_event.uuid}")
Cachex.del(:ics, "event_#{new_event.uuid}")
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event,
new_event,
changes
)
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event,
new_event,
changes
)
Task.start(fn -> Search.update_search_event(new_event) end)
Task.start(fn -> Search.update_search_event(new_event) end)
{:ok, new_event}
end
{:ok, Repo.preload(new_event, @event_preloads)}
end
end
@doc """
Deletes an event.
"""
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Changeset.t()}
def delete_event(%Event{} = event), do: Repo.delete(event)
@doc """
@ -450,7 +476,7 @@ defmodule Mobilizon.Events do
@doc """
Gets an existing tag or creates the new one.
"""
@spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(%{"name" => "#" <> title}) do
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
@ -461,10 +487,24 @@ defmodule Mobilizon.Events do
end
end
@doc """
Gets an existing tag or creates the new one.
"""
@spec get_or_create_tag(String.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(title) do
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
{:ok, tag}
nil ->
create_tag(%{"title" => title})
end
end
@doc """
Creates a tag.
"""
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def create_tag(attrs \\ %{}) do
%Tag{}
|> Tag.changeset(attrs)
@ -474,7 +514,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a tag.
"""
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def update_tag(%Tag{} = tag, attrs) do
tag
|> Tag.changeset(attrs)
@ -484,7 +524,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a tag.
"""
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def delete_tag(%Tag{} = tag), do: Repo.delete(tag)
@doc """
@ -524,7 +564,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a relation between two tags.
"""
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()}
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Changeset.t()}
def create_tag_relation(attrs \\ {}) do
%TagRelation{}
|> TagRelation.changeset(attrs)
@ -538,7 +578,7 @@ defmodule Mobilizon.Events do
Removes a tag relation.
"""
@spec delete_tag_relation(TagRelation.t()) ::
{:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()}
{:ok, TagRelation.t()} | {:error, Changeset.t()}
def delete_tag_relation(%TagRelation{} = tag_relation) do
Repo.delete(tag_relation)
end
@ -763,7 +803,7 @@ defmodule Mobilizon.Events do
end
@doc """
Counts participant participants.
Counts participant participants (participants with no extra role)
"""
@spec count_participant_participants(integer | String.t()) :: integer
def count_participant_participants(event_id) do
@ -773,17 +813,6 @@ defmodule Mobilizon.Events do
|> Repo.aggregate(:count, :id)
end
@doc """
Counts unapproved participants.
"""
@spec count_unapproved_participants(integer | String.t()) :: integer
def count_unapproved_participants(event_id) do
event_id
|> count_participants_query()
|> filter_unapproved_role()
|> Repo.aggregate(:count, :id)
end
@doc """
Counts rejected participants.
"""
@ -805,12 +834,40 @@ defmodule Mobilizon.Events do
@doc """
Creates a participant.
"""
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
def create_participant(attrs \\ %{}) do
with {:ok, %Participant{} = participant} <-
%Participant{}
|> Participant.changeset(attrs)
|> Repo.insert() do
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Changeset.t()}
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: role,
event_id: event_id
} = _participant
} ->
with {:update_event_participation_stats, true} <-
{:update_event_participation_stats, update_event_participation_stats},
{:ok, %Event{} = event} <- get_event(event_id),
%EventParticipantStats{} = participant_stats <-
Map.get(event, :participant_stats),
%EventParticipantStats{} = participant_stats <-
Map.update(participant_stats, role, 0, &(&1 + 1)),
{: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)
|> Repo.transaction() do
{:ok, Repo.preload(participant, [:event, :actor])}
end
end
@ -819,7 +876,7 @@ defmodule Mobilizon.Events do
Updates a participant.
"""
@spec update_participant(Participant.t(), map) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
{:ok, Participant.t()} | {:error, Changeset.t()}
def update_participant(%Participant{} = participant, attrs) do
participant
|> Participant.changeset(attrs)
@ -830,7 +887,7 @@ defmodule Mobilizon.Events do
Deletes a participant.
"""
@spec delete_participant(Participant.t()) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
{:ok, Participant.t()} | {:error, Changeset.t()}
def delete_participant(%Participant{} = participant), do: Repo.delete(participant)
@doc """
@ -843,7 +900,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a session.
"""
@spec create_session(map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()}
@spec create_session(map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def create_session(attrs \\ %{}) do
%Session{}
|> Session.changeset(attrs)
@ -853,7 +910,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a session.
"""
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()}
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def update_session(%Session{} = session, attrs) do
session
|> Session.changeset(attrs)