Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 71 additions & 2 deletions lib/kaffy/resource_admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Kaffy.ResourceAdmin do
alias Kaffy.ResourceSchema
alias Kaffy.Utils

@default_id_separator ":"

@moduledoc """
ResourceAdmin modules should be created for every schema you want to customize/configure in Kaffy.

Expand Down Expand Up @@ -148,7 +150,7 @@ defmodule Kaffy.ResourceAdmin do
@doc """
`ordering/1` takes a schema and returns how the entries should be ordered.

If `ordering/1` is not defined, Kaffy will return `[desc: :id]`.
If `ordering/1` is not defined, Kaffy will return `[desc: primary_key]`, or the first field of the primary key, if it's a composite.

## Examples

Expand All @@ -159,7 +161,10 @@ defmodule Kaffy.ResourceAdmin do
```
"""
def ordering(resource) do
Utils.get_assigned_value_or_default(resource, :ordering, desc: :id)
schema = resource[:schema]
[order_key | _] = ResourceSchema.primary_keys(schema)

Utils.get_assigned_value_or_default(resource, :ordering, desc: order_key)
end

@doc """
Expand Down Expand Up @@ -331,6 +336,70 @@ defmodule Kaffy.ResourceAdmin do
Utils.get_assigned_value_or_default(resource, :plural_name, default)
end

@doc """
`serialize_id/2` takes a schema and record and must return a string to be used in the URL and form values.

If `serialize_id/2` is not defined, Kaffy will concatenate multiple primary keys with `":"` as a separator.

Examples:

```elixir
# Default method with fixed keys
def serialize_id(_schema, record) do
Enum.join([record.post_id, record.tag_id], ":")
end

# ETF
def serialize_id(_schema, record) do
{record.post_id, record.tag_id}
|> :erlang.term_to_binary()
|> Base.url_encode64()
end
```
"""
def serialize_id(resource, entry) do
schema = resource[:schema]
default = schema
|> ResourceSchema.primary_keys()
|> Enum.map_join(@default_id_separator, &Map.get(entry, &1))

Utils.get_assigned_value_or_default(resource, :serialize_id, default, [entry])
end

@doc """
`deserialize_id/2` takes a schema and serialized id and must return a complete
keyword list in the form of [{:primary_key, value}, ...].

If `deserialize_id/2` is not defined, Kaffy will split multiple primary keys with `":"` as a separator.

Examples:

```elixir
# Default method with fixed keys
def deserialize_id(_schema, serialized_id) do
Enum.zip([:post_id, :tag_id], String.split(serialized_id, ":"))
end

# Deserialize from ETF
def deserialize_id(_schema, serialized_id) do
{product_id, tag_id} = serialized_id
|> Base.url_decode64!()
|> :erlang.binary_to_term()

