Hello, beginner here and looking for possible code refactor/simplification:
# UserController
def index(conn, _params) do
users = Accounts.list_users() |> Accounts.user_preload_role()
# can I pipe this into the above instead?
# basically want superusers hidden from non super-users
users =
if !conn.assigns.is_super_user do
users |> Enum.filter(fn u -> !u.role.is_super end)
else
users
end
render(conn, "index.html", users: users)
end
# Accounts context
def list_users do
Repo.all(User)
end
def user_preload_role(user), do: Repo.preload(user, :role) # is preloading this way dumb?
I’m interested in how you would approach this problem.
def index(conn, _params) do
users
= Accounts.list_users()
|> Accounts.user_preload_role()
|> Enum.filter(fn user ->
should_user_be_shown(user, conn.assigns.is_super_user)
end)
render(conn, "index.html", users: users)
end
defp should_user_be_shown(_user, true=_is_admin), do: true
defp should_user_be_shown(user,_is_admin), do: !user.role.is_super_user
Makes the logic test for whether a use should be shown more explicit, plus you can pipe it. You could move the preload call into Accounts.list_users_with_roles. You could also use the & notation for calling functions in the Enum.filter call to reduce a bit of visual clutter, but typing this out on iPhone has been a bit tedious so I’ll leave that to you!
In addition to what has been said, I would prefer to filter at the db level, and not on the result of a db call… Imagine You also want some pagination, this will break.
And this is working, but it does not really work as You think…
In fact You pass a list of users, not a single user.
To make it work at db level, You need to know Ecto, and pass some criteria.
This can be done by separating list_user in list_user_query. Something like this…
users =
if conn.assigns.is_super_user do
Accounts.list_users()
else
Accounts.list_users(admin: false)
end
|> Accounts.preload_role()
def preload_role(user_or_list), do: Repo.preload(user_or_list, :role)
def list_users(criteria \\ []) do
criteria
|> list_users_query()
|> Repo.all()
end
def list_users_query(criteria) do
... # hint, use Enum reduce on criteria
end
I like to have a separate query, because it allows filtering, but also pagination, preload…
def list_users_query(criteria \\ []) do
query = from p in User
Enum.reduce(criteria, query, fn
{:limit, limit}, query ->
from p in query, limit: ^limit
{:offset, offset}, query ->
from p in query, offset: ^offset
{:admin, false}, query ->
# your code to filter non admin user
{:order, order}, query ->
from p in query, order_by: [{^order, ^@user_order_field}]
{:preload, preloads}, query ->
from p in query, preload: ^preloads
arg, query ->
Logger.info("args is not matched in query #{inspect arg}")
query
end)
end
Thanks for the answers. Takeaway is: have thinner controllers, move business logic as much as possible to context (since it can be reused for other controllers as well), do more with Ecto. I’m glad I asked.
# UserController
def index(conn, _params) do
users = Accounts.list_users_visible_for(conn.assigns.current_user, preload: [:role])
render(conn, "index.html", users: users)
end
# Accounts context
def list_users(criteria \\ []) do
criteria
|> user_query()
|> Repo.all()
end
def list_users_visible_for(%User{} = user, criteria) do
criteria
|> Keyword.put(:is_super, user.role.is_super)
|> list_users()
end
# renamed to user_query (from list_users_query) because I use this for get_user too
def user_query(criteria \\ []) do
query = from(u in User)
Enum.reduce(criteria, query, fn
{:is_super, false}, query ->
from u in query,
inner_join: r in assoc(u, :role),
where: not r.is_super
{:is_super, true}, query ->
query
{:preload, preloads}, query ->
from u in query, preload: ^preloads
arg, query ->
Logger.info("args is not matched in query #{inspect(arg)}")
query
end)
end