174
lib/mobilizon/discussions/comment.ex
Normal file
174
lib/mobilizon/discussions/comment.ex
Normal file
@@ -0,0 +1,174 @@
|
||||
defmodule Mobilizon.Discussions.Comment do
|
||||
@moduledoc """
|
||||
Represents an actor comment (for instance on an event or on a group).
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Mention
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
text: String.t(),
|
||||
url: String.t(),
|
||||
local: boolean,
|
||||
visibility: CommentVisibility.t(),
|
||||
uuid: Ecto.UUID.t(),
|
||||
actor: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
event: Event.t(),
|
||||
tags: [Tag.t()],
|
||||
mentions: [Mention.t()],
|
||||
in_reply_to_comment: t,
|
||||
origin_comment: t
|
||||
}
|
||||
|
||||
# When deleting an event we only nihilify everything
|
||||
@required_attrs [:url]
|
||||
@creation_required_attrs @required_attrs ++ [:text, :actor_id]
|
||||
@optional_attrs [
|
||||
:text,
|
||||
:actor_id,
|
||||
:event_id,
|
||||
:in_reply_to_comment_id,
|
||||
:origin_comment_id,
|
||||
:attributed_to_id,
|
||||
:deleted_at,
|
||||
:local,
|
||||
:discussion_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "comments" do
|
||||
field(:text, :string)
|
||||
field(:url, :string)
|
||||
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)
|
||||
belongs_to(:discussion, Discussion, type: :binary_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)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the id of the first comment in the discussion.
|
||||
"""
|
||||
@spec get_thread_id(t) :: integer
|
||||
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
|
||||
origin_comment_id || id
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = comment, attrs) do
|
||||
comment
|
||||
|> common_changeset(attrs)
|
||||
|> validate_required(@creation_required_attrs)
|
||||
end
|
||||
|
||||
def update_changeset(%__MODULE__{} = comment, attrs) do
|
||||
comment
|
||||
|> changeset(attrs)
|
||||
|
||||
# TODO handle comment edits
|
||||
# |> put_change(:edits, comment.edits + 1)
|
||||
end
|
||||
|
||||
@spec delete_changeset(t) :: Ecto.Changeset.t()
|
||||
def delete_changeset(%__MODULE__{} = comment) do
|
||||
comment
|
||||
|> change()
|
||||
|> put_change(:text, nil)
|
||||
|> put_change(:actor_id, nil)
|
||||
|> put_change(:discussion_id, nil)
|
||||
|> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second))
|
||||
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)
|
||||
|> maybe_generate_uuid()
|
||||
|> maybe_generate_url()
|
||||
|> put_tags(attrs)
|
||||
|> put_mentions(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()
|
||||
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
|
||||
102
lib/mobilizon/discussions/discussion.ex
Normal file
102
lib/mobilizon/discussions/discussion.ex
Normal file
@@ -0,0 +1,102 @@
|
||||
defmodule Mobilizon.Discussions.Discussion.TitleSlug do
|
||||
@moduledoc """
|
||||
Module to generate the slug for discussions
|
||||
"""
|
||||
use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
|
||||
|
||||
def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
|
||||
[title, ShortUUID.encode!(id)]
|
||||
|> Enum.join("-")
|
||||
|> Slugger.slugify()
|
||||
end
|
||||
|
||||
def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: ""
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Discussions.Discussion do
|
||||
@moduledoc """
|
||||
Represents a discussion
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Discussions.Discussion.TitleSlug
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
creator: Actor.t(),
|
||||
actor: Actor.t(),
|
||||
title: String.t(),
|
||||
url: String.t(),
|
||||
slug: String.t(),
|
||||
last_comment: Comment.t(),
|
||||
comments: list(Comment.t())
|
||||
}
|
||||
|
||||
@required_attrs [:actor_id, :creator_id, :title, :last_comment_id, :url, :id]
|
||||
@optional_attrs []
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||
|
||||
schema "discussions" do
|
||||
field(:title, :string)
|
||||
field(:slug, TitleSlug.Type)
|
||||
field(:url, :string)
|
||||
belongs_to(:creator, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:last_comment, Comment)
|
||||
has_many(:comments, Comment, foreign_key: :discussion_id)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = discussion, attrs) do
|
||||
discussion
|
||||
|> cast(attrs, @attrs)
|
||||
|> maybe_generate_id()
|
||||
|> validate_required([:title, :id])
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> TitleSlug.unique_constraint()
|
||||
|> maybe_generate_url()
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
defp maybe_generate_id(%Ecto.Changeset{} = changeset) do
|
||||
case fetch_field(changeset, :id) do
|
||||
res when res in [:error, {:data, nil}] ->
|
||||
put_change(changeset, :id, 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, slug} when changes in [:changes, :data] <-
|
||||
fetch_field(changeset, :slug),
|
||||
{_changes, actor_id} <-
|
||||
fetch_field(changeset, :actor_id),
|
||||
%Actor{preferred_username: preferred_username} <-
|
||||
Actors.get_actor(actor_id),
|
||||
url <- generate_url(preferred_username, slug) do
|
||||
put_change(changeset, :url, url)
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec generate_url(String.t(), String.t()) :: String.t()
|
||||
defp generate_url(preferred_username, slug),
|
||||
do: Routes.page_url(Endpoint, :discussion, preferred_username, slug)
|
||||
end
|
||||
407
lib/mobilizon/discussions/discussions.ex
Normal file
407
lib/mobilizon/discussions/discussions.ex
Normal file
@@ -0,0 +1,407 @@
|
||||
defmodule Mobilizon.Discussions do
|
||||
@moduledoc """
|
||||
The discussions context
|
||||
"""
|
||||
|
||||
import EctoEnum
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Changeset
|
||||
alias Ecto.Multi
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
|
||||
defenum(
|
||||
CommentVisibility,
|
||||
:comment_visibility,
|
||||
[
|
||||
:public,
|
||||
:unlisted,
|
||||
:private,
|
||||
:moderated,
|
||||
:invite
|
||||
]
|
||||
)
|
||||
|
||||
defenum(
|
||||
CommentModeration,
|
||||
:comment_moderation,
|
||||
[
|
||||
:allow_all,
|
||||
:moderated,
|
||||
:closed
|
||||
]
|
||||
)
|
||||
|
||||
@comment_preloads [
|
||||
:actor,
|
||||
:event,
|
||||
:attributed_to,
|
||||
:in_reply_to_comment,
|
||||
:origin_comment,
|
||||
:replies,
|
||||
:tags,
|
||||
:mentions,
|
||||
:discussion
|
||||
]
|
||||
|
||||
@discussion_preloads [
|
||||
:last_comment,
|
||||
:comments,
|
||||
:creator,
|
||||
:actor
|
||||
]
|
||||
|
||||
@public_visibility [:public, :unlisted]
|
||||
|
||||
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))
|
||||
# TODO: This was added because we don't want to count deleted comments in total_replies.
|
||||
# However, it also excludes all top-level comments with deleted replies from being selected
|
||||
# |> 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.
|
||||
"""
|
||||
@spec get_comment(integer | String.t()) :: Comment.t() | nil
|
||||
def get_comment(nil), do: nil
|
||||
def get_comment(id), do: Repo.get(Comment, id)
|
||||
|
||||
@doc """
|
||||
Gets a single comment.
|
||||
Raises `Ecto.NoResultsError` if the comment does not exist.
|
||||
"""
|
||||
@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.
|
||||
"""
|
||||
@spec get_comment_from_url(String.t()) :: Comment.t() | nil
|
||||
def get_comment_from_url(url), do: Repo.get_by(Comment, url: url)
|
||||
|
||||
@doc """
|
||||
Gets a comment by its URL.
|
||||
Raises `Ecto.NoResultsError` if the comment does not exist.
|
||||
"""
|
||||
@spec get_comment_from_url!(String.t()) :: Comment.t()
|
||||
def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url)
|
||||
|
||||
@doc """
|
||||
Gets a comment by its URL, with all associations loaded.
|
||||
"""
|
||||
@spec get_comment_from_url_with_preload(String.t()) ::
|
||||
{:ok, Comment.t()} | {:error, :comment_not_found}
|
||||
def get_comment_from_url_with_preload(url) do
|
||||
query = from(c in Comment, where: c.url == ^url)
|
||||
|
||||
comment =
|
||||
query
|
||||
|> preload_for_comment()
|
||||
|> Repo.one()
|
||||
|
||||
case comment do
|
||||
%Comment{} = comment ->
|
||||
{:ok, comment}
|
||||
|
||||
nil ->
|
||||
{:error, :comment_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a comment by its URL, with all associations loaded.
|
||||
Raises `Ecto.NoResultsError` if the comment does not exist.
|
||||
"""
|
||||
@spec get_comment_from_url_with_preload(String.t()) :: Comment.t()
|
||||
def get_comment_from_url_with_preload!(url) do
|
||||
Comment
|
||||
|> Repo.get_by!(url: url)
|
||||
|> Repo.preload(@comment_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a comment by its UUID, with all associations loaded.
|
||||
"""
|
||||
@spec get_comment_from_uuid_with_preload(String.t()) :: Comment.t()
|
||||
def get_comment_from_uuid_with_preload(uuid) do
|
||||
Comment
|
||||
|> Repo.get_by(uuid: uuid)
|
||||
|> 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)}
|
||||
nil -> create_comment(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a comment.
|
||||
"""
|
||||
@spec create_comment(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def create_comment(attrs \\ %{}) do
|
||||
with {:ok, %Comment{} = comment} <-
|
||||
%Comment{}
|
||||
|> Comment.changeset(attrs)
|
||||
|> Repo.insert(),
|
||||
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
|
||||
{:ok, comment}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a comment.
|
||||
"""
|
||||
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def update_comment(%Comment{} = comment, attrs) do
|
||||
comment
|
||||
|> Comment.update_changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
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
|
||||
comment
|
||||
|> Comment.delete_changeset()
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of public comments.
|
||||
"""
|
||||
@spec list_comments :: [Comment.t()]
|
||||
def list_comments do
|
||||
Repo.all(from(c in Comment, where: c.visibility == ^:public))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of public comments for the actor.
|
||||
"""
|
||||
@spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
actor_id
|
||||
|> public_comments_for_actor_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of comments by an actor and a list of ids.
|
||||
"""
|
||||
@spec list_comments_by_actor_and_ids(integer | String.t(), [integer | String.t()]) ::
|
||||
[Comment.t()]
|
||||
def list_comments_by_actor_and_ids(actor_id, comment_ids \\ [])
|
||||
def list_comments_by_actor_and_ids(_actor_id, []), do: []
|
||||
|
||||
def list_comments_by_actor_and_ids(actor_id, comment_ids) do
|
||||
Comment
|
||||
|> where([c], c.id in ^comment_ids)
|
||||
|> where([c], c.actor_id == ^actor_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_comments_for_discussion(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def get_comments_for_discussion(discussion_id, page \\ nil, limit \\ nil) do
|
||||
Comment
|
||||
|> where([c], c.discussion_id == ^discussion_id)
|
||||
|> order_by(asc: :inserted_at)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts local comments.
|
||||
"""
|
||||
@spec count_local_comments :: integer
|
||||
def count_local_comments, do: Repo.one(count_local_comments_query())
|
||||
|
||||
def get_discussion(discussion_id) do
|
||||
Discussion
|
||||
|> Repo.get(discussion_id)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
@spec get_discussion_by_url(String.t() | nil) :: Discussion.t() | nil
|
||||
def get_discussion_by_url(nil), do: nil
|
||||
|
||||
def get_discussion_by_url(discussion_url) do
|
||||
Discussion
|
||||
|> Repo.get_by(url: discussion_url)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
def get_discussion_by_slug(discussion_slug) do
|
||||
Discussion
|
||||
|> Repo.get_by(slug: discussion_slug)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
@spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||
Discussion
|
||||
|> where([c], c.actor_id == ^actor_id)
|
||||
|> preload(^@discussion_preloads)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a discussion.
|
||||
"""
|
||||
@spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def create_discussion(attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(
|
||||
%Comment{},
|
||||
Map.merge(attrs, %{actor_id: attrs.creator_id, attributed_to_id: attrs.actor_id})
|
||||
)
|
||||
)
|
||||
|> Multi.insert(:discussion, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Discussion.changeset(
|
||||
%Discussion{},
|
||||
Map.merge(attrs, %{last_comment_id: comment_id})
|
||||
)
|
||||
end)
|
||||
|> Multi.update(:comment_discussion, fn %{
|
||||
comment: %Comment{} = comment,
|
||||
discussion: %Discussion{
|
||||
id: discussion_id,
|
||||
url: discussion_url
|
||||
}
|
||||
} ->
|
||||
Changeset.change(comment, %{discussion_id: discussion_id, url: discussion_url})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(
|
||||
%Comment{},
|
||||
Map.merge(attrs, %{
|
||||
discussion_id: discussion_id,
|
||||
actor_id: Map.get(attrs, :creator_id, attrs.actor_id)
|
||||
})
|
||||
)
|
||||
)
|
||||
|> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Discussion.changeset(
|
||||
discussion,
|
||||
%{last_comment_id: comment_id}
|
||||
)
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
# Discussion is not updated
|
||||
{:ok, Map.put(discussion, :last_comment, comment)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a discussion. Only their title for now.
|
||||
"""
|
||||
@spec update_discussion(Discussion.t(), map()) ::
|
||||
{:ok, Discussion.t()} | {:error, Changeset.t()}
|
||||
def update_discussion(%Discussion{} = discussion, attrs \\ %{}) do
|
||||
discussion
|
||||
|> Discussion.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a discussion.
|
||||
"""
|
||||
@spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
|
||||
def delete_discussion(%Discussion{} = discussion) do
|
||||
discussion
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
defp public_comments_for_actor_query(actor_id) do
|
||||
Comment
|
||||
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
|
||||
|> order_by([c], desc: :id)
|
||||
|> 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 count_local_comments_query :: Ecto.Query.t()
|
||||
defp count_local_comments_query do
|
||||
from(
|
||||
c in Comment,
|
||||
select: count(c.id),
|
||||
where: c.local == ^true and c.visibility in ^@public_visibility
|
||||
)
|
||||
end
|
||||
|
||||
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
|
||||
|
||||
# @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
# defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads)
|
||||
end
|
||||
Reference in New Issue
Block a user