Merge branch 'feature/add-cli-commands' into 'master'

Added mix commands to manage users and view actors

Closes #314

See merge request framasoft/mobilizon!329
This commit is contained in:
Thomas Citharel 2019-11-21 16:23:18 +01:00
commit 51fe96a7eb
11 changed files with 542 additions and 9 deletions

View file

@ -24,7 +24,8 @@ In order to move participant stats to the event table for existing events, you n
- Make tags clickable, redirecting to search - Make tags clickable, redirecting to search
- Add a different welcome message when coming from registration - Add a different welcome message when coming from registration
- Link to participation page from event page when you are an organizer - Link to participation page from event page when you are an organizer
- Added a warning on login that everything is deleted regularily - Added mix commands to manage users and view actors
- Added a warning on login that everything is deleted regularly
- Updated Occitan translations (Quentin) - Updated Occitan translations (Quentin)
- Updated French translations (Gavy, Zilverspar, ty kayn) - Updated French translations (Gavy, Zilverspar, ty kayn)
- Updated Swedish translations (Anton Strömkvist) - Updated Swedish translations (Anton Strömkvist)

View file

@ -0,0 +1,34 @@
defmodule Mix.Tasks.Mobilizon.Actors.Show do
@moduledoc """
Task to display an actor details
"""
use Mix.Task
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
@shortdoc "Show a Mobilizon user details"
@impl Mix.Task
def run([preferred_username]) do
Mix.Task.run("app.start")
case {:actor, Actors.get_actor_by_name_with_preload(preferred_username)} do
{:actor, %Actor{} = actor} ->
Mix.shell().info("""
Informations for the actor #{actor.preferred_username}:
- Type: #{actor.type}
- Domain: #{if is_nil(actor.domain), do: "Local", else: actor.domain}
- Name: #{actor.name}
- Summary: #{actor.summary}
- User: #{if is_nil(actor.user), do: "Remote", else: actor.user.email}
""")
{:actor, nil} ->
Mix.raise("Error: No such actor")
end
end
def run(_) do
Mix.raise("mobilizon.actors.show requires an username as argument")
end
end

View file

@ -0,0 +1,14 @@
defmodule Mix.Tasks.Mobilizon.Users do
@moduledoc """
Tasks to manage users
"""
use Mix.Task
@shortdoc "Manages Mobilizon users"
@impl Mix.Task
def run(_) do
Mix.shell().info("\nAvailable tasks:")
Mix.Tasks.Help.run(["--search", "mobilizon.users."])
end
end

View file

@ -0,0 +1,47 @@
defmodule Mix.Tasks.Mobilizon.Users.Delete do
@moduledoc """
Task to delete a user
"""
use Mix.Task
alias Mobilizon.Users
alias Mobilizon.Users.User
@shortdoc "Deletes a Mobilizon user"
@impl Mix.Task
def run([email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
assume_yes: :boolean
],
aliases: [
y: :assume_yes
]
)
assume_yes? = Keyword.get(options, :assume_yes, false)
Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
true <- assume_yes? or Mix.shell().yes?("Continue with deleting user #{user.email}?"),
{:ok, %User{} = user} <-
Users.delete_user(user) do
Mix.shell().info("""
The user #{user.email} has been deleted
""")
else
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
_ ->
Mix.raise("User has not been deleted.")
end
end
def run(_) do
Mix.raise("mobilizon.users.delete requires an email as argument")
end
end

View file

