Improve member adding and excluding flow

Allow to exclude a member

Send emails to the member when it's excluded

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-08-14 11:32:23 +02:00
parent ad13a57afc
commit 156eba0551
94 changed files with 2650 additions and 1862 deletions

View File

@@ -283,6 +283,7 @@ defmodule Mobilizon.Actors.Actor do
|> validate_required(@remote_actor_creation_required_attrs)
|> common_changeset(attrs)
|> unique_username_validator()
|> validate_required(:domain)
|> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100)

View File

@@ -558,7 +558,7 @@ defmodule Mobilizon.Actors do
@doc """
Gets a single member.
"""
@spec get_member(integer | String.t()) :: Member.t() | nil
@spec get_member(integer | String.t()) :: {:ok, Member.t()} | nil
def get_member(id) do
Member
|> Repo.get(id)
@@ -642,7 +642,14 @@ defmodule Mobilizon.Actors do
with {:ok, %Member{} = member} <-
%Member{}
|> Member.changeset(attrs)
|> Repo.insert() do
|> Repo.insert(
on_conflict: {:replace_all_except, [:id, :url, :actor_id, :parent_id]},
conflict_target: [:actor_id, :parent_id],
# See https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert/2-upserts,
# when doing an upsert with on_conflict, PG doesn't return whether it's an insert or upsert
# so we need to refresh the fields
returning: true
) do
{:ok, Repo.preload(member, [:actor, :parent, :invited_by])}
end
end
@@ -739,6 +746,20 @@ defmodule Mobilizon.Actors do
|> Repo.all()
end
@doc """
Returns whether the member is the last administrator for a group
"""
@spec is_only_administrator?(integer | String.t(), integer | String.t()) :: boolean()
def is_only_administrator?(member_id, group_id) do
Member
|> where(
[m],
m.parent_id == ^group_id and m.id != ^member_id and m.role in ^@administrator_roles
)
|> Repo.aggregate(:count)
|> (&(&1 == 0)).()
end
@doc """
Gets a single bot.
Raises `Ecto.NoResultsError` if the bot does not exist.
@@ -1240,7 +1261,7 @@ defmodule Mobilizon.Actors do
from(
m in Member,
where: m.actor_id == ^actor_id,
preload: [:parent]
preload: [:parent, :invited_by]
)
end

View File

@@ -6,6 +6,7 @@ defmodule Mobilizon.Discussions.Comment do
use Ecto.Schema
import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
@@ -32,7 +33,7 @@ defmodule Mobilizon.Discussions.Comment do
# When deleting an event we only nihilify everything
@required_attrs [:url]
@creation_required_attrs @required_attrs ++ [:text, :actor_id]
@creation_required_attrs @required_attrs ++ [:text, :actor_id, :published_at]
@optional_attrs [
:text,
:actor_id,
@@ -54,6 +55,7 @@ defmodule Mobilizon.Discussions.Comment do
field(:uuid, Ecto.UUID)
field(:total_replies, :integer, virtual: true, default: 0)
field(:deleted_at, :utc_datetime)
field(:published_at, :utc_datetime)
belongs_to(:actor, Actor, foreign_key: :actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
@@ -98,7 +100,6 @@ defmodule Mobilizon.Discussions.Comment do
|> 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
@@ -116,6 +117,7 @@ defmodule Mobilizon.Discussions.Comment do
defp common_changeset(%__MODULE__{} = comment, attrs) do
comment
|> cast(attrs, @attrs)
|> maybe_add_published_at()
|> maybe_generate_uuid()
|> maybe_generate_url()
|> put_tags(attrs)

View File

@@ -294,6 +294,7 @@ defmodule Mobilizon.Discussions do
Discussion
|> where([c], c.actor_id == ^actor_id)
|> preload(^@discussion_preloads)
|> order_by(desc: :updated_at)
|> Page.build_page(page, limit)
end

View File

@@ -5,7 +5,7 @@ defmodule Mobilizon.Resources.Resource do
use Ecto.Schema
import Ecto.Changeset
alias Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
import EctoEnum
defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50)
@@ -22,7 +22,8 @@ defmodule Mobilizon.Resources.Resource do
parent: __MODULE__,
actor: Actor.t(),
creator: Actor.t(),
local: boolean
local: boolean,
published_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@@ -34,6 +35,7 @@ defmodule Mobilizon.Resources.Resource do
field(:type, TypeEnum)
field(:path, :string)
field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
embeds_one :metadata, Metadata, on_replace: :delete do
field(:type, :string)
@@ -58,7 +60,7 @@ defmodule Mobilizon.Resources.Resource do
timestamps()
end
@required_attrs [:title, :url, :actor_id, :creator_id, :type, :path]
@required_attrs [:title, :url, :actor_id, :creator_id, :type, :path, :published_at]
@optional_attrs [:summary, :parent_id, :resource_url, :local]
@attrs @required_attrs ++ @optional_attrs
@metadata_attrs [
@@ -82,6 +84,7 @@ defmodule Mobilizon.Resources.Resource do
|> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2)
|> ensure_url(:resource)
|> maybe_add_published_at()
|> validate_resource_or_folder()
|> validate_required(@required_attrs)
|> unique_constraint(:url, name: :resource_url_index)

View File

@@ -22,7 +22,7 @@ defmodule Mobilizon.Resources do
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)
|> order_by(desc: :published_at)
|> preload([r], [:actor, :creator])
|> Page.build_page(page, limit)
end

View File

@@ -4,14 +4,15 @@ defmodule Mobilizon.Storage.Ecto do
"""
import Ecto.Query, warn: false
import Ecto.Changeset, only: [fetch_change: 2, put_change: 3]
import Ecto.Changeset, only: [fetch_change: 2, put_change: 3, get_field: 2]
alias Ecto.{Changeset, Query}
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes
@doc """
Adds sort to the query.
"""
@spec sort(Ecto.Query.t(), atom, atom) :: Ecto.Query.t()
@spec sort(Query.t(), atom, atom) :: Query.t()
def sort(query, sort, direction) do
from(query, order_by: [{^direction, ^sort}])
end
@@ -22,8 +23,8 @@ defmodule Mobilizon.Storage.Ecto do
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
@spec ensure_url(Changeset.t(), atom()) :: Changeset.t()
def ensure_url(%Changeset{data: %{url: nil}} = changeset, route) do
case fetch_change(changeset, :url) do
{:ok, _url} ->
changeset
@@ -33,10 +34,10 @@ defmodule Mobilizon.Storage.Ecto do
end
end
def ensure_url(%Ecto.Changeset{} = changeset, _route), do: changeset
def ensure_url(%Changeset{} = changeset, _route), do: changeset
@spec generate_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
defp generate_url(%Ecto.Changeset{} = changeset, route) do
@spec generate_url(Changeset.t(), atom()) :: Changeset.t()
defp generate_url(%Changeset{} = changeset, route) do
uuid = Ecto.UUID.generate()
changeset
@@ -46,4 +47,13 @@ defmodule Mobilizon.Storage.Ecto do
apply(Routes, String.to_existing_atom("page_url"), [Endpoint, route, uuid])
)
end
@spec maybe_add_published_at(Changeset.t()) :: Changeset.t()
def maybe_add_published_at(%Changeset{} = changeset) do
if is_nil(get_field(changeset, :published_at)) do
put_change(changeset, :published_at, DateTime.utc_now() |> DateTime.truncate(:second))
else
changeset
end
end
end