[product_id: product_id, tag_id: tag_id]
end
```
"""
def deserialize_id(resource, id) do
schema = resource[:schema]
id_list = String.split(id, @default_id_separator)
default = schema
|> ResourceSchema.primary_keys()
|> Enum.zip(id_list)

Utils.get_assigned_value_or_default(resource, :deserialize_id, default, [id])
end

def resource_actions(resource, conn) do
Utils.get_assigned_value_or_default(resource, :resource_actions, nil, [conn], false)
end
Expand Down
12 changes: 9 additions & 3 deletions lib/kaffy/resource_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ defmodule Kaffy.ResourceForm do
opts
end

# Check if any primary key fields are nil
is_create_event = changeset.data.__struct__
|> Kaffy.ResourceSchema.primary_keys()
|> Enum.map(&Map.get(changeset.data, &1))
|> Enum.any?(&is_nil/1)

permission =
case is_nil(changeset.data.id) do
case is_create_event do
true -> Map.get(options, :create, :editable)
false -> Map.get(options, :update, :editable)
end
Expand Down Expand Up @@ -115,13 +121,13 @@ defmodule Kaffy.ResourceForm do
textarea(form, field, [value: value, rows: 4, placeholder: "JSON Content"] ++ opts)

:id ->
case Kaffy.ResourceSchema.primary_key(schema) == [field] do
case field in Kaffy.ResourceSchema.primary_keys(schema) do
true -> text_input(form, field, opts)
false -> text_or_assoc(conn, schema, form, field, opts)
end

:binary_id ->
case Kaffy.ResourceSchema.primary_key(schema) == [field] do
case field in Kaffy.ResourceSchema.primary_keys(schema) do
true -> text_input(form, field, opts)
false -> text_or_assoc(conn, schema, form, field, opts)
end
Expand Down
28 changes: 25 additions & 3 deletions lib/kaffy/resource_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ defmodule Kaffy.ResourceQuery do

def fetch_resource(conn, resource, id) do
schema = resource[:schema]
query = from(s in schema, where: s.id == ^id)

id_filter = Kaffy.ResourceAdmin.deserialize_id(resource, id)
query = from(s in schema, where: ^id_filter)

case Kaffy.ResourceAdmin.custom_show_query(conn, resource, query) do
{custom_query, opts} -> Kaffy.Utils.repo().one(custom_query, opts)
Expand All @@ -65,8 +67,13 @@ defmodule Kaffy.ResourceQuery do
def fetch_list(resource, ids) do
schema = resource[:schema]

from(s in schema, where: s.id in ^ids)
|> Kaffy.Utils.repo().all()
primary_keys = Kaffy.ResourceSchema.primary_keys(schema)
ids = Enum.map(ids, &Kaffy.ResourceAdmin.deserialize_id(resource, &1))

case build_list_query(schema, primary_keys, ids) do
{:error, error_msg} -> {:error, error_msg}
query -> Kaffy.Utils.repo().all(query)
end
end

def total_count(schema, do_cache, query, opts \\ [])
Expand Down Expand Up @@ -149,6 +156,21 @@ defmodule Kaffy.ResourceQuery do
{query, limited_query}
end

defp build_list_query(_schema, [], _key_pairs) do
{:error, "No private keys. List action not supported."}
end

defp build_list_query(schema, [primary_key], ids) do
ids = Enum.map(ids, fn [{_key, id}] -> id end)
from(s in schema, where: field(s, ^primary_key) in ^ids)
end

defp build_list_query(schema, _composite_key, key_pairs) do
Enum.reduce(key_pairs, schema, fn pair, query_acc ->
from query_acc, or_where: ^pair
end)
end

defp build_filtered_fields_query(query, []), do: query

defp build_filtered_fields_query(query, [filter | rest]) do
Expand Down
11 changes: 8 additions & 3 deletions lib/kaffy/resource_schema.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Kaffy.ResourceSchema do
@moduledoc false

def primary_key(schema) do
def primary_keys(schema) do
schema.__schema__(:primary_key)
end

Expand All @@ -26,7 +26,10 @@ defmodule Kaffy.ResourceSchema do
end

def form_fields(schema) do
to_be_removed = fields_to_be_removed(schema) ++ [:id, :inserted_at, :updated_at]
to_be_removed =
fields_to_be_removed(schema) ++
primary_keys(schema) ++
[:inserted_at, :updated_at]
Keyword.drop(fields(schema), to_be_removed)
end

Expand All @@ -35,7 +38,9 @@ defmodule Kaffy.ResourceSchema do
fields_to_be_removed(schema) ++
get_has_many_associations(schema) ++
get_has_one_assocations(schema) ++
get_many_to_many_associations(schema) ++ [:id, :inserted_at, :updated_at]
get_many_to_many_associations(schema) ++
primary_keys(schema) ++
[:inserted_at, :updated_at]

Keyword.drop(fields(schema), to_be_removed)
end
Expand Down
18 changes: 12 additions & 6 deletions lib/kaffy_web/controllers/resource_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,14 @@ defmodule KaffyWeb.ResourceController do
action_record = get_action_record(actions, action_key)
kaffy_inputs = Map.get(params, "kaffy-input", %{})

result =
case Map.get(action_record, :inputs, []) do
[] -> action_record.action.(conn, entries)
_ -> action_record.action.(conn, entries, kaffy_inputs)
end
result = case entries do
{:error, error_msg} -> {:error, error_msg}
entries ->
case Map.get(action_record, :inputs, []) do
[] -> action_record.action.(conn, entries)
_ -> action_record.action.(conn, entries, kaffy_inputs)
end
end

case result do
:ok ->
Expand Down Expand Up @@ -391,14 +394,17 @@ defmodule KaffyWeb.ResourceController do
end

defp redirect_to_resource(conn, context, resource, entry) do
my_resource = Kaffy.Utils.get_resource(conn, context, resource)
id = Kaffy.ResourceAdmin.serialize_id(my_resource, entry)

redirect(conn,
to:
Kaffy.Utils.router().kaffy_resource_path(
conn,
:show,
context,
resource,
entry.id
id
)
)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/kaffy_web/templates/resource/_table.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
</thead>

<tbody>
<%= for entry <- @entries do %>
<%= for {entry, index} <- Enum.with_index(@entries) do %>
<tr>
<td>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input select-item kaffy-resource-checkbox" id="kaffy-select-<%= entry.id %>" name="resource" value="<%= entry.id %>"/>
<label class="custom-control-label" for="kaffy-select-<%= entry.id %>"></label>
<input type="checkbox" class="custom-control-input select-item kaffy-resource-checkbox" id="kaffy-select-<%= index %>" name="resource" value="<%= Kaffy.ResourceAdmin.serialize_id(@my_resource, entry) %>"/>
<label class="custom-control-label" for="kaffy-select-<%= index %>"></label>
</div>
</td>
<%= for {field, index} <- Enum.with_index(@fields) do %>
<%= if index == 0 do %>
<td><%= link Kaffy.ResourceSchema.kaffy_field_value(@conn, entry, field), to: Kaffy.Utils.router().kaffy_resource_path(@conn, :show, @context, @resource, entry.id) %></td>
<td><%= link Kaffy.ResourceSchema.kaffy_field_value(@conn, entry, field), to: Kaffy.Utils.router().kaffy_resource_path(@conn, :show, @context, @resource, Kaffy.ResourceAdmin.serialize_id(@my_resource, entry)) %></td>
<% else %>
<td><%= Kaffy.ResourceSchema.kaffy_field_value(@conn, entry, field) %></td>
<% end %>
Expand Down
8 changes: 4 additions & 4 deletions lib/kaffy_web/templates/resource/show.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="card-header">
<div class="row justify-content-between">
<div class="col-auto mr-auto">
<h1><%= @resource_name %> #<%= @changeset.data.id %></h1>
<h1><%= @resource_name %> #<%= Kaffy.ResourceAdmin.serialize_id(@my_resource, @changeset.data) %></h1>
</div>
<div class="col-auto">
<%= if Kaffy.ResourceAdmin.resource_actions(@my_resource, @conn) do %>
Expand All @@ -20,7 +20,7 @@
</button>
<div class="dropdown-menu">
<%= for {action_key, options} <- Kaffy.ResourceAdmin.resource_actions(@my_resource, @conn) do %>
<%= form_tag(Kaffy.Utils.router().kaffy_resource_path(@conn, :single_action, @context, @resource, @changeset.data.id, to_string(action_key)), method: :post) %>
<%= form_tag(Kaffy.Utils.router().kaffy_resource_path(@conn, :single_action, @context, @resource, Kaffy.ResourceAdmin.serialize_id(@my_resource, @changeset.data), to_string(action_key)), method: :post) %>
<input type="submit" name="submit" value="<%= options.name %>" class="dropdown-item" />
</form>
<% end %>
Expand All @@ -32,7 +32,7 @@
</div>
</div>
<div class="card-body">
<%= form_for @changeset, Kaffy.Utils.router().kaffy_resource_path(@conn, :update, @context, @resource, @changeset.data.id), [method: :put, multipart: true], fn f -> %>
<%= form_for @changeset, Kaffy.Utils.router().kaffy_resource_path(@conn, :update, @context, @resource, Kaffy.ResourceAdmin.serialize_id(@my_resource, @changeset.data)), [method: :put, multipart: true], fn f -> %>
<%= for {field, options} <- Kaffy.ResourceAdmin.form_fields(@my_resource) do %>
<%= if options.update != :hidden do %>
<%= Kaffy.ResourceForm.kaffy_input @conn, @changeset, f, field, options %>
Expand All @@ -53,7 +53,7 @@
</div>

<!-- Modal -->
<%= form_for @changeset, Kaffy.Utils.router().kaffy_resource_path(@conn, :delete, @context, @resource, @changeset.data.id), [method: :delete], fn _f -> %>
<%= form_for @changeset, Kaffy.Utils.router().kaffy_resource_path(@conn, :delete, @context, @resource, Kaffy.ResourceAdmin.serialize_id(@my_resource, @changeset.data)), [method: :delete], fn _f -> %>
<div class="modal fade" id="delete-modal" tabindex="-1" role="dialog" aria-labelledby="delete-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
Expand Down
63 changes: 63 additions & 0 deletions test/actions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,35 @@ defmodule ActionsTest do
end
end

defmodule ActionsCompositeKeyAdmin do
@string_action "test_action"
@response %{product_id: 1, tag_id: 1}

def index(_), do: []

def resource_actions(_conn) do
%{
@string_action => %{
name: "Test Action",
action: fn _, _ ->
{:ok, @response}
end
}
}
end

def list_actions(_conn) do
%{
@string_action => %{
name: "Test Action",
action: fn _, _ ->
:ok
end
}
}
end
end

defmodule FakeSchema do
use Ecto.Schema

Expand Down Expand Up @@ -99,6 +128,12 @@ defmodule ActionsTest do
resources: [
test: [schema: FakeSchema, admin: ActionsMapAdmin]
]
],
composite: [
name: "composite",
resources: [
test: [schema: KaffyTest.Schemas.Owner, admin: ActionsCompositeKeyAdmin]
]
]
]
end)
Expand Down Expand Up @@ -140,4 +175,32 @@ defmodule ActionsTest do
assert %{"success" => _} = get_flash(result_conn)
end
end

test "single action handles composite primary keys", %{conn: conn} do
with_mock Kaffy.ResourceQuery, fetch_resource: fn _, _, _ -> %{product_id: 1, tag_id: 1} end do
result_conn =
ResourceController.single_action(conn, %{
"context" => "composite",
"resource" => "test",
"action_key" => "test_action",
"id" => "1:1"
})

assert %{"success" => _} = get_flash(result_conn)
end
end

test "list action handles composite primary keys", %{conn: conn} do
with_mock Kaffy.ResourceQuery, fetch_list: fn _, _ -> [%{product_id: 1, tag_id: 1}, %{product_id: 1, tag_id: 2}] end do
result_conn =
ResourceController.list_action(conn, %{
"context" => "composite",
"resource" => "test",
"action_key" => "test_action",
"id" => "1:1,1:2"
})

assert %{"success" => _} = get_flash(result_conn)
end
end
end
Loading