@ -0,0 +1,102 @@
defmodule Mix.Tasks.Mobilizon.Users.Modify do
@moduledoc """
Task to modify an existing Mobilizon user
"""
use Mix.Task
alias Mobilizon.Users
alias Mobilizon.Users.User
@shortdoc "Modify a Mobilizon user"
@impl Mix.Task
def run([email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
email: :string,
disable: :boolean,
enable: :boolean,
user: :boolean,
moderator: :boolean,
admin: :boolean
]
)
user? = Keyword.get(options, :user, false)
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
disable? = Keyword.get(options, :disable, false)
enable? = Keyword.get(options, :enable, false)
new_email = Keyword.get(options, :email)
if disable? && enable? do
Mix.raise("Can't use both --enabled and --disable options at the same time.")
end
Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
attrs <- %{},
role <- calculate_role(admin?, moderator?, user?),
attrs <- process_new_value(attrs, :mail, new_email, user.email),
attrs <- process_new_value(attrs, :role, role, user.role),
attrs <-
if(disable? && !is_nil(user.confirmed_at),
do: Map.put(attrs, :confirmed_at, nil),
else: attrs
),
attrs <-
if(enable? && is_nil(user.confirmed_at),
do: Map.put(attrs, :confirmed_at, DateTime.utc_now()),
else: attrs
),
{:makes_changes, true} <- {:makes_changes, attrs != %{}},
{:ok, %User{} = user} <- Users.update_user(user, attrs) do
Mix.shell().info("""
An user has been modified with the following information:
- email: #{user.email}
- Role: #{user.role}
- Activated: #{if user.confirmed_at, do: user.confirmed_at, else: "False"}
""")
else
{:makes_changes, false} ->
Mix.shell().info("No change has been made")
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
{:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors))
Mix.raise("User has not been modified because of the above reason.")
err ->
Mix.shell().error(inspect(err))
Mix.raise("User has not been modified because of an unknown reason.")
end
end
def run(_) do
Mix.raise("mobilizon.users.new requires an email as argument")
end
@spec process_new_value(map(), atom(), any(), any()) :: map()
defp process_new_value(attrs, attribute, new_value, old_value) do
if !is_nil(new_value) && new_value != old_value do
Map.put(attrs, attribute, new_value)
else
attrs
end
end
@spec calculate_role(boolean(), boolean(), boolean()) ::
:administrator | :moderator | :user | nil
defp calculate_role(admin?, moderator?, user?) do
cond do
admin? -> :administrator
moderator? -> :moderator
user? -> :user
true -> nil
end
end
end

View file

@ -0,0 +1,75 @@
defmodule Mix.Tasks.Mobilizon.Users.New do
@moduledoc """
Task to create a new user
"""
use Mix.Task
alias Mobilizon.Users
alias Mobilizon.Users.User
@shortdoc "Manages Mobilizon users"
@impl Mix.Task
def run([email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
password: :string,
moderator: :boolean,
admin: :boolean
],
aliases: [
p: :password
]
)
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
role =
cond do
admin? -> :administrator
moderator? -> :moderator
true -> :user
end
password =
Keyword.get(
options,
:password,
:crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16)
)
Mix.Task.run("app.start")
case Users.register(%{
email: email,
password: password,
role: role,
confirmed_at: DateTime.utc_now(),
confirmation_sent_at: nil,
confirmation_token: nil
}) do
{:ok, %User{} = user} ->
Mix.shell().info("""
An user has been created with the following information:
- email: #{user.email}
- password: #{password}
- Role: #{user.role}
The user will be prompted to create a new profile after login for the first time.
""")
{:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors))
Mix.raise("User has not been created because of the above reason.")
err ->
Mix.shell().error(inspect(err))
Mix.raise("User has not been created because of an unknown reason.")
end
end
def run(_) do
Mix.raise("mobilizon.users.new requires an email as argument")
end
end

View file

@ -0,0 +1,48 @@
defmodule Mix.Tasks.Mobilizon.Users.Show do
@moduledoc """
Task to display an user details
"""
use Mix.Task
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Actors.Actor
@shortdoc "Show a Mobilizon user details"
@impl Mix.Task
def run([email]) do
Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
actors <- Users.get_actors_for_user(user) do
Mix.shell().info("""
Informations for the user #{user.email}:
- Activated: #{user.confirmed_at}
- Role: #{user.role}
#{display_actors(actors)}
""")
else
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
end
end
def run(_) do
Mix.raise("mobilizon.users.show requires an email as argument")
end
defp display_actors([]), do: ""
defp display_actors(actors) do
"""
Identities (#{length(actors)}):
#{actors |> Enum.map(&display_actor/1) |> Enum.join("")}
"""
end
defp display_actor(%Actor{} = actor) do
"""
- @#{actor.preferred_username} / #{actor.name}
"""
end
end

