Introduce group basic federation, event new page and notifications
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -9,7 +9,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|
||||
alias Mobilizon.{Actors, Config, Crypto, Mention, Share}
|
||||
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
||||
alias Mobilizon.Events.{Comment, Event, FeedToken}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Users.User
|
||||
@@ -67,7 +68,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:summary,
|
||||
:manually_approves_followers,
|
||||
:last_refreshed_at,
|
||||
:user_id
|
||||
:user_id,
|
||||
:visibility
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@@ -92,15 +94,25 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:members_url,
|
||||
:resources_url,
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers
|
||||
:manually_approves_followers,
|
||||
:visibility
|
||||
]
|
||||
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
|
||||
@remote_actor_creation_optional_attrs
|
||||
|
||||
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
|
||||
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
|
||||
@group_creation_required_attrs [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:type,
|
||||
:preferred_username,
|
||||
:members_url
|
||||
]
|
||||
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary, :visibility]
|
||||
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
|
||||
|
||||
schema "actors" do
|
||||
@@ -110,6 +122,9 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:following_url, :string)
|
||||
field(:followers_url, :string)
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:members_url, :string)
|
||||
field(:resources_url, :string)
|
||||
field(:todos_url, :string)
|
||||
field(:type, ActorType, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
@@ -274,18 +289,13 @@ defmodule Mobilizon.Actors.Actor do
|
||||
def group_creation_changeset(%__MODULE__{} = actor, params) do
|
||||
actor
|
||||
|> cast(params, @group_creation_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> build_urls(:Group)
|
||||
|> common_changeset()
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|
||||
|> put_change(:type, :Group)
|
||||
|> unique_username_validator()
|
||||
|> validate_required(@group_creation_required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
end
|
||||
@@ -309,13 +319,14 @@ defmodule Mobilizon.Actors.Actor do
|
||||
@spec build_urls(Ecto.Changeset.t(), ActorType.t()) :: Ecto.Changeset.t()
|
||||
defp build_urls(changeset, type \\ :Person)
|
||||
|
||||
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
|
||||
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, type) do
|
||||
changeset
|
||||
|> put_change(:outbox_url, build_url(username, :outbox))
|
||||
|> put_change(:followers_url, build_url(username, :followers))
|
||||
|> put_change(:following_url, build_url(username, :following))
|
||||
|> put_change(:inbox_url, build_url(username, :inbox))
|
||||
|> put_change(:shared_inbox_url, "#{Endpoint.url()}/inbox")
|
||||
|> put_change(:members_url, if(type == :Group, do: build_url(username, :members), else: nil))
|
||||
|> put_change(:url, build_url(username, :page))
|
||||
end
|
||||
|
||||
@@ -333,14 +344,16 @@ defmodule Mobilizon.Actors.Actor do
|
||||
def build_url("relay", :page, _args),
|
||||
do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode()
|
||||
|
||||
def build_url(preferred_username, :page, args) do
|
||||
def build_url(preferred_username, endpoint, args) when endpoint in [:page, :resources] do
|
||||
endpoint = if endpoint == :page, do: :actor, else: endpoint
|
||||
|
||||
Endpoint
|
||||
|> Routes.page_url(:actor, preferred_username, args)
|
||||
|> Routes.page_url(endpoint, preferred_username, args)
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
def build_url(preferred_username, endpoint, args)
|
||||
when endpoint in [:outbox, :following, :followers] do
|
||||
when endpoint in [:outbox, :following, :followers, :members, :todos] do
|
||||
Endpoint
|
||||
|> Routes.activity_pub_url(endpoint, preferred_username, args)
|
||||
|> URI.decode()
|
||||
|
||||
@@ -43,11 +43,13 @@ defmodule Mobilizon.Actors do
|
||||
])
|
||||
|
||||
defenum(MemberRole, :member_role, [
|
||||
:invited,
|
||||
:not_approved,
|
||||
:member,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
:creator,
|
||||
:rejected
|
||||
])
|
||||
|
||||
@public_visibility [:public, :unlisted]
|
||||
@@ -341,9 +343,19 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_group(attrs \\ %{}) do
|
||||
%Actor{}
|
||||
|> Actor.group_creation_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs))
|
||||
|> Multi.insert(:add_admin_member, fn %{insert_group: group} ->
|
||||
Member.changeset(%Member{}, %{
|
||||
parent_id: group.id,
|
||||
actor_id: attrs.creator_actor_id,
|
||||
role: :administrator
|
||||
})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -354,11 +366,10 @@ defmodule Mobilizon.Actors do
|
||||
@doc """
|
||||
Lists the groups.
|
||||
"""
|
||||
@spec list_groups(integer | nil, integer | nil) :: [Actor.t()]
|
||||
@spec list_groups(integer | nil, integer | nil) :: Page.t()
|
||||
def list_groups(page \\ nil, limit \\ nil) do
|
||||
groups_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -371,6 +382,16 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member.
|
||||
"""
|
||||
@spec get_member(integer | String.t()) :: Member.t() | nil
|
||||
def get_member(id) do
|
||||
Member
|
||||
|> Repo.get(id)
|
||||
|> Repo.preload([:actor, :parent, :invited_by])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member.
|
||||
Raises `Ecto.NoResultsError` if the member does not exist.
|
||||
@@ -393,6 +414,44 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_member(integer | String.t(), integer | String.t(), list()) ::
|
||||
{:ok, Member.t()} | {:error, :member_not_found}
|
||||
def get_member(actor_id, parent_id, roles) do
|
||||
case Member
|
||||
|> where([m], m.actor_id == ^actor_id and m.parent_id == ^parent_id and m.role in ^roles)
|
||||
|> Repo.one() do
|
||||
nil ->
|
||||
{:error, :member_not_found}
|
||||
|
||||
member ->
|
||||
{:ok, member}
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_member?(integer | String.t(), integer | String.t()) :: boolean()
|
||||
def is_member?(actor_id, parent_id) do
|
||||
match?(
|
||||
{:ok, %Member{}},
|
||||
get_member(actor_id, parent_id, [
|
||||
:member,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
])
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group).
|
||||
"""
|
||||
@spec get_member_by_url(String.t()) :: Member.t() | nil
|
||||
def get_member_by_url(url) do
|
||||
Member
|
||||
|> where(url: ^url)
|
||||
|> preload([:actor, :parent, :invited_by])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a member.
|
||||
"""
|
||||
@@ -402,7 +461,7 @@ defmodule Mobilizon.Actors do
|
||||
%Member{}
|
||||
|> Member.changeset(attrs)
|
||||
|> Repo.insert() do
|
||||
{:ok, Repo.preload(member, [:actor, :parent])}
|
||||
{:ok, Repo.preload(member, [:actor, :parent, :invited_by])}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -422,36 +481,69 @@ defmodule Mobilizon.Actors do
|
||||
@spec delete_member(Member.t()) :: {:ok, Member.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_member(%Member{} = member), do: Repo.delete(member)
|
||||
|
||||
@doc """
|
||||
Returns the list of memberships for an user.
|
||||
|
||||
Default behaviour is to not return :not_approved memberships
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_event_participations_for_user(5)
|
||||
%Page{total: 3, elements: [%Participant{}, ...]}
|
||||
|
||||
"""
|
||||
@spec list_memberships_for_user(
|
||||
integer,
|
||||
integer | nil,
|
||||
integer | nil
|
||||
) :: Page.t()
|
||||
def list_memberships_for_user(user_id, page, limit) do
|
||||
user_id
|
||||
|> list_members_for_user_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of members for an actor.
|
||||
"""
|
||||
@spec list_members_for_actor(Actor.t()) :: [Member.t()]
|
||||
def list_members_for_actor(%Actor{id: actor_id}) do
|
||||
@spec list_members_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_members_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
actor_id
|
||||
|> members_for_actor_query()
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of members for a group.
|
||||
"""
|
||||
@spec list_members_for_group(Actor.t()) :: [Member.t()]
|
||||
def list_members_for_group(%Actor{id: group_id, type: :Group}) do
|
||||
@spec list_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_members_for_group(%Actor{id: group_id, type: :Group}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
|> members_for_group_query()
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_external_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_external_members_for_group(
|
||||
%Actor{id: group_id, type: :Group},
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
) do
|
||||
group_id
|
||||
|> members_for_group_query()
|
||||
|> filter_external()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of administrator members for a group.
|
||||
"""
|
||||
@spec list_administrator_members_for_group(integer | String.t(), integer | nil, integer | nil) ::
|
||||
[Member.t()]
|
||||
Page.t()
|
||||
def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do
|
||||
id
|
||||
|> administrator_members_for_group_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -909,6 +1001,17 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
end
|
||||
|
||||
@spec list_members_for_user_query(integer()) :: Ecto.Query.t()
|
||||
defp list_members_for_user_query(user_id) do
|
||||
from(
|
||||
m in Member,
|
||||
join: a in Actor,
|
||||
on: m.actor_id == a.id,
|
||||
where: a.user_id == ^user_id and m.role != ^:not_approved,
|
||||
preload: [:parent, :actor, :invited_by]
|
||||
)
|
||||
end
|
||||
|
||||
@spec members_for_actor_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp members_for_actor_query(actor_id) do
|
||||
from(
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule Mobilizon.Actors.Member do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.{Actor, MemberRole}
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
role: MemberRole.t(),
|
||||
@@ -15,13 +16,21 @@ defmodule Mobilizon.Actors.Member do
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:parent_id, :actor_id]
|
||||
@optional_attrs [:role]
|
||||
@required_attrs [:parent_id, :actor_id, :url]
|
||||
@optional_attrs [:role, :invited_by_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
@metadata_attrs []
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "members" do
|
||||
field(:role, MemberRole, default: :member)
|
||||
field(:url, :string)
|
||||
|
||||
embeds_one :metadata, Metadata, on_replace: :delete do
|
||||
# TODO : Use this space to put notes when someone is invited / requested to join
|
||||
end
|
||||
|
||||
belongs_to(:invited_by, Actor)
|
||||
belongs_to(:parent, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
@@ -44,16 +53,49 @@ defmodule Mobilizon.Actors.Member do
|
||||
@doc """
|
||||
Checks whether the member is an administrator (admin or creator) of the group.
|
||||
"""
|
||||
def is_administrator(%__MODULE__{role: :administrator}), do: {:is_admin, true}
|
||||
def is_administrator(%__MODULE__{role: :creator}), do: {:is_admin, true}
|
||||
def is_administrator(%__MODULE__{}), do: {:is_admin, false}
|
||||
def is_administrator(%__MODULE__{role: :administrator}), do: true
|
||||
def is_administrator(%__MODULE__{role: :creator}), do: true
|
||||
def is_administrator(%__MODULE__{}), do: false
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, @attrs)
|
||||
|> cast_embed(:metadata, with: &metadata_changeset/2)
|
||||
|> ensure_url()
|
||||
|> validate_required(@required_attrs)
|
||||
# On both parent_id and actor_id
|
||||
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
|
||||
|> unique_constraint(:url, name: :members_url_index)
|
||||
end
|
||||
|
||||
defp metadata_changeset(schema, params) do
|
||||
schema
|
||||
|> cast(params, @metadata_attrs)
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} ->
|
||||
changeset
|
||||
|
||||
:error ->
|
||||
generate_url(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
# Most time just go with the given URL
|
||||
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
@spec generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp generate_url(%Ecto.Changeset{} = changeset) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
changeset
|
||||
|> put_change(:id, uuid)
|
||||
|> put_change(:url, "#{Endpoint.url()}/member/#{uuid}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -139,6 +139,29 @@ defmodule Mobilizon.Config do
|
||||
:enabled
|
||||
]
|
||||
|
||||
def instance_resource_providers do
|
||||
types = get_in(Application.get_env(:mobilizon, Mobilizon.Service.ResourceProviders), [:types])
|
||||
|
||||
providers =
|
||||
get_in(Application.get_env(:mobilizon, Mobilizon.Service.ResourceProviders), [:providers])
|
||||
|
||||
providers_map = :maps.filter(fn key, _value -> key in Keyword.values(types) end, providers)
|
||||
|
||||
case Enum.count(providers_map) do
|
||||
0 ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
Enum.map(providers_map, fn {key, value} ->
|
||||
%{
|
||||
type: key,
|
||||
software: types |> Enum.find(fn {_key, val} -> val == key end) |> elem(0),
|
||||
endpoint: value
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
|
||||
def relay_actor_id, do: get_cached_value(:relay_actor_id)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Mobilizon.Events.Comment do
|
||||
defmodule Mobilizon.Conversations.Comment do
|
||||
@moduledoc """
|
||||
Represents an actor comment (for instance on an event or on a group).
|
||||
"""
|
||||
@@ -8,7 +8,8 @@ defmodule Mobilizon.Events.Comment do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Comment, CommentVisibility, Event, Tag}
|
||||
alias Mobilizon.Conversations.{Comment, CommentVisibility, Conversation}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Mention
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@@ -40,7 +41,8 @@ defmodule Mobilizon.Events.Comment do
|
||||
:origin_comment_id,
|
||||
:attributed_to_id,
|
||||
:deleted_at,
|
||||
:local
|
||||
:local,
|
||||
:conversation_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@@ -58,6 +60,7 @@ 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)
|
||||
belongs_to(:conversation, Conversation)
|
||||
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)
|
||||
@@ -81,6 +84,14 @@ defmodule Mobilizon.Events.Comment do
|
||||
|> 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
|
||||
53
lib/mobilizon/conversations/conversation.ex
Normal file
53
lib/mobilizon/conversations/conversation.ex
Normal file
@@ -0,0 +1,53 @@
|
||||
defmodule Mobilizon.Conversations.Conversation.TitleSlug do
|
||||
@moduledoc """
|
||||
Module to generate the slug for conversations
|
||||
"""
|
||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Conversations.Conversation do
|
||||
@moduledoc """
|
||||
Represents a conversation
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Conversations.Conversation.TitleSlug
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
creator: Actor.t(),
|
||||
actor: Actor.t(),
|
||||
title: String.t(),
|
||||
slug: String.t(),
|
||||
last_comment: Comment.t(),
|
||||
comments: list(Comment.t())
|
||||
}
|
||||
|
||||
@required_attrs [:actor_id, :creator_id, :title, :last_comment_id]
|
||||
@optional_attrs []
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "conversations" do
|
||||
field(:title, :string)
|
||||
field(:slug, TitleSlug.Type)
|
||||
belongs_to(:creator, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:last_comment, Comment)
|
||||
has_many(:comments, Comment, foreign_key: :conversation_id)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||
conversation
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
end
|
||||
end
|
||||
385
lib/mobilizon/conversations/conversations.ex
Normal file
385
lib/mobilizon/conversations/conversations.ex
Normal file
@@ -0,0 +1,385 @@
|
||||
defmodule Mobilizon.Conversations do
|
||||
@moduledoc """
|
||||
The conversations context
|
||||
"""
|
||||
|
||||
import EctoEnum
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Changeset
|
||||
alias Ecto.Multi
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.{Comment, Conversation}
|
||||
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
|
||||
]
|
||||
|
||||
@conversation_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))
|
||||
|> 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) ::
|
||||
{:ok, [Comment.t()], integer}
|
||||
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
comments =
|
||||
actor_id
|
||||
|> public_comments_for_actor_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|
||||
count_comments =
|
||||
actor_id
|
||||
|> count_comments_query()
|
||||
|> Repo.one()
|
||||
|
||||
{:ok, comments, count_comments}
|
||||
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_conversation(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def get_comments_for_conversation(conversation_id, page \\ nil, limit \\ nil) do
|
||||
Comment
|
||||
|> where([c], c.conversation_id == ^conversation_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_conversation(conversation_id) do
|
||||
Conversation
|
||||
|> Repo.get(conversation_id)
|
||||
|> Repo.preload(@conversation_preloads)
|
||||
end
|
||||
|
||||
@spec find_conversations_for_actor(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def find_conversations_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||
Conversation
|
||||
|> where([c], c.actor_id == ^actor_id)
|
||||
|> preload(^@conversation_preloads)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a conversation.
|
||||
"""
|
||||
@spec create_conversation(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def create_conversation(attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(%Comment{}, Map.merge(attrs, %{actor_id: attrs.creator_id}))
|
||||
)
|
||||
|> Multi.insert(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Conversation.changeset(
|
||||
%Conversation{},
|
||||
Map.merge(attrs, %{last_comment_id: comment_id})
|
||||
)
|
||||
end)
|
||||
|> Multi.update(:comment_conversation, fn %{
|
||||
comment: %Comment{} = comment,
|
||||
conversation: %Conversation{
|
||||
id: conversation_id
|
||||
}
|
||||
} ->
|
||||
Changeset.change(comment, %{conversation_id: conversation_id})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(%Comment{}, Map.merge(attrs, %{conversation_id: conversation_id}))
|
||||
)
|
||||
|> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Conversation.changeset(
|
||||
conversation,
|
||||
%{last_comment_id: comment_id}
|
||||
)
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
# For some reason conversation is not updated
|
||||
{:ok, Map.put(conversation, :last_comment, comment)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a conversation. Only their title for now.
|
||||
"""
|
||||
@spec update_conversation(Conversation.t(), map()) ::
|
||||
{:ok, Conversation.t()} | {:error, Changeset.t()}
|
||||
def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do
|
||||
conversation
|
||||
|> Conversation.changeset(attrs)
|
||||
|> Repo.update()
|
||||
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_comments_query(integer) :: Ecto.Query.t()
|
||||
defp count_comments_query(actor_id) do
|
||||
from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)
|
||||
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_conversation(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
# defp preload_for_conversation(query), do: preload(query, ^@conversation_preloads)
|
||||
end
|
||||
@@ -13,8 +13,9 @@ defmodule Mobilizon.Events.Event do
|
||||
alias Mobilizon.{Addresses, Events, Media, Mention}
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Conversations.Comment
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
Comment,
|
||||
EventOptions,
|
||||
EventParticipantStats,
|
||||
EventStatus,
|
||||
@@ -78,7 +79,8 @@ defmodule Mobilizon.Events.Event do
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
:physical_address_id,
|
||||
:attributed_to_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Conversations.CommentModeration
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
CommentModeration,
|
||||
EventOffer,
|
||||
EventParticipationCondition
|
||||
}
|
||||
@@ -25,7 +26,8 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
offers: [EventOffer.t()],
|
||||
participation_condition: [EventParticipationCondition.t()],
|
||||
show_start_time: boolean,
|
||||
show_end_time: boolean
|
||||
show_end_time: boolean,
|
||||
hide_organizer_when_group_event: boolean
|
||||
}
|
||||
|
||||
@attrs [
|
||||
@@ -38,7 +40,8 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
:comment_moderation,
|
||||
:show_participation_price,
|
||||
:show_start_time,
|
||||
:show_end_time
|
||||
:show_end_time,
|
||||
:hide_organizer_when_group_event
|
||||
]
|
||||
|
||||
@primary_key false
|
||||
@@ -54,6 +57,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
field(:show_participation_price, :boolean)
|
||||
field(:show_start_time, :boolean, default: true)
|
||||
field(:show_end_time, :boolean, default: true)
|
||||
field(:hide_organizer_when_group_event, :boolean, default: false)
|
||||
|
||||
embeds_many(:offers, EventOffer)
|
||||
embeds_many(:participation_condition, EventParticipationCondition)
|
||||
|
||||
@@ -16,7 +16,6 @@ defmodule Mobilizon.Events do
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
Comment,
|
||||
Event,
|
||||
EventParticipantStats,
|
||||
FeedToken,
|
||||
@@ -61,20 +60,6 @@ defmodule Mobilizon.Events do
|
||||
:meeting
|
||||
])
|
||||
|
||||
defenum(CommentVisibility, :comment_visibility, [
|
||||
:public,
|
||||
:unlisted,
|
||||
:private,
|
||||
:moderated,
|
||||
:invite
|
||||
])
|
||||
|
||||
defenum(CommentModeration, :comment_moderation, [
|
||||
:allow_all,
|
||||
:moderated,
|
||||
:closed
|
||||
])
|
||||
|
||||
defenum(ParticipantRole, :participant_role, [
|
||||
:not_approved,
|
||||
:not_confirmed,
|
||||
@@ -100,17 +85,6 @@ defmodule Mobilizon.Events do
|
||||
:picture
|
||||
]
|
||||
|
||||
@comment_preloads [
|
||||
:actor,
|
||||
:event,
|
||||
:attributed_to,
|
||||
:in_reply_to_comment,
|
||||
:origin_comment,
|
||||
:replies,
|
||||
:tags,
|
||||
:mentions
|
||||
]
|
||||
|
||||
@doc """
|
||||
Gets a single event.
|
||||
"""
|
||||
@@ -427,6 +401,14 @@ defmodule Mobilizon.Events do
|
||||
{:ok, events, events_count}
|
||||
end
|
||||
|
||||
@spec list_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_organized_events_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
|> event_for_group_query()
|
||||
|> 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
|
||||
@@ -796,13 +778,12 @@ defmodule Mobilizon.Events do
|
||||
DateTime.t() | nil,
|
||||
integer | nil,
|
||||
integer | nil
|
||||
) :: list(Participant.t())
|
||||
) :: Page.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.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1127,219 +1108,6 @@ 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.
|
||||
"""
|
||||
@spec get_comment(integer | String.t()) :: Comment.t()
|
||||
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.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) ::
|
||||
{:ok, [Comment.t()], integer}
|
||||
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
comments =
|
||||
actor_id
|
||||
|> public_comments_for_actor_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|
||||
count_comments =
|
||||
actor_id
|
||||
|> count_comments_query()
|
||||
|> Repo.one()
|
||||
|
||||
{:ok, comments, count_comments}
|
||||
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
|
||||
|
||||
@doc """
|
||||
Counts local comments.
|
||||
"""
|
||||
@spec count_local_comments :: integer
|
||||
def count_local_comments, do: Repo.one(count_local_comments_query())
|
||||
|
||||
@doc """
|
||||
Gets a single feed token.
|
||||
"""
|
||||
@@ -1429,6 +1197,15 @@ defmodule Mobilizon.Events do
|
||||
)
|
||||
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: [desc: :id]
|
||||
)
|
||||
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(
|
||||
@@ -1656,20 +1433,6 @@ defmodule Mobilizon.Events do
|
||||
from(s in Session, where: s.track_id == ^track_id)
|
||||
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 list_participants_for_event_query(String.t()) :: Ecto.Query.t()
|
||||
defp list_participants_for_event_query(event_id) do
|
||||
from(
|
||||
@@ -1711,20 +1474,6 @@ defmodule Mobilizon.Events do
|
||||
)
|
||||
end
|
||||
|
||||
@spec count_comments_query(integer) :: Ecto.Query.t()
|
||||
defp count_comments_query(actor_id) do
|
||||
from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)
|
||||
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 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])
|
||||
@@ -1825,6 +1574,17 @@ defmodule Mobilizon.Events do
|
||||
|> 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)
|
||||
|
||||
@@ -1833,7 +1593,4 @@ defmodule Mobilizon.Events do
|
||||
|
||||
@spec preload_for_event(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
defp preload_for_event(query), do: preload(query, ^@event_preloads)
|
||||
|
||||
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
|
||||
end
|
||||
|
||||
@@ -6,7 +6,8 @@ defmodule Mobilizon.Mention do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
||||
@@ -8,7 +8,8 @@ defmodule Mobilizon.Reports.Report do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.{Note, ReportStatus}
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
104
lib/mobilizon/resources/resource.ex
Normal file
104
lib/mobilizon/resources/resource.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule Mobilizon.Resources.Resource do
|
||||
@moduledoc """
|
||||
Represents a web resource
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.Changeset
|
||||
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
|
||||
|
||||
import EctoEnum
|
||||
defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50)
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
title: String.t(),
|
||||
summary: String.t(),
|
||||
url: String.t(),
|
||||
resource_url: String.t(),
|
||||
type: atom(),
|
||||
metadata: Mobilizon.Resources.Resource.Metadata.t(),
|
||||
children: list(__MODULE__),
|
||||
parent: __MODULE__,
|
||||
actor: Actor.t(),
|
||||
creator: Actor.t(),
|
||||
local: boolean
|
||||
}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "resource" do
|
||||
field(:summary, :string)
|
||||
field(:title, :string)
|
||||
field(:url, :string)
|
||||
field(:resource_url, :string)
|
||||
field(:type, TypeEnum)
|
||||
field(:path, :string)
|
||||
field(:local, :boolean, default: true)
|
||||
|
||||
embeds_one :metadata, Metadata, on_replace: :delete do
|
||||
field(:type, :string)
|
||||
field(:title, :string)
|
||||
field(:description, :string)
|
||||
field(:image_remote_url, :string)
|
||||
field(:width, :integer)
|
||||
field(:height, :integer)
|
||||
field(:author_name, :string)
|
||||
field(:author_url, :string)
|
||||
field(:provider_name, :string)
|
||||
field(:provider_url, :string)
|
||||
field(:html, :string)
|
||||
field(:favicon_url, :string)
|
||||
end
|
||||
|
||||
has_many(:children, __MODULE__, foreign_key: :parent_id)
|
||||
belongs_to(:parent, __MODULE__, type: :binary_id)
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:creator, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@required_attrs [:title, :url, :actor_id, :creator_id, :type, :path]
|
||||
@optional_attrs [:summary, :parent_id, :resource_url, :local]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
@metadata_attrs [
|
||||
:type,
|
||||
:title,
|
||||
:description,
|
||||
:image_remote_url,
|
||||
:width,
|
||||
:height,
|
||||
:author_name,
|
||||
:author_url,
|
||||
:provider_name,
|
||||
:provider_url,
|
||||
:html,
|
||||
:favicon_url
|
||||
]
|
||||
|
||||
@doc false
|
||||
def changeset(resource, attrs) do
|
||||
resource
|
||||
|> cast(attrs, @attrs)
|
||||
|> cast_embed(:metadata, with: &metadata_changeset/2)
|
||||
|> ensure_url(:resource)
|
||||
|> validate_resource_or_folder()
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:url, name: :resource_url_index)
|
||||
end
|
||||
|
||||
defp metadata_changeset(schema, params) do
|
||||
schema
|
||||
|> cast(params, @metadata_attrs)
|
||||
end
|
||||
|
||||
@spec validate_resource_or_folder(Changeset.t()) :: Changeset.t()
|
||||
defp validate_resource_or_folder(%Changeset{} = changeset) do
|
||||
with {status, type} when status in [:changes, :data] <- fetch_field(changeset, :type),
|
||||
true <- type != :folder do
|
||||
validate_required(changeset, [:resource_url])
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
227
lib/mobilizon/resources/resources.ex
Normal file
227
lib/mobilizon/resources/resources.ex
Normal file
@@ -0,0 +1,227 @@
|
||||
defmodule Mobilizon.Resources do
|
||||
@moduledoc """
|
||||
The Resources context.
|
||||
"""
|
||||
alias Ecto.Adapters.SQL
|
||||
alias Ecto.Multi
|
||||
alias Ecto.UUID
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
@resource_preloads [:actor, :creator, :children, :parent]
|
||||
|
||||
@doc """
|
||||
Returns the list of recent resources for a group
|
||||
"""
|
||||
@spec get_resources_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_resources_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
Resource
|
||||
|> where(actor_id: ^group_id)
|
||||
|> order_by(desc: :updated_at)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of top-level resources for a group
|
||||
"""
|
||||
def get_top_level_resources_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
get_resources_for_folder(%Resource{id: "root_something", actor_id: group_id}, page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of resources for a resource folder.
|
||||
"""
|
||||
@spec get_resources_for_folder(Resource.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_resources_for_folder(resource, page \\ nil, limit \\ nil)
|
||||
|
||||
def get_resources_for_folder(
|
||||
%Resource{id: "root_" <> _group_id, actor_id: group_id},
|
||||
page,
|
||||
limit
|
||||
) do
|
||||
Resource
|
||||
|> where([r], r.actor_id == ^group_id and is_nil(r.parent_id))
|
||||
|> order_by(asc: :type)
|
||||
|> preload([r], [:actor, :creator])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
def get_resources_for_folder(%Resource{id: resource_id}, page, limit) do
|
||||
Resource
|
||||
|> where([r], r.parent_id == ^resource_id)
|
||||
|> order_by(asc: :type)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a resource by it's ID
|
||||
"""
|
||||
@spec get_resource(integer | String.t()) :: Resource.t() | nil
|
||||
def get_resource(nil), do: nil
|
||||
def get_resource(id), do: Repo.get(Resource, id)
|
||||
|
||||
@spec get_resource_with_preloads(integer | String.t()) :: Resource.t() | nil
|
||||
def get_resource_with_preloads(id) do
|
||||
Resource
|
||||
|> Repo.get(id)
|
||||
|> Repo.preload(@resource_preloads)
|
||||
end
|
||||
|
||||
@spec get_resource_by_group_and_path_with_preloads(String.t() | integer, String.t()) ::
|
||||
Resource.t() | nil
|
||||
def get_resource_by_group_and_path_with_preloads(group_id, "/") do
|
||||
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
%Resource{
|
||||
actor_id: group_id,
|
||||
id: "root_#{group_id}",
|
||||
actor: group,
|
||||
path: "/",
|
||||
title: "Root"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def get_resource_by_group_and_path_with_preloads(group_id, path) do
|
||||
Resource
|
||||
|> Repo.get_by(actor_id: group_id, path: path)
|
||||
|> Repo.preload(@resource_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a resource by it's URL
|
||||
"""
|
||||
@spec get_resource_by_url(String.t()) :: Resource.t() | nil
|
||||
def get_resource_by_url(url), do: Repo.get_by(Resource, url: url)
|
||||
|
||||
@spec get_resource_by_url_with_preloads(String.t()) :: Resource.t() | nil
|
||||
def get_resource_by_url_with_preloads(url) do
|
||||
Resource
|
||||
|> Repo.get_by(url: url)
|
||||
|> Repo.preload(@resource_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a resource.
|
||||
"""
|
||||
@spec create_resource(map) :: {:ok, Resource.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_resource(attrs \\ %{}) do
|
||||
Multi.new()
|
||||
|> do_find_parent_path(Map.get(attrs, :parent_id))
|
||||
|> Multi.insert(:insert, fn %{find_parent_path: path} ->
|
||||
Resource.changeset(%Resource{}, Map.put(attrs, :path, "#{path}/#{attrs.title}"))
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{insert: %Resource{} = resource}} ->
|
||||
{:ok, resource}
|
||||
|
||||
{:error, operation, reason, _changes} ->
|
||||
{:error, "Error while inserting resource when #{operation} because of #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a resource.
|
||||
|
||||
Since a resource can be a folder and hold children, we do the following in a transaction:
|
||||
* Get the parent path so that we can reconstruct the path for current resource (if moved or simply renamed)
|
||||
* Update all children with the new parent path
|
||||
* Update the resource path itself
|
||||
"""
|
||||
@spec update_resource(Resource.t(), map) :: {:ok, Resource.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_resource(%Resource{title: old_title} = resource, attrs) do
|
||||
Multi.new()
|
||||
|> find_parent_path(resource, attrs)
|
||||
|> update_children(resource, attrs)
|
||||
|> Multi.update(:update, fn %{find_parent_path: path} ->
|
||||
title = Map.get(attrs, :title, old_title)
|
||||
Resource.changeset(resource, Map.put(attrs, :path, "#{path}/#{title}"))
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok,
|
||||
%{
|
||||
find_parent_path: _parent_path,
|
||||
update: %Resource{} = resource,
|
||||
update_children: children
|
||||
}} ->
|
||||
resource = Map.put(resource, :children, children)
|
||||
{:ok, resource}
|
||||
|
||||
# collect errors into record changesets
|
||||
{:error, operation, reason, _changes} ->
|
||||
{:error, "Error while updating resource when #{operation} because of #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_parent_path(Multi.t(), Resource.t(), map()) :: Multi.t()
|
||||
defp find_parent_path(
|
||||
%Multi{} = multi,
|
||||
%Resource{parent_id: old_parent_id} = _resource,
|
||||
attrs
|
||||
) do
|
||||
updated_parent_id = Map.get(attrs, :parent_id, old_parent_id)
|
||||
Logger.debug("Finding parent path for updated_parent_id #{inspect(updated_parent_id)}")
|
||||
do_find_parent_path(multi, updated_parent_id)
|
||||
end
|
||||
|
||||
@spec do_find_parent_path(Multi.t(), String.t() | nil) :: Multi.t()
|
||||
defp do_find_parent_path(%Multi{} = multi, nil),
|
||||
do: Multi.run(multi, :find_parent_path, fn _, _ -> {:ok, ""} end)
|
||||
|
||||
defp do_find_parent_path(%Multi{} = multi, parent_id) do
|
||||
Multi.run(multi, :find_parent_path, fn _repo, _changes ->
|
||||
case get_resource(parent_id) do
|
||||
%Resource{path: path} = _resource -> {:ok, path}
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec update_children(Multi.t(), Resource.t(), map()) :: Multi.t()
|
||||
defp update_children(
|
||||
%Multi{} = multi,
|
||||
%Resource{
|
||||
id: id,
|
||||
type: :folder,
|
||||
title: old_title,
|
||||
actor_id: actor_id
|
||||
},
|
||||
attrs
|
||||
) do
|
||||
title = Map.get(attrs, :title, old_title)
|
||||
|
||||
Multi.run(multi, :update_children, fn repo, %{find_parent_path: path} ->
|
||||
{:ok, uuid} = UUID.dump(id)
|
||||
|
||||
{query, params} =
|
||||
{"UPDATE resource SET path = CONCAT($1::text, title) WHERE actor_id = $2 AND parent_id = $3::uuid",
|
||||
["#{path}/#{title}/", actor_id, uuid]}
|
||||
|
||||
{:ok, _} =
|
||||
SQL.query(
|
||||
repo,
|
||||
query,
|
||||
params
|
||||
)
|
||||
|
||||
children = repo.all(from(r in Resource, where: r.parent_id == ^id))
|
||||
|
||||
{:ok, children}
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_children(multi, _, _),
|
||||
do: Multi.run(multi, :update_children, fn _, _ -> {:ok, ""} end)
|
||||
|
||||
@doc """
|
||||
Deletes a resource
|
||||
"""
|
||||
@spec delete_resource(Resource.t()) :: {:ok, Resource.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_resource(%Resource{} = resource), do: Repo.delete(resource)
|
||||
end
|
||||
@@ -4,6 +4,9 @@ defmodule Mobilizon.Storage.Ecto do
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
import Ecto.Changeset, only: [fetch_change: 2, put_change: 3]
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@doc """
|
||||
Adds sort to the query.
|
||||
@@ -12,4 +15,35 @@ defmodule Mobilizon.Storage.Ecto do
|
||||
def sort(query, sort, direction) do
|
||||
from(query, order_by: [{^direction, ^sort}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensure changeset contains an URL
|
||||
|
||||
If there's a blank URL that's because we're doing the first insert.
|
||||
Most of the time just go with the given URL.
|
||||
"""
|
||||
@spec ensure_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
def ensure_url(%Ecto.Changeset{data: %{url: nil}} = changeset, route) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} ->
|
||||
changeset
|
||||
|
||||
:error ->
|
||||
generate_url(changeset, route)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_url(%Ecto.Changeset{} = changeset, _route), do: changeset
|
||||
|
||||
@spec generate_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
defp generate_url(%Ecto.Changeset{} = changeset, route) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
changeset
|
||||
|> put_change(:id, uuid)
|
||||
|> put_change(
|
||||
:url,
|
||||
apply(Routes, String.to_existing_atom("#{to_string(route)}_url"), [Endpoint, route, uuid])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
47
lib/mobilizon/todos/todo.ex
Normal file
47
lib/mobilizon/todos/todo.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defmodule Mobilizon.Todos.Todo do
|
||||
@moduledoc """
|
||||
Represents a todo, or task
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Todos.TodoList
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
status: boolean(),
|
||||
title: String.t(),
|
||||
due_date: DateTime.t(),
|
||||
todo_list: TodoList.t(),
|
||||
creator: Actor.t(),
|
||||
assigned_to: Actor.t(),
|
||||
local: boolean
|
||||
}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "todos" do
|
||||
field(:status, :boolean, default: false)
|
||||
field(:title, :string)
|
||||
field(:url, :string)
|
||||
field(:due_date, :utc_datetime)
|
||||
field(:local, :boolean, default: true)
|
||||
belongs_to(:todo_list, TodoList, type: :binary_id)
|
||||
belongs_to(:creator, Actor)
|
||||
belongs_to(:assigned_to, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@required_attrs [:title, :creator_id, :url, :todo_list_id]
|
||||
@optional_attrs [:status, :due_date, :assigned_to_id, :local]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@doc false
|
||||
def changeset(todo, attrs) do
|
||||
todo
|
||||
|> cast(attrs, @attrs)
|
||||
|> ensure_url(:todo)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
42
lib/mobilizon/todos/todo_list.ex
Normal file
42
lib/mobilizon/todos/todo_list.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
defmodule Mobilizon.Todos.TodoList do
|
||||
@moduledoc """
|
||||
Represents a todo list, or task list
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Todos.Todo
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
title: String.t(),
|
||||
todos: [Todo.t()],
|
||||
actor: Actor.t(),
|
||||
local: boolean
|
||||
}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "todo_lists" do
|
||||
field(:title, :string)
|
||||
field(:url, :string)
|
||||
field(:local, :boolean, default: true)
|
||||
|
||||
belongs_to(:actor, Actor)
|
||||
has_many(:todos, Todo)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@required_attrs [:title, :url, :actor_id]
|
||||
@optional_attrs [:local]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@doc false
|
||||
def changeset(todo_list, attrs) do
|
||||
todo_list
|
||||
|> cast(attrs, @attrs)
|
||||
|> ensure_url(:todo_list)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
109
lib/mobilizon/todos/todos.ex
Normal file
109
lib/mobilizon/todos/todos.ex
Normal file
@@ -0,0 +1,109 @@
|
||||
defmodule Mobilizon.Todos do
|
||||
@moduledoc """
|
||||
The Todos context.
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
import Ecto.Query
|
||||
|
||||
@doc """
|
||||
Get a todo list by it's ID
|
||||
"""
|
||||
@spec get_todo_list(integer | String.t()) :: TodoList.t() | nil
|
||||
def get_todo_list(id), do: Repo.get(TodoList, id)
|
||||
|
||||
@doc """
|
||||
Get a todo list by it's URL
|
||||
"""
|
||||
@spec get_todo_list_by_url(String.t()) :: TodoList.t() | nil
|
||||
def get_todo_list_by_url(url), do: Repo.get_by(TodoList, url: url)
|
||||
|
||||
@doc """
|
||||
Returns the list of todo lists for a group.
|
||||
"""
|
||||
@spec get_todo_lists_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_todo_lists_for_group(%Actor{id: group_id, type: :Group}, page \\ nil, limit \\ nil) do
|
||||
TodoList
|
||||
|> where(actor_id: ^group_id)
|
||||
|> order_by(desc: :updated_at)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of todos for a group.
|
||||
"""
|
||||
@spec get_todos_for_todo_list(TodoList.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_todos_for_todo_list(%TodoList{id: todo_list_id}, page \\ nil, limit \\ nil) do
|
||||
Todo
|
||||
|> where(todo_list_id: ^todo_list_id)
|
||||
|> order_by(asc: :status)
|
||||
# |> order_by(desc: :updated_at)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a todo list.
|
||||
"""
|
||||
@spec create_todo_list(map) :: {:ok, TodoList.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_todo_list(attrs \\ %{}) do
|
||||
%TodoList{}
|
||||
|> TodoList.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a todo list.
|
||||
"""
|
||||
@spec update_todo_list(TodoList.t(), map) ::
|
||||
{:ok, TodoList.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_todo_list(%TodoList{} = todo_list, attrs) do
|
||||
todo_list
|
||||
|> TodoList.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a todo list
|
||||
"""
|
||||
@spec delete_todo_list(TodoList.t()) :: {:ok, TodoList.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_todo_list(%TodoList{} = todo_list), do: Repo.delete(todo_list)
|
||||
|
||||
@doc """
|
||||
Get a todo by it's ID
|
||||
"""
|
||||
@spec get_todo(integer | String.t()) :: Todo.t() | nil
|
||||
def get_todo(id), do: Repo.get(Todo, id)
|
||||
|
||||
@doc """
|
||||
Get a todo by it's URL
|
||||
"""
|
||||
@spec get_todo_by_url(String.t()) :: Todo.t() | nil
|
||||
def get_todo_by_url(url), do: Repo.get_by(Todo, url: url)
|
||||
|
||||
@doc """
|
||||
Creates a todo.
|
||||
"""
|
||||
@spec create_todo(map) :: {:ok, Todo.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_todo(attrs \\ %{}) do
|
||||
%Todo{}
|
||||
|> Todo.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a todo.
|
||||
"""
|
||||
@spec update_todo(Todo.t(), map) :: {:ok, Todo.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_todo(%Todo{} = todo, attrs) do
|
||||
todo
|
||||
|> Todo.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a todo
|
||||
"""
|
||||
@spec delete_todo(Todo.t()) :: {:ok, Todo.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_todo(%Todo{} = todo), do: Repo.delete(todo)
|
||||
end
|
||||
38
lib/mobilizon/users/setting.ex
Normal file
38
lib/mobilizon/users/setting.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule Mobilizon.Users.Setting do
|
||||
@moduledoc """
|
||||
Module to manage users settings
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@required_attrs [:user_id]
|
||||
|
||||
@optional_attrs [
|
||||
:timezone,
|
||||
:notification_on_day,
|
||||
:notification_each_week,
|
||||
:notification_before_event
|
||||
]
|
||||
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@primary_key {:user_id, :id, autogenerate: false}
|
||||
schema "user_settings" do
|
||||
field(:timezone, :string)
|
||||
field(:notification_on_day, :boolean)
|
||||
field(:notification_each_week, :boolean)
|
||||
field(:notification_before_event, :boolean)
|
||||
belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(setting, attrs) do
|
||||
setting
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ defmodule Mobilizon.Users.User do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Events.FeedToken
|
||||
alias Mobilizon.Users.UserRole
|
||||
alias Mobilizon.Users.{Setting, UserRole}
|
||||
alias Mobilizon.Web.Email.Checker
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@@ -68,6 +68,7 @@ defmodule Mobilizon.Users.User do
|
||||
belongs_to(:default_actor, Actor)
|
||||
has_many(:actors, Actor)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :user_id)
|
||||
has_one(:settings, Setting)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Mobilizon.Users do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
||||
alias Mobilizon.Web.Auth
|
||||
|
||||
@@ -44,6 +44,15 @@ defmodule Mobilizon.Users do
|
||||
@spec get_user!(integer | String.t()) :: User.t()
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@spec get_user(integer | String.t()) :: User.t() | nil
|
||||
def get_user(id), do: Repo.get(User, id)
|
||||
|
||||
def get_user_with_settings!(id) do
|
||||
User
|
||||
|> Repo.get(id)
|
||||
|> Repo.preload([:settings])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an user by its email.
|
||||
"""
|
||||
@@ -265,6 +274,96 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a settings for an user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Setting does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_setting!(123)
|
||||
%Setting{}
|
||||
|
||||
iex> get_setting!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_setting!(user_id), do: Repo.get!(Setting, user_id)
|
||||
|
||||
@spec get_setting(User.t()) :: Setting.t()
|
||||
def get_setting(%User{id: user_id}), do: get_setting(user_id)
|
||||
|
||||
@spec get_setting(String.t() | integer()) :: Setting.t()
|
||||
def get_setting(user_id), do: Repo.get(Setting, user_id)
|
||||
|
||||
@doc """
|
||||
Creates a setting.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_setting(%{field: value})
|
||||
{:ok, %Setting{}}
|
||||
|
||||
iex> create_setting(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_setting(attrs \\ %{}) do
|
||||
%Setting{}
|
||||
|> Setting.changeset(attrs)
|
||||
|> Repo.insert(
|
||||
on_conflict: {:replace_all_except, [:user_id, :inserted_at]},
|
||||
conflict_target: :user_id
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a setting.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_setting(setting, %{field: new_value})
|
||||
{:ok, %Setting{}}
|
||||
|
||||
iex> update_setting(setting, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_setting(%Setting{} = setting, attrs) do
|
||||
setting
|
||||
|> Setting.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a setting.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_setting(setting)
|
||||
{:ok, %Setting{}}
|
||||
|
||||
iex> delete_setting(setting)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_setting(%Setting{} = setting) do
|
||||
Repo.delete(setting)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking setting changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_setting(setting)
|
||||
%Ecto.Changeset{source: %Setting{}}
|
||||
|
||||
"""
|
||||
def change_setting(%Setting{} = setting) do
|
||||
Setting.changeset(setting, %{})
|
||||
end
|
||||
|
||||
@spec user_by_email_query(String.t(), boolean | nil) :: Ecto.Query.t()
|
||||
defp user_by_email_query(email, nil) do
|
||||
from(u in User,
|
||||
|
||||
Reference in New Issue
Block a user