Current section

Files

Jump to
kaffy lib kaffy utils.ex
Raw

lib/kaffy/utils.ex

defmodule Kaffy.Utils do
@moduledoc false
@doc """
Returns the :admin_title config if present, otherwise returns "Kaffy"
"""
@spec title() :: String.t()
def title() do
env(:admin_title, "Kaffy")
end
@doc """
Returns the :admin_footer config if present, otherwise returns default copyright.
"""
@spec footer() :: String.t() | {:safe, String.t()}
def footer() do
env(:admin_footer, "Copyright © 2022 Kaffy. All rights reserved.")
end
@doc """
Returns the static path to the asset.
"""
@spec static_asset_path(Plug.Conn.t(), String.t()) :: String.t()
def static_asset_path(conn, asset_path) do
router().static_path(conn, asset_path)
end
@doc """
Returns the :admin_logo config if present, otherwise returns Kaffy default logo.
"""
@spec logo(Plug.Conn.t()) :: {:safe, String.t()}
def logo(_conn, logo_version \\ :full) do
default_kaffy_logo =
case logo_version do
:full -> "/kaffy/assets/images/logo.png"
:mini -> "/kaffy/assets/images/logo-mini.png"
end
config_key =
case logo_version do
:full -> :admin_logo
:mini -> :admin_logo_mini
end
admin_logo = env(config_key, default_kaffy_logo)
cond do
is_list(admin_logo) ->
url = Keyword.get(admin_logo, :url, default_kaffy_logo)
css_class = Keyword.get(admin_logo, :class)
css_style = Keyword.get(admin_logo, :style)
tag =
~s[<img src="#{url}" alt="logo" class="#{css_class}" style="#{css_style}" />]
{:safe, tag}
true ->
tag = ~s[<img src="#{admin_logo}" alt="logo" />]
{:safe, tag}
end
end
@doc """
Returns the JSON package used by phoenix configs. If no such config exists, raise an exception.
"""
@spec json() :: atom()
def json() do
case Application.get_env(:phoenix, :json_library) do
nil ->
raise "A json package must be configured. For example: config :phoenix, :json_library, Jason"
j ->
j
end
end
@doc """
Returns the Repo from Kaffy configs. If it is not present, raise an exception.
"""
@spec repo() :: atom()
def repo() do
case env(:ecto_repo) do
nil -> raise "Must define :ecto_repo for Kaffy to work properly."
r -> r
end
end
@doc """
Returns the version of the provided app.
Example:
> get_version_of(:phoenix)
> "1.5.3"
"""
@spec get_version_of(atom()) :: String.t()
def get_version_of(package) do
case package do
:elixir ->
System.version()
_ ->
{:ok, version} = :application.get_key(package, :vsn)
version
end
|> to_string()
end
@doc """
Returns true when phoenix's version has the same prefix as the provided argument, false otherwise.
Example:
phoenix_version?("1.4.")
> true # returns true for all phoenix 1.4.x versions
"""
@spec phoenix_version?(String.t()) :: boolean()
def phoenix_version?(prefix) do
version = get_version_of(:phoenix)
String.starts_with?(version, prefix)
end
@doc """
Returns the router helper module from the configs. Raises if the router isn't specified.
"""
@spec router() :: atom()
def router() do
case env(:router) do
nil -> raise "The :router config must be specified: config :kaffy, router: MyAppWeb.Router"
r -> r
end
|> Module.concat(Helpers)
end
@doc """
Returns a keyword list of all the resources specified in config.exs.
If the :resources key isn't specified, this function will load all application modules,
filters the schemas modules, combine them into a keyword list, and returns that list.
Example:
```elixir
full_resources()
[
categories: [
schemas: [
category: [
schema: Bakery.Categories.Category,
admin: Bakery.Categories.CategoryAdmin
]
]
]
]
```
"""
@spec full_resources(Plug.Conn.t()) :: [any()]
def full_resources(conn) do
case env(:resources) do
f when is_function(f) -> f.(conn)
l when is_list(l) -> l
_ -> setup_resources()
end
end
def auto_detect_resources(), do: setup_resources()
@doc """
Returns a list of contexts as atoms.
Example:
iex> contexts()
[:blog, :products, :users]
"""
@spec contexts(Plug.Conn.t()) :: [atom()]
def contexts(conn) do
full_resources(conn)
|> Enum.map(fn {context, _options} -> context end)
end
@doc """
Returns the context name based on the configs.
Example:
```elixir
context = [
categories: [
schemas: [
category: [schema: Bakery.Categories.Category]
]
]
]
context_name(context)
> "Categories"
context = [
categories: [
name: "Types",
schemas: [
category: [schema: Bakery.Categories.Category]
]
]
]
context_name(context)
> "Types"
```
"""
@spec context_name(Plug.Conn.t(), list()) :: String.t()
def context_name(conn, context) do
default = Kaffy.ResourceAdmin.humanize_term(context)
get_in(full_resources(conn), [context, :name]) || default
end
@doc """
Returns the context list from the configs for a specific schema.
This is usually used to get the name or other information of the schema context.
"""
@spec get_context_for_schema(Plug.Conn.t(), module()) :: list()
def get_context_for_schema(conn, schema) do
contexts(conn)
|> Enum.filter(fn c ->
schemas = Enum.map(schemas_for_context(conn, c), fn {_k, v} -> Keyword.get(v, :schema) end)
schema in schemas
end)
|> Enum.at(0)
end
def get_schema_key(conn, context, schema) do
schemas_for_context(conn, context)
|> Enum.reduce([], fn {k, v}, keys ->
case schema == Keyword.get(v, :schema) do
true -> [k | keys]
false -> keys
end
end)
|> Enum.at(0)
end
@doc """
Returns the resource entry from the configs.
Example:
iex> get_resource("blog", "post")
[schema: MyApp.Blog.Post, admin: MyApp.Blog.PostAdmin]
"""
@spec get_resource(Plug.Conn.t(), String.t(), String.t()) :: list()
def get_resource(conn, context, resource) do
preloaded_resources = full_resources(conn)
{context, resource} = convert_to_atoms(context, resource)
get_in(preloaded_resources, [context, :resources, resource])
end
@doc """
Returns all the schemas for the given context.
Example:
iex> schemas_for_context("blog")
[
post: [schema: MyApp.Blog.Post, admin: MyApp.Blog.PostAdmin],
comment: [schema: MyApp.Blog.Comment],
]
"""
@spec schemas_for_context(Plug.Conn.t(), list()) :: list()
def schemas_for_context(conn, context) do
preloaded_resources = full_resources(conn)
context = convert_to_atom(context)
get_in(preloaded_resources, [context, :resources])
end
# @doc """
# Get the schema for the provided context/resource combination.
# iex> schema_for_resource("blog", "post")
# MyApp.Blog.Post
# """
# @spec schema_for_resource(String.t(), String.t()) :: module()
# def schema_for_resource(context, resource) do
# {context, resource} = convert_to_atoms(context, resource)
# get_in(full_resources(), [context, :schemas, resource, :schema])
# end
# @doc """
# Like schema_for_resource/2, but returns the admin module, or nil if an admin module doesn't exist.
# iex> admin-fro_resource("blog", "post")
# MyApp.Blog.PostAdmin
# """
# @spec admin_for_resource(String.t(), String.t()) :: module() | nil
# def admin_for_resource(context, resource) do
# {context, resource} = convert_to_atoms(context, resource)
# get_in(full_resources(), [context, :schemas, resource, :admin])
# end
def get_assigned_value_or_default(resource, function, default, params \\ [], add_schema \\ true) do
admin = resource[:admin]
schema = resource[:schema]
arguments = if add_schema, do: [schema] ++ params, else: params
case !is_nil(admin) && has_function?(admin, function) do
true -> apply(admin, function, arguments)
false -> default
end
end
@doc """
Returns true if the given module implements the given function, false otherwise.
iex> has_function?(MyApp.Blog.PostAdmin, :form_fields)
true
"""
@spec has_function?(module(), atom()) :: boolean()
def has_function?(nil, _), do: false
def has_function?(admin, func) do
functions = admin.__info__(:functions)
Keyword.has_key?(functions, func)
end
def context_admins_include_function?(conn, context, func) do
schemas_for_context(conn, context)
|> Enum.filter(fn {_, options} -> Keyword.has_key?(options, :admin) end)
|> Enum.map(fn {_, options} -> has_function?(Keyword.get(options, :admin), func) end)
|> Enum.any?()
end
@doc """
Returns true if `thing` is an elixir module, false otherwise.
"""
@spec is_module(module()) :: boolean()
def is_module(thing), do: is_atom(thing) && function_exported?(thing, :__info__, 1)
@doc """
Returns whether the dashboard link should be displayed or hidden. Default behavior is to show the dashboard link.
This option is taken from the :hide_dashboard config option.
iex> show_dashboard?()
true
"""
@spec show_dashboard?() :: boolean()
def show_dashboard?() do
env(:hide_dashboard, false) == false
end
@doc """
Takes a conn struct and returns the route to display as the root route.
This option can be optionally provided in the configs. If it is not provided, the default route is the dashboard.
Options are:
- [kaffy: :dashboard]
- [schema: ["blog", "post"]]
- [page: "my-custom-page"]
iex> home_page(conn)
"/admin/dashboard"
"""
@spec home_page(Plug.Conn.t()) :: String.t()
def home_page(conn) do
case env(:home_page, kaffy: :dashboard) do
[kaffy: :dashboard] ->
router().kaffy_dashboard_path(conn, :dashboard)
[schema: [context, resource]] ->
router().kaffy_resource_path(conn, :index, context, resource)
[page: slug] ->
router().kaffy_page_path(conn, :index, slug)
end
end
def extensions(conn) do
env(:extensions, [])
|> Enum.reduce(
%{stylesheets: [], javascripts: []},
fn ext, acc ->
Code.ensure_loaded(ext)
stylesheets =
if function_exported?(ext, :stylesheets, 1) do
ext.stylesheets(conn)
else
[]
end
javascripts =
if function_exported?(ext, :javascripts, 1) do
ext.javascripts(conn)
else
[]
end
%{
stylesheets: stylesheets ++ acc.stylesheets,
javascripts: javascripts ++ acc.javascripts
}
end
)
end
def show_context_dashboards?() do
env(:enable_context_dashboards, true)
end
defp env(key, default \\ nil) do
Application.get_env(:kaffy, key, default)
end
defp convert_to_atoms(context, resource) do
{convert_to_atom(context), convert_to_atom(resource)}
end
defp convert_to_atom(string) do
if is_binary(string), do: String.to_existing_atom(string), else: string
end
defp setup_resources do
otp_app = env(:otp_app)
{:ok, mods} = :application.get_key(otp_app, :modules)
get_schemas(mods)
|> build_resources()
end
defp get_schemas(mods) do
Enum.filter(mods, fn m ->
functions = m.__info__(:functions)
Keyword.has_key?(functions, :__schema__) && Map.has_key?(m.__struct__, :__meta__)
end)
end
defp build_resources(schemas) do
Enum.reduce(schemas, [], fn schema, resources ->
schema_module =
to_string(schema)
|> String.split(".")
context_module =
schema_module
|> Enum.reverse()
|> tl()
|> Enum.reverse()
|> Enum.join(".")
context_name =
schema_module
|> Enum.at(-2)
|> Macro.underscore()
|> String.to_atom()
schema_name_string =
schema_module
|> Enum.at(-1)
schema_name =
schema_name_string
|> Macro.underscore()
|> String.to_atom()
schema_admin = String.to_atom("#{context_module}.#{schema_name_string}Admin")
schema_options =
case function_exported?(schema_admin, :__info__, 1) do
true -> [schema: schema, admin: schema_admin]
false -> [schema: schema]
end
humanized_context = Kaffy.ResourceAdmin.humanize_term(context_name)
resources = Keyword.put_new(resources, context_name, name: humanized_context, resources: [])
resources = put_in(resources, [context_name, :resources, schema_name], schema_options)
existing_schemas = get_in(resources, [context_name, :resources]) |> Enum.sort()
put_in(resources, [context_name, :resources], existing_schemas)
end)
|> Enum.sort()
end
def get_task_modules() do
env(:scheduled_tasks, [])
end
def visible?(options) do
Keyword.get(options, :in_menu, true)
end
def version_match?(app, version) do
get_version_of(app)
|> Version.match?(version)
end
end