Introduce group posts

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-07-09 17:24:28 +02:00
parent bec1c69d4b
commit 9c9f1385fb
249 changed files with 11886 additions and 5023 deletions

137
lib/mobilizon/posts/post.ex Normal file
View File

@@ -0,0 +1,137 @@
defmodule Mobilizon.Posts.Post.TitleSlug do
@moduledoc """
Module to generate the slug for posts
"""
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.Posts.Post do
@moduledoc """
Module that represent Posts published by groups
"""
use Ecto.Schema
import Ecto.Changeset
alias Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Tag
alias Mobilizon.Media.Picture
alias Mobilizon.Posts.Post.TitleSlug
alias Mobilizon.Posts.PostVisibility
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes
@type t :: %__MODULE__{
url: String.t(),
local: boolean,
slug: String.t(),
body: String.t(),
title: String.t(),
draft: boolean,
visibility: PostVisibility.t(),
publish_at: DateTime.t(),
author: Actor.t(),
attributed_to: Actor.t(),
picture: Picture.t(),
tags: [Tag.t()]
}
@primary_key {:id, Ecto.UUID, autogenerate: true}
schema "posts" do
field(:body, :string)
field(:draft, :boolean, default: false)
field(:local, :boolean, default: true)
field(:slug, TitleSlug.Type)
field(:title, :string)
field(:url, :string)
field(:publish_at, :utc_datetime)
field(:visibility, PostVisibility, default_value: :public)
belongs_to(:author, Actor)
belongs_to(:attributed_to, Actor)
belongs_to(:picture, Picture, on_replace: :update)
many_to_many(:tags, Tag, join_through: "posts_tags", on_replace: :delete)
timestamps()
end
@required_attrs [
:id,
:title,
:body,
:draft,
:slug,
:url,
:author_id,
:attributed_to_id
]
@optional_attrs [:picture_id, :local, :publish_at, :visibility]
@attrs @required_attrs ++ @optional_attrs
@doc false
def changeset(%__MODULE__{} = post, attrs) do
post
|> cast(attrs, @attrs)
|> maybe_generate_id()
|> put_tags(attrs)
|> maybe_put_publish_date()
# Validate ID and title here because they're needed for slug
|> validate_required([:id, :title])
|> 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, id_and_slug} when changes in [:changes, :data] <-
fetch_field(changeset, :slug),
url <- generate_url(id_and_slug) do
put_change(changeset, :url, url)
else
_ -> changeset
end
end
@spec generate_url(String.t()) :: String.t()
defp generate_url(id_and_slug), do: Routes.page_url(Endpoint, :post, id_and_slug)
@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
defp process_tag(tag), do: Tag.changeset(%Tag{}, tag)
defp maybe_put_publish_date(%Changeset{} = changeset) do
publish_at =
if get_field(changeset, :draft, true) == false,
do: DateTime.utc_now() |> DateTime.truncate(:second),
else: nil
put_change(changeset, :publish_at, publish_at)
end
end

View File

@@ -0,0 +1,135 @@
defmodule Mobilizon.Posts do
@moduledoc """
The Posts context.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Tag
alias Mobilizon.Posts.Post
alias Mobilizon.Storage.{Page, Repo}
import Ecto.Query
require Logger
@post_preloads [:author, :attributed_to, :picture]
import EctoEnum
defenum(PostVisibility, :post_visibility, [
:public,
:unlisted,
:restricted,
:private
])
@doc """
Returns the list of recent posts for a group
"""
@spec get_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
def get_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
group_id
|> do_get_posts_for_group()
|> Page.build_page(page, limit)
end
@spec get_public_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
def get_public_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
group_id
|> do_get_posts_for_group()
|> where([p], p.visibility == ^:public and not p.draft)
|> Page.build_page(page, limit)
end
def do_get_posts_for_group(group_id) do
Post
|> where(attributed_to_id: ^group_id)
|> order_by(desc: :inserted_at)
|> preload([p], [:author, :attributed_to, :picture])
end
@doc """
Get a post by it's ID
"""
@spec get_post(integer | String.t()) :: Post.t() | nil
def get_post(nil), do: nil
def get_post(id), do: Repo.get(Post, id)
@spec get_post_with_preloads(integer | String.t()) :: Post.t() | nil
def get_post_with_preloads(id) do
Post
|> Repo.get(id)
|> Repo.preload(@post_preloads)
end
@spec get_post_by_slug(String.t()) :: Post.t() | nil
def get_post_by_slug(nil), do: nil
def get_post_by_slug(slug), do: Repo.get_by(Post, slug: slug)
@spec get_post_by_slug_with_preloads(String.t()) :: Post.t() | nil
def get_post_by_slug_with_preloads(slug) do
Post
|> Repo.get_by(slug: slug)
|> Repo.preload(@post_preloads)
end
@doc """
Get a post by it's URL
"""
@spec get_post_by_url(String.t()) :: Post.t() | nil
def get_post_by_url(url), do: Repo.get_by(Post, url: url)
@spec get_post_by_url_with_preloads(String.t()) :: Post.t() | nil
def get_post_by_url_with_preloads(url) do
Post
|> Repo.get_by(url: url)
|> Repo.preload(@post_preloads)
end
@doc """
Creates a post.
"""
@spec create_post(map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a post.
"""
@spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
def update_post(%Post{} = post, attrs) do
post
|> Repo.preload(:tags)
|> Post.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a post
"""
@spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
def delete_post(%Post{} = post), do: Repo.delete(post)
@doc """
Returns the list of tags for the post.
"""
@spec list_tags_for_post(integer | String.t()) :: [Tag.t()]
def list_tags_for_post(post_id) do
{:ok, uuid} = Ecto.UUID.dump(post_id)
uuid
|> tags_for_post_query()
|> Repo.all()
end
@spec tags_for_post_query(integer) :: Ecto.Query.t()
defp tags_for_post_query(post_id) do
from(
t in Tag,
join: p in "posts_tags",
on: t.id == p.tag_id,
where: p.post_id == ^post_id
)
end
end