View File

@@ -5,7 +5,7 @@ defmodule Mobilizon.Todos.Todo do
use Ecto.Schema
import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor
alias Mobilizon.Todos.TodoList
@@ -16,7 +16,8 @@ defmodule Mobilizon.Todos.Todo do
todo_list: TodoList.t(),
creator: Actor.t(),
assigned_to: Actor.t(),
local: boolean
local: boolean,
published_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@@ -26,6 +27,7 @@ defmodule Mobilizon.Todos.Todo do
field(:url, :string)
field(:due_date, :utc_datetime)
field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
belongs_to(:todo_list, TodoList, type: :binary_id)
belongs_to(:creator, Actor)
belongs_to(:assigned_to, Actor)
@@ -33,7 +35,7 @@ defmodule Mobilizon.Todos.Todo do
timestamps()
end
@required_attrs [:title, :creator_id, :url, :todo_list_id]
@required_attrs [:title, :creator_id, :url, :todo_list_id, :published_at]
@optional_attrs [:status, :due_date, :assigned_to_id, :local]
@attrs @required_attrs ++ @optional_attrs
@@ -42,6 +44,7 @@ defmodule Mobilizon.Todos.Todo do
todo
|> cast(attrs, @attrs)
|> ensure_url(:todo)
|> maybe_add_published_at()
|> validate_required(@required_attrs)
end
end

View File

@@ -5,7 +5,7 @@ defmodule Mobilizon.Todos.TodoList do
use Ecto.Schema
import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2]
import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor
alias Mobilizon.Todos.Todo
@@ -13,7 +13,8 @@ defmodule Mobilizon.Todos.TodoList do
title: String.t(),
todos: [Todo.t()],
actor: Actor.t(),
local: boolean
local: boolean,
published_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@@ -21,14 +22,14 @@ defmodule Mobilizon.Todos.TodoList do
field(:title, :string)
field(:url, :string)
field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
belongs_to(:actor, Actor)
has_many(:todos, Todo)
timestamps()
end
@required_attrs [:title, :url, :actor_id]
@required_attrs [:title, :url, :actor_id, :published_at]
@optional_attrs [:local]
@attrs @required_attrs ++ @optional_attrs
@@ -37,6 +38,7 @@ defmodule Mobilizon.Todos.TodoList do
todo_list
|> cast(attrs, @attrs)
|> ensure_url(:todo_list)
|> maybe_add_published_at()
|> validate_required(@required_attrs)
end
end