legendary-doc-site/apps/admin/kaffy/lib/kaffy/resource_admin.ex
2020-07-27 20:28:41 +00:00

405 lines
11 KiB
Elixir

defmodule Kaffy.ResourceAdmin do
alias Kaffy.ResourceSchema
alias Kaffy.Utils
@moduledoc """
ResourceAdmin modules should be created for every schema you want to customize/configure in Kaffy.
If you have a schema like `MyApp.Products.Product`, you should create an admin module with
name `MyApp.Products.ProductAdmin` and add functions documented in this module to customize the behavior.
All functions are optional.
"""
@doc """
`index/1` takes the schema module and should return a keyword list of fields and
their options.
Supported options are `:name` and `:value`.
Both options can be a string or an anonymous function.
If a fuction is provided, the current entry is passed to it.
If index/1 is not defined, Kaffy will return all the fields of the schema and their default values.
Example:
```elixir
def index(_schema) do
[
id: %{name: "ID", value: fn post -> post.id + 100 end},
title: nil, # this will render the default name for this field (Title) and its default value (post.title)
views: %{name: "Hits", value: fn post -> post.views + 10 end},
published: %{name: "Published?", value: fn post -> published?(post) end},
comment_count: %{name: "Comments", value: fn post -> comment_count(post) end}
]
end
```
"""
def index(resource) do
schema = resource[:schema]
Utils.get_assigned_value_or_default(resource, :index, ResourceSchema.index_fields(schema))
end
@doc """
form_fields/1 takes a schema and returns a keyword list of fields and their options for the new/edit form.
Supported options are:
`:label`, `:type`, `:choices`, and `:permission`
`:type` can be any ecto type in addition to `:file` and `:textarea`
If `:choices` is provided, it must be a keyword list and
the field will be rendered as a `<select>` element regardless of the actual field type.
Setting `:permission` to `:read` will make the field non-editable. It is `:write` by default.
If you want to remove a field from being rendered, just remove it from the list.
If form_fields/1 is not defined, Kaffy will return all the fields with
their default types based on the schema.
Example:
```elixir
def form_fields(_schema) do
[
title: %{label: "Subject"},
slug: nil,
image: %{type: :file},
status: %{choices: [{"Pending", "pending"}, {"Published", "published"}]},
body: %{type: :textarea, rows: 3},
views: %{permission: :read}
]
end
```
"""
def form_fields(resource) do
schema = resource[:schema]
Utils.get_assigned_value_or_default(
resource,
:form_fields,
ResourceSchema.form_fields(schema)
)
|> set_default_field_options(schema)
end
defp set_default_field_options(fields, schema) do
Enum.map(fields, fn {f, o} ->
default_options = Kaffy.ResourceSchema.default_field_options(schema, f)
final_options = Map.merge(default_options, o || %{})
{f, final_options}
end)
end
@doc """
`search_fields/1` takes a schema and must return a list of `:string` fields to search against when typing in the search box.
If `search_fields/1` is not defined, Kaffy will return all the `:string` fields of the schema.
Example:
```elixir
def search_fields(_schema) do
[:title, :slug, :body]
end
```
"""
def search_fields(resource) do
Utils.get_assigned_value_or_default(
resource,
:search_fields,
ResourceSchema.search_fields(resource)
)
end
@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]`.
Example:
```elixir
def ordering(_schema) do
[asc: :title]
end
```
"""
def ordering(resource) do
Utils.get_assigned_value_or_default(resource, :ordering, desc: :id)
end
@doc """
`authorized?/2` takes the schema and the current Plug.Conn struct and
should return a boolean value.
Returning false will prevent the access of this resource for the current user/request.
If `authorized?/2` is not defined, Kaffy will return true.
Example:
```elixir
def authorized?(_schema, _conn) do
true
end
```
"""
def authorized?(resource, conn) do
Utils.get_assigned_value_or_default(resource, :authorized?, true, [conn])
end
@doc """
`create_changeset/2` takes the record and the changes and should return a changeset for creating a new record.
If `create_changeset/2` is not defined, Kaffy will try to call `schema.changeset/2`
and if that's not defined, `Ecto.Changeset.change/2` will be called.
Example:
```elixir
def create_changeset(schema, attrs) do
MyApp.Blog.Post.create_changeset(schema, attrs)
end
```
"""
def create_changeset(resource, changes) do
schema = resource[:schema]
schema_struct = schema.__struct__
functions = schema.__info__(:functions)
default =
case Keyword.has_key?(functions, :changeset) do
true ->
schema.changeset(schema_struct, changes)
false ->
cast_fields = Kaffy.ResourceSchema.cast_fields(schema) |> Keyword.keys()
schema_struct
|> Ecto.Changeset.cast(changes, cast_fields)
|> Ecto.Changeset.change(changes)
end
Utils.get_assigned_value_or_default(
resource,
:create_changeset,
default,
[schema.__struct__, changes],
false
)
end
@doc """
`update_changeset/2` takes the record and the changes and should return a changeset for updating an existing record.
If `update_changeset/2` is not defined, Kaffy will try to call `schema.changeset/2`
and if that's not defined, `Ecto.Changeset.change/2` will be called.
Example:
```elixir
def update_changeset(schema, attrs) do
MyApp.Blog.Post.create_changeset(schema, attrs)
end
```
"""
def update_changeset(resource, entry, changes) do
schema = resource[:schema]
functions = schema.__info__(:functions)
default =
case Keyword.has_key?(functions, :changeset) do
true ->
schema.changeset(entry, changes)
false ->
cast_fields = Kaffy.ResourceSchema.cast_fields(schema) |> Keyword.keys()
entry
|> Ecto.Changeset.cast(changes, cast_fields)
|> Ecto.Changeset.change(changes)
Ecto.Changeset.change(entry, changes)
end
Utils.get_assigned_value_or_default(
resource,
:update_changeset,
default,
[entry, changes],
false
)
end
@doc """
This function should return a string for the singular name of a resource.
If `singular_name/1` is not defined, Kaffy will use the name of
the last part of the schema module (e.g. Post in MyApp.Blog.Post)
This is useful for when you have a schema but you want to display its name differently.
If you have "Post" and you want to display "Article" for example.
Example:
```elixir
def singular_name(_schema) do
"Article"
end
```
"""
def singular_name(resource) do
default = humanize_term(resource[:schema])
Utils.get_assigned_value_or_default(resource, :singular_name, default)
end
def humanize_term(term) do
term
|> to_string()
|> String.split(".")
|> Enum.at(-1)
|> Macro.underscore()
|> String.split("_")
|> Enum.map(fn s -> String.capitalize(s) end)
|> Enum.join(" ")
end
@doc """
This is useful for names that cannot be plural by adding an "s" at the end.
Like "Category" => "Categories" or "Person" => "People".
If `plural_name/1` is not defined, Kaffy will use the singular
name and add an "s" to it (e.g. Posts).
Example:
```elixir
def plural_name(_schema) do
"Categories"
end
```
"""
def plural_name(resource) do
default = singular_name(resource) <> "s"
Utils.get_assigned_value_or_default(resource, :plural_name, default)
end
def resource_actions(resource, conn) do
Utils.get_assigned_value_or_default(resource, :resource_actions, nil, [conn], false)
end
def list_actions(resource, conn) do
Utils.get_assigned_value_or_default(resource, :list_actions, nil, [conn], false)
end
def widgets(resource, conn) do
Utils.get_assigned_value_or_default(
resource,
:widgets,
ResourceSchema.widgets(resource),
[conn]
)
end
def collect_widgets(conn) do
Enum.reduce(Kaffy.Utils.contexts(conn), [], fn c, all ->
widgets =
Enum.reduce(Kaffy.Utils.schemas_for_context(conn, c), [], fn {_, resource}, all ->
all ++ Kaffy.ResourceAdmin.widgets(resource, conn)
end)
|> Enum.map(fn widget ->
width = Map.get(widget, :width)
type = widget.type
cond do
is_nil(width) and type == "tidbit" -> Map.put(widget, :width, 3)
is_nil(width) and type == "chart" -> Map.put(widget, :width, 12)
is_nil(width) and type == "flash" -> Map.put(widget, :width, 4)
true -> Map.put_new(widget, :width, 6)
end
end)
all ++ widgets
end)
|> Enum.sort_by(fn w -> Map.get(w, :order, 999) end)
end
def custom_pages(resource, conn) do
Utils.get_assigned_value_or_default(resource, :custom_pages, [], [conn])
end
def collect_pages(conn) do
Enum.reduce(Kaffy.Utils.contexts(conn), [], fn c, all ->
all ++
Enum.reduce(Kaffy.Utils.schemas_for_context(conn, c), [], fn {_, resource}, all ->
all ++ Kaffy.ResourceAdmin.custom_pages(resource, conn)
end)
end)
|> Enum.sort_by(fn p -> Map.get(p, :order, 999) end)
end
def find_page(conn, slug) do
conn
|> collect_pages()
|> Enum.filter(fn p -> p.slug == slug end)
|> Enum.at(0)
end
def custom_links(resource, location \\ nil) do
links = Utils.get_assigned_value_or_default(resource, :custom_links, [])
case location do
nil -> links
:top -> Enum.filter(links, fn l -> Map.get(l, :location, :sub) == :top end)
:sub -> Enum.filter(links, fn l -> Map.get(l, :location, :sub) == :sub end)
end
|> Enum.sort_by(fn l -> Map.get(l, :order, 999) end)
|> Enum.map(fn l -> Map.merge(%{target: "_self", icon: "link", method: :get}, l) end)
end
def collect_links(conn, location) do
contexts = Kaffy.Utils.contexts(conn)
Enum.reduce(contexts, [], fn c, all ->
resources = Kaffy.Utils.schemas_for_context(conn, c)
Enum.reduce(resources, all, fn {_r, options}, all ->
links =
Kaffy.ResourceAdmin.custom_links(options)
|> Enum.filter(fn link -> Map.get(link, :location, :sub) == location end)
all ++ links
end)
end)
|> Enum.sort_by(fn c -> Map.get(c, :order, 999) end)
end
def custom_index_query(conn, resource, query) do
Utils.get_assigned_value_or_default(
resource,
:custom_index_query,
query,
[conn, resource[:schema], query],
false
)
end
def custom_show_query(conn, resource, query) do
Utils.get_assigned_value_or_default(
resource,
:custom_show_query,
query,
[conn, resource[:schema], query],
false
)
end
end