Current section
Files
Jump to
Current section
Files
lib/ex_admin/repo.ex
defmodule ExAdmin.Repo do
@moduledoc false
require Logger
alias ExAdmin.Utils
alias ExAdmin.Helpers
alias ExAdmin.Changeset
import Ecto.Query
require IEx
def repo, do: Application.get_env(:ex_admin_runtime, :repo)
defp stringify_key(key) when is_atom(key), do: Atom.to_string(key)
defp stringify_key(key) when is_binary(key), do: key
def changeset(fun, resource, nil), do: changeset(fun, resource, %{})
def changeset(fun, resource, params) do
%ExAdmin.Changeset{}
|> changeset(fun, resource, params)
|> changeset_attributes_for(resource, params)
|> changeset_collection(resource, params)
end
def changeset(%Changeset{} = changeset, fun, resource, nil),
do: changeset(changeset, fun, resource, %{})
def changeset(%Changeset{} = changeset, fun, resource, params) do
cs = fun.(resource, param_stringify_keys(params))
Changeset.update(changeset, valid?: cs.valid?, changeset: cs, errors: cs.errors)
end
def changeset_attributes_for(%Changeset{} = changeset, resource, params) do
{new_changeset, fields} =
Enum.reduce insert_or_update_attributes_for(resource, params), {changeset, []},
fn({cs, fun, field}, {acc, fields}) ->
cs = ExAdmin.Changeset.set_data cs, params
{Changeset.update(acc, valid?: cs.valid?, dependents: {cs, fun}, errors: cs.errors), fields ++ [{field, cs}]}
end
struct(new_changeset, changeset: set_dependents(new_changeset.changeset, fields))
end
def changeset_collection(%Changeset{} = changeset, resource, params) do
{new_changeset, fields} =
Enum.reduce insert_or_update_collection(resource, params), {changeset, []},
fn({coll, fun, field}, {acc, fields}) ->
{Changeset.update(acc, dependents: {nil, fun}), fields ++ [{field, coll}]}
end
set_changeset_collection fields, new_changeset
end
def set_changeset_collection(fields, %{changeset: %{data: data}} = changeset) do
data = Enum.reduce fields, data, fn({k, v}, acc) ->
struct(acc, [{String.to_atom(k), v}])
end
struct(changeset, changeset: struct(changeset.changeset, data: data))
end
def set_changeset_collection(fields, %{changeset: %{model: data}} = changeset) do
data = Enum.reduce fields, data, fn({k, v}, acc) ->
struct(acc, [{String.to_atom(k), v}])
end
struct(changeset, changeset: struct(changeset.changeset, model: data))
end
def set_dependents(changeset, list) do
fields = Helpers.group_by(list, fn({key, _val}) -> key end)
|> Enum.map(fn({k,v}) ->
{String.to_atom(k), set_dependents_list(v)}
end)
ExAdmin.Changeset.set_data changeset, fields
end
defp set_dependents_list([]), do: []
defp set_dependents_list([{_, %{data: _}} | _] = map) do
for %{data: data, changes: changes} <- Keyword.values(map) do
struct(data, Map.to_list(changes))
end
end
defp set_dependents_list([{_, %{model: _}} | _] = map) do
for %{model: data, changes: changes} <- Keyword.values(map) do
struct(data, Map.to_list(changes))
end
end
def update(%Changeset{} = changeset) do
case repo().update changeset.changeset do
{:ok, resource} ->
for {cs, fun} <- changeset.dependents do
if cs do
dependent = if Map.get(cs.params, "id") do
repo().update!(cs)
else
repo().insert!(cs)
end
fun.(resource, dependent)
else
fun.(resource)
end
end
resource
error -> error
end
end
def update(resource, params) do
insert_or_update_collection(resource, params)
|> insert_or_update_attributes_for(params)
|> struct(Map.to_list(params))
|> repo().update
end
def insert(%ExAdmin.Changeset{} = changeset) do
case repo().insert changeset.changeset do
{:ok, resource} ->
case ExAdmin.Schema.primary_key(resource) do
nil -> resource
key ->
resource = repo().get(resource.__struct__, Map.get(resource, key))
for {cs, fun} <- changeset.dependents do
if cs do
fun.(resource, repo().insert!(cs))
else
fun.(resource)
end
end
resource
end
error -> error
end
end
def insert(resource, params) do
repo().insert!(struct(resource, params))
|> insert_or_update_collection(params)
|> insert_or_update_attributes_for(params)
end
def delete(resource, _params) do
repo().delete resource
end
def get_attrs_list(params) do
Enum.map(Map.keys(params), &(Atom.to_string &1))
|> Enum.reduce([], fn(x, acc) ->
if String.ends_with?(x, "_attributes") do
atom = String.to_atom(x)
acc ++ [{String.replace(x, "_attributes", ""), build_params_list(params[atom])}]
else
acc
end
end)
end
def get_collections(params) do
Enum.reduce params, [], fn({key, val}, acc) ->
key_str = Atom.to_string key
cond do
is_list(val) and String.ends_with?(key_str, "ids") ->
val = Enum.filter(val, &(&1 != ""))
ids = Enum.map(val, &(String.to_integer(&1)))
[{String.replace(key_str, "_id", "") |> String.to_atom, ids} | acc]
is_map(val) and String.ends_with?(key_str, "ids") ->
ids = Map.keys(val) |> Enum.map(&(Atom.to_string(&1) |> String.to_integer))
[{String.replace(key_str, "_id", "") |> String.to_atom, ids} | acc]
true ->
acc
end
end
end
def insert_or_update_collection(resource, params) do
get_collections(params)
|> Enum.reduce([], &(&2 ++ [do_collection(resource, &1)]))
end
def do_collection(resource, {assoc, ids}) do
{assoc_model, join_model} = get_assoc_model resource, assoc
assoc_instance = assoc_model.__struct__
selected_collection = for id <- ids, do: struct(assoc_instance, id: id)
fun = fn(resource) ->
id = resource.id
resource = repo().one from c in resource.__struct__, where: ^id == c.id, preload: [^assoc]
existing_ids = for ass <- Helpers.get_resource_field2(resource, assoc), do: ass.id
# removes
for id <- Utils.not_in(existing_ids, ids) do
new_model = struct(assoc_instance, id: id)
repo().delete_all get_join_model_instance(resource, assoc, join_model, new_model)
end
# insert adds
for id <- Utils.not_in(ids, existing_ids) do
new_model = struct(assoc_instance, id: id)
repo().insert! join_model_instance(resource, assoc, join_model, new_model)
end
end
{selected_collection, fun, Atom.to_string(assoc)}
end
def insert_or_update_attributes_for(resource, params) do
res = get_attrs_list(params)
|> Enum.reduce([], fn({model, items}, acc1) ->
List.keysort(items, 0)
|> Enum.with_index
|> Enum.reduce(acc1, fn({{id, item}, inx}, acc2) ->
acc2 ++ [do_attributes_for(resource, model, id, item, inx)]
end)
end)
res
end
def do_attributes_for(resource, model, id, params, inx) when id > 100000000000 or id == nil do
{assoc_model, join_model} = get_assoc_model resource, model
cs = assoc_model.changeset(assoc_model.__struct__, params)
|> attributes_for_translate_errors(resource, model, inx)
fun = fn(resource, new_model) ->
# repo.insert! join_model_instance(resource, model, join_model, new_model)
res = join_model_instance(resource, model, join_model, new_model)
repo().insert! res
end
{cs, fun, model}
end
# handle the case to destroy the association
def do_attributes_for(resource, model, _id, %{_destroy: "1"} = params, inx) do
{assoc_model, join_model} = get_assoc_model resource, model
assoc_resource = repo().get assoc_model, params[:id]
cs = assoc_model.changeset(assoc_resource, params)
|> attributes_for_translate_errors(resource, model, inx)
fun = fn(resource, new_model) ->
get_join_model_instance(resource, model, join_model, new_model)
|> repo().delete_all
end
{cs, fun, model}
end
# Handles update the attributes for an nested attributes
# For example, when resource is an instance of Contact
# model = "phone_numbers", assoc_model = PhoneNumber
#
def do_attributes_for(resource, model, _id, params, inx) do
{assoc_model, _} = get_assoc_model resource, model
assoc_resource = repo().get assoc_model, params[:id]
cs = assoc_model.changeset(assoc_resource, params)
|> attributes_for_translate_errors(resource, model, inx)
fun = fn(_, _) -> nil end
{cs, fun, model}
end
def attributes_for_translate_errors(%{errors: errors} = cs, resource, model, inx) do
errors = for {name, val} <- errors do
{"#{Helpers.model_name(resource)}[#{model}_attributes][#{inx}][#{name}]", val}
end
struct(cs, errors: errors)
end
def join_model_instance(resource, has_many_atom, join_model, new_model) do
res_model = resource.__struct__
# get the join model atom
join_table_name = get_assoc_join_name(resource, Utils.to_atom(has_many_atom))
res = struct(join_model.__struct__, [
{res_model.__schema__(:association, join_table_name).related_key, resource.id},
{new_model.__struct__.__schema__(:association, join_table_name).related_key, new_model.id}
])
res
end
def get_join_model_instance(resource, has_many_atom, join_model, new_model) do
res_model = resource.__struct__
# get the join model atom
join_table_name = get_assoc_join_name(resource, Utils.to_atom(has_many_atom))
field1 = res_model.__schema__(:association, join_table_name).related_key
field2 = new_model.__struct__.__schema__(:association, join_table_name).related_key
"from c in #{join_model}, where: c.#{field1} == #{resource.id} and " <>
"c.#{field2} == #{new_model.id}"
|> Code.eval_string([], __ENV__)
|> elem(0)
#|> repo.one
end
def get_assoc_join_name(resource, field) do
res_model = resource.__struct__
case res_model.__schema__(:association, field) do
%{through: [first, _]} -> first
_ -> {:error, :notfound}
end
end
def get_assoc_join_fields(resource, field) when is_binary(field) do
get_assoc_join_fields(resource, String.to_atom(field))
end
def get_assoc_join_fields(resource, field) do
res_model = resource.__struct__
case res_model.__schema__(:association, field) do
%{through: [first, _]} ->
# assoc_model = res_model.__schema__(:association, first).assoc_key
{:ok, {
res_model.__schema__(:association, first).related_key,
res_model.__schema__(:association, first).related.related_key,
}}
_ ->
{:error, :notfound}
end
end
def get_assoc_join_model(resource, field) when is_binary(field) do
get_assoc_join_model(resource, String.to_atom(field))
end
def get_assoc_join_model(resource, field) do
res_model = resource.__struct__
case res_model.__schema__(:association, field) do
%Ecto.Association.Has{queryable: queryable} ->
{:ok, queryable}
%{through: [first, second]} ->
{:ok, {res_model.__schema__(:association, first).related, second}}
_ ->
{:error, :notfound}
end
end
def get_assoc_model(resource, field) when is_binary(field) do
get_assoc_model resource, String.to_atom(field)
end
def get_assoc_model(resource, field) do
case get_assoc_join_model(resource, field) do
{:ok, {assoc, second}} ->
{assoc.__schema__(:association, second).related, assoc}
{:ok, assoc_model} ->
{assoc_model, field}
error ->
error
end
end
defp build_params_list(params_map) do
res = params_map
|> Map.to_list
|> Enum.map(fn({k, v}) ->
{k |> Atom.to_string |> String.to_integer, v}
end)
|> Enum.sort(&(elem(&1, 0) < elem(&2, 0)))
res
end
def param_stringify_keys(%{__struct__: _}=params), do: params
def param_stringify_keys(params) when is_map(params) do
Map.to_list(params)
|> Enum.map(fn {key, value} -> {stringify_key(key), param_stringify_keys(value)} end)
|> Enum.into(%{})
end
def param_stringify_keys(params), do: params
end