Introduce comments below events

Also add tomstones

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-11-15 18:36:47 +01:00
parent 45155a3bde
commit dc07f34d78
71 changed files with 2642 additions and 879 deletions

View File

@@ -29,8 +29,19 @@ defmodule Mobilizon.Events.Comment do
origin_comment: t
}
@required_attrs [:text, :actor_id, :url]
@optional_attrs [:event_id, :in_reply_to_comment_id, :origin_comment_id, :attributed_to_id]
# When deleting an event we only nihilify everything
@required_attrs [:url]
@creation_required_attrs @required_attrs ++ [:text, :actor_id]
@deletion_required_attrs @required_attrs ++ [:deleted_at]
@optional_attrs [
:text,
:actor_id,
:event_id,
:in_reply_to_comment_id,
:origin_comment_id,
:attributed_to_id,
:deleted_at
]
@attrs @required_attrs ++ @optional_attrs
schema "comments" do
@@ -39,12 +50,15 @@ defmodule Mobilizon.Events.Comment do
field(:local, :boolean, default: true)
field(:visibility, CommentVisibility, default: :public)
field(:uuid, Ecto.UUID)
field(:total_replies, :integer, virtual: true, default: 0)
field(:deleted_at, :utc_datetime)
belongs_to(:actor, Actor, foreign_key: :actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
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)
has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
@@ -62,16 +76,56 @@ defmodule Mobilizon.Events.Comment do
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do
uuid = Map.get(attrs, :uuid) || Ecto.UUID.generate()
url = Map.get(attrs, :url) || generate_url(uuid)
comment
|> common_changeset(attrs)
|> validate_required(@creation_required_attrs)
end
@spec delete_changeset(t, map) :: Ecto.Changeset.t()
def delete_changeset(%__MODULE__{} = comment, attrs) do
comment
|> common_changeset(attrs)
|> validate_required(@deletion_required_attrs)
end
@doc """
Checks whether an comment can be managed.
"""
@spec can_be_managed_by(t, integer | String.t()) :: boolean
def can_be_managed_by(%__MODULE__{actor_id: creator_actor_id}, actor_id)
when creator_actor_id == actor_id do
{:comment_can_be_managed, true}
end
def can_be_managed_by(_comment, _actor), do: {:comment_can_be_managed, false}
defp common_changeset(%__MODULE__{} = comment, attrs) do
comment
|> cast(attrs, @attrs)
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> maybe_generate_uuid()
|> maybe_generate_url()
|> put_tags(attrs)
|> put_mentions(attrs)
|> validate_required(@required_attrs)
end
@spec maybe_generate_uuid(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp maybe_generate_uuid(%Ecto.Changeset{} = changeset) do
case fetch_field(changeset, :uuid) do
:error -> put_change(changeset, :uuid, Ecto.UUID.generate())
{:data, nil} -> put_change(changeset, :uuid, Ecto.UUID.generate())
_ -> changeset
end
end
@spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
{changes, uuid} when changes in [:changes, :data] <- fetch_field(changeset, :uuid),
url <- generate_url(uuid) do
put_change(changeset, :url, url)
else
_ -> changeset
end
end
@spec generate_url(String.t()) :: String.t()

View File

@@ -14,6 +14,7 @@ defmodule Mobilizon.Events.Event do
alias Mobilizon.Addresses
alias Mobilizon.Events.{
Comment,
EventOptions,
EventStatus,
EventVisibility,
@@ -111,6 +112,7 @@ defmodule Mobilizon.Events.Event do
has_many(:tracks, Track)
has_many(:sessions, Session)
has_many(:mentions, Mention)
has_many(:comments, Comment)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)

View File

@@ -89,12 +89,21 @@ defmodule Mobilizon.Events do
:sessions,
:tracks,
:tags,
:comments,
:participants,
:physical_address,
:picture
]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment, :tags, :mentions]
@comment_preloads [
:actor,
:attributed_to,
:in_reply_to_comment,
:origin_comment,
:replies,
:tags,
:mentions
]
@doc """
Gets a single event.
@@ -1001,6 +1010,29 @@ defmodule Mobilizon.Events do
|> Repo.all()
end
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
@doc """
Query for comment dataloader
We only get first comment of thread, and count replies.
Read: https://hexdocs.pm/absinthe/ecto.html#dataloader
"""
def query(Comment, _params) do
Comment
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|> where([c, _], is_nil(c.in_reply_to_comment_id))
|> where([_, r], is_nil(r.deleted_at))
|> group_by([c], c.id)
|> select([c, r], %{c | total_replies: count(r.id)})
end
def query(queryable, _) do
queryable
end
@doc """
Gets a single comment.
"""
@@ -1015,6 +1047,15 @@ defmodule Mobilizon.Events do
@spec get_comment!(integer | String.t()) :: Comment.t()
def get_comment!(id), do: Repo.get!(Comment, id)
def get_comment_with_preload(nil), do: nil
def get_comment_with_preload(id) do
Comment
|> where(id: ^id)
|> preload_for_comment()
|> Repo.one()
end
@doc """
Gets a comment by its URL.
"""
@@ -1071,6 +1112,25 @@ defmodule Mobilizon.Events do
|> Repo.preload(@comment_preloads)
end
def get_threads(event_id) do
Comment
|> where([c, _], c.event_id == ^event_id and is_nil(c.origin_comment_id))
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|> group_by([c], c.id)
|> select([c, r], %{c | total_replies: count(r.id)})
|> Repo.all()
end
@doc """
Gets paginated replies for root comment
"""
@spec get_thread_replies(integer()) :: [Comment.t()]
def get_thread_replies(parent_id) do
parent_id
|> public_replies_for_thread_query()
|> Repo.all()
end
def get_or_create_comment(%{"url" => url} = attrs) do
case Repo.get_by(Comment, url: url) do
%Comment{} = comment -> {:ok, Repo.preload(comment, @comment_preloads)}
@@ -1103,10 +1163,20 @@ defmodule Mobilizon.Events do
end
@doc """
Deletes a comment.
Deletes a comment
But actually just empty the fields so that threads are not broken.
"""
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def delete_comment(%Comment{} = comment), do: Repo.delete(comment)
def delete_comment(%Comment{} = comment) do
comment
|> Comment.delete_changeset(%{
text: nil,
actor_id: nil,
deleted_at: DateTime.utc_now()
})
|> Repo.update()
end
@doc """
Returns the list of public comments.
@@ -1119,7 +1189,7 @@ defmodule Mobilizon.Events do
@doc """
Returns the list of public comments for the actor.
"""
@spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) ::
@spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) ::
{:ok, [Comment.t()], integer}
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
comments =
@@ -1480,6 +1550,13 @@ defmodule Mobilizon.Events do
|> preload_for_comment()
end
defp public_replies_for_thread_query(comment_id) do
Comment
|> where([c], c.origin_comment_id == ^comment_id and c.visibility in ^@public_visibility)
|> group_by([c], [c.in_reply_to_comment_id, c.id])
|> preload_for_comment()
end
@spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
defp list_participants_for_event_query(event_id) do
from(

View File

@@ -14,7 +14,7 @@ defmodule Mobilizon.Reports.Report do
@type t :: %__MODULE__{
content: String.t(),
status: ReportStatus.t(),
uri: String.t(),
url: String.t(),
reported: Actor.t(),
reporter: Actor.t(),
manager: Actor.t(),
@@ -23,17 +23,18 @@ defmodule Mobilizon.Reports.Report do
notes: [Note.t()]
}
@required_attrs [:uri, :reported_id, :reporter_id]
@optional_attrs [:content, :status, :manager_id, :event_id]
@required_attrs [:url, :reported_id, :reporter_id]
@optional_attrs [:content, :status, :manager_id, :event_id, :local]
@attrs @required_attrs ++ @optional_attrs
@timestamps_opts [type: :utc_datetime]
@derive {Jason.Encoder, only: [:status, :uri]}
@derive {Jason.Encoder, only: [:status, :url]}
schema "reports" do
field(:content, :string)
field(:status, ReportStatus, default: :open)
field(:uri, :string)
field(:url, :string)
field(:local, :boolean, default: true)
# The reported actor
belongs_to(:reported, Actor)
@@ -56,14 +57,24 @@ defmodule Mobilizon.Reports.Report do
def changeset(%__MODULE__{} = report, attrs) do
report
|> cast(attrs, @attrs)
|> maybe_generate_url()
|> maybe_put_comments(attrs)
|> validate_required(@required_attrs)
end
@doc false
@spec creation_changeset(t, map) :: Ecto.Changeset.t()
def creation_changeset(%__MODULE__{} = report, attrs) do
report
|> changeset(attrs)
|> put_assoc(:comments, attrs["comments"])
defp maybe_put_comments(%Ecto.Changeset{} = changeset, %{comments: comments}) do
put_assoc(changeset, :comments, comments)
end
defp maybe_put_comments(%Ecto.Changeset{} = changeset, _), do: changeset
@spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
url <- "#{MobilizonWeb.Endpoint.url()}/report/#{Ecto.UUID.generate()}" do
put_change(changeset, :url, url)
else
_ -> changeset
end
end
end

View File

@@ -0,0 +1,45 @@
defmodule Mobilizon.Tombstone do
@moduledoc """
Represent tombstones for deleted objects. Saves only URI
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Storage.Repo
@type t :: %__MODULE__{
uri: String.t(),
actor: Actor.t()
}
@required_attrs [:uri, :actor_id]
@optional_attrs []
@attrs @required_attrs ++ @optional_attrs
schema "tombstones" do
field(:uri, :string)
belongs_to(:actor, Actor)
timestamps()
end
@doc false
def changeset(%__MODULE__{} = tombstone, attrs) do
tombstone
|> cast(attrs, @attrs)
|> validate_required(@attrs)
end
@spec create_tombstone(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def create_tombstone(attrs) do
%__MODULE__{}
|> changeset(attrs)
|> Repo.insert(on_conflict: :replace_all, conflict_target: :uri)
end
@spec find_tombstone(String.t()) :: Ecto.Schema.t() | nil
def find_tombstone(uri) do
Repo.get_by(__MODULE__, uri: uri)
end
end