View file

@ -156,7 +156,7 @@ defmodule Mobilizon.Actors do
def get_actor_by_name_with_preload(name, type \\ nil) do def get_actor_by_name_with_preload(name, type \\ nil) do
name name
|> get_actor_by_name(type) |> get_actor_by_name(type)
|> Repo.preload(:organized_events) |> Repo.preload([:organized_events, :user])
end end
@doc """ @doc """

View file

@ -137,7 +137,7 @@ defmodule Mobilizon.Users.User do
end end
@doc """ @doc """
Checks whether an user is confirmed. Checks whether an user is confirmed.
""" """
@spec is_confirmed(t) :: boolean @spec is_confirmed(t) :: boolean
def is_confirmed(%__MODULE__{confirmed_at: nil}), do: false def is_confirmed(%__MODULE__{confirmed_at: nil}), do: false
@ -154,20 +154,22 @@ defmodule Mobilizon.Users.User do
end end
@spec save_confirmation_token(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec save_confirmation_token(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp save_confirmation_token(%Ecto.Changeset{} = changeset) do defp save_confirmation_token(
case changeset do %Ecto.Changeset{valid?: true, changes: %{email: _email}} = changeset
%Ecto.Changeset{valid?: true, changes: %{email: _email}} -> ) do
now = DateTime.utc_now() case fetch_change(changeset, :confirmed_at) do
:error ->
changeset changeset
|> put_change(:confirmation_token, Crypto.random_string(@confirmation_token_length)) |> put_change(:confirmation_token, Crypto.random_string(@confirmation_token_length))
|> put_change(:confirmation_sent_at, DateTime.truncate(now, :second)) |> put_change(:confirmation_sent_at, DateTime.utc_now() |> DateTime.truncate(:second))
_ -> _ ->
changeset changeset
end end
end end
defp save_confirmation_token(%Ecto.Changeset{} = changeset), do: changeset
@spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_email(%Ecto.Changeset{} = changeset) do defp validate_email(%Ecto.Changeset{} = changeset) do
changeset = validate_length(changeset, :email, min: 3, max: 250) changeset = validate_length(changeset, :email, min: 3, max: 250)

View file

@ -0,0 +1,52 @@
defmodule Mix.Tasks.Mobilizon.ActorsTest do
use Mobilizon.DataCase
alias Mobilizon.Actors.Actor
alias Mix.Tasks.Mobilizon.Actors.Show
import Mobilizon.Factory
Mix.shell(Mix.Shell.Process)
@username "someone"
@domain "somewhere.tld"
describe "show actor" do
test "show existing local actor" do
%Actor{} = actor = insert(:actor, preferred_username: @username)
output = """
Informations for the actor #{@username}:
- Type: Person
- Domain: Local
- Name: #{actor.name}
- Summary: #{actor.summary}
- User: #{actor.user.email}
"""
Show.run([@username])
assert_received {:mix_shell, :info, [output_received]}
assert output_received == output
end
test "show existing remote actor" do
%Actor{} = actor = insert(:actor, preferred_username: @username, user: nil, domain: @domain)
output = """
Informations for the actor #{@username}:
- Type: Person
- Domain: #{@domain}
- Name: #{actor.name}
- Summary: #{actor.summary}
- User: Remote
"""
Show.run(["#{@username}@#{@domain}"])
assert_received {:mix_shell, :info, [output_received]}
assert output_received == output
end
test "show non-existing actor" do
assert_raise Mix.Error, "Error: No such actor", fn -> Show.run([@username]) end
end
end
end

158
test/tasks/users_test.exs Normal file
View file

@ -0,0 +1,158 @@
defmodule Mix.Tasks.Mobilizon.UsersTest do
use Mobilizon.DataCase
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mix.Tasks.Mobilizon.Users.{New, Delete, Show, Modify}
import Mobilizon.Factory
Mix.shell(Mix.Shell.Process)
@email "test@email.tld"
describe "create user" do
test "create with no options" do
New.run([@email])
assert {:ok, %User{email: email, role: role, confirmed_at: confirmed_at}} =
Users.get_user_by_email(@email)
assert email == @email
assert role == :user
refute is_nil(confirmed_at)
end
test "create a moderator" do
New.run([@email, "--moderator"])
assert {:ok, %User{email: email, role: role}} = Users.get_user_by_email(@email)
assert email == @email
assert role == :moderator
end
test "create an administrator" do
New.run([@email, "--admin"])
assert {:ok, %User{email: email, role: role}} = Users.get_user_by_email(@email)
assert email == @email
assert role == :administrator
end
test "create with already used email" do
insert(:user, email: @email)
assert_raise Mix.Error, "User has not been created because of the above reason.", fn ->
New.run([@email])
end
end
end
describe "delete user" do
test "delete existing user" do
insert(:user, email: @email)
Delete.run([@email, "-y"])
assert {:error, :user_not_found} == Users.get_user_by_email(@email)
end
test "delete non-existing user" do
assert_raise Mix.Error, "Error: No such user", fn -> Delete.run([@email, "-y"]) end
end
end
describe "show user" do
test "show existing user" do
%User{confirmed_at: confirmed_at, role: role} = user = insert(:user, email: @email)
actor1 = insert(:actor, user: user)
actor2 = insert(:actor, user: user)
output =
"Informations for the user #{@email}:\n - Activated: #{confirmed_at}\n - Role: #{role}\n Identities (2):\n - @#{
actor1.preferred_username
} / \n - @#{actor2.preferred_username} / \n\n\n"
Show.run([@email])
assert_received {:mix_shell, :info, [output_received]}
assert output_received == output
end
test "show non-existing user" do
assert_raise Mix.Error, "Error: No such user", fn -> Show.run([@email]) end
end
end
describe "modify user" do
test "modify existing user without any changes" do
insert(:user, email: @email)
Modify.run([@email])
assert_received {:mix_shell, :info, [output_received]}
assert output_received == "No change has been made"
end
test "promote an user to moderator" do
insert(:user, email: @email)
Modify.run([@email, "--moderator"])
assert {:ok, %User{role: role}} = Users.get_user_by_email(@email)
assert role == :moderator
end
test "promote an user to administrator" do
insert(:user, email: @email)
Modify.run([@email, "--admin"])
assert {:ok, %User{role: role}} = Users.get_user_by_email(@email)
assert role == :administrator
Modify.run([@email, "--user"])
assert {:ok, %User{role: role}} = Users.get_user_by_email(@email)
assert role == :user
end
test "disable and enable an user" do
user = insert(:user, email: @email)
Modify.run([@email, "--disable"])
assert_received {:mix_shell, :info, [output_received]}
assert output_received ==
"An user has been modified with the following information:\n - email: #{
user.email
}\n - Role: #{user.role}\n - Activated: False\n"
assert {:ok, %User{email: email, confirmed_at: confirmed_at}} =
Users.get_user_by_email(@email)
assert is_nil(confirmed_at)
Modify.run([@email, "--enable"])
assert_received {:mix_shell, :info, [output_received]}
assert {:ok, %User{email: email, confirmed_at: confirmed_at}} =
Users.get_user_by_email(@email)
assert output_received ==
"An user has been modified with the following information:\n - email: #{
user.email
}\n - Role: #{user.role}\n - Activated: #{confirmed_at}\n"
refute is_nil(confirmed_at)
Modify.run([@email, "--enable"])
assert {:ok, %User{email: email, confirmed_at: confirmed_at}} =
Users.get_user_by_email(@email)
refute is_nil(confirmed_at)
assert_received {:mix_shell, :info, [output_received]}
assert output_received == "No change has been made"
end
test "enable and disable at the same time" do
assert_raise Mix.Error,
"Can't use both --enabled and --disable options at the same time.",
fn ->
Modify.run([@email, "--disable", "--enable"])
end
end
end
end