├── .formatter.exs
├── .gitignore
├── .tool-versions
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin
├── bootstrap
├── cibuild
├── console
├── setup
├── teardown
├── test
└── update
├── config
└── config.exs
├── lib
├── adminable.ex
├── adminable
│ ├── admin_controller.ex
│ ├── error_helpers.ex
│ ├── exporter.ex
│ ├── plug.ex
│ ├── router.ex
│ ├── templates
│ │ ├── admin
│ │ │ ├── _pagination.html.eex
│ │ │ ├── dashboard.html.eex
│ │ │ ├── edit.html.eex
│ │ │ ├── index.html.eex
│ │ │ └── new.html.eex
│ │ └── layout
│ │ │ └── app.html.eex
│ └── views
│ │ ├── admin_view.ex
│ │ ├── field.ex
│ │ ├── layout_view.ex
│ │ └── view_helpers.ex
└── mix
│ └── tasks
│ └── adminable.gen.view.ex
├── mix.exs
├── mix.lock
└── test
├── adminable_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | adminable-*.tar
24 |
25 | /.elixir_ls
26 |
27 | dev.secret.exs
28 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | erlang 24.0.5
2 | elixir 1.12.2-otp-24
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: elixir
3 | elixir:
4 | - 1.12
5 | otp_release:
6 | - 24.0
7 | cache:
8 | directories:
9 | - _build
10 | - deps
11 | install:
12 | - mix local.hex --force
13 | - mix local.rebar --force
14 | - mix deps.get
15 | script:
16 | - mix test --cover
17 | before_deploy:
18 | - mix compile
19 | deploy:
20 | skip_cleanup: true
21 | provider: script
22 | script: mix hex.publish --yes
23 | on:
24 | tags: true
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.3.1] - 2020-02-04
9 |
10 | ### Updated
11 |
12 | - Put plug options in private instead of assigns
13 |
14 | ## [0.3.0] - 2020-02-04
15 |
16 | ### Added
17 |
18 | - `view_module` option to `Adminable.Plug` to allow users to use their own views and templates
19 | - `mix adminable.gen.view` to export Adminable's view and templates for modification
20 |
21 | ## [0.2.0] - 2019-12-23
22 |
23 | ### Updated
24 |
25 | - Loosened Harmonium dependency
26 |
27 | ## [0.1.0] - 2019-04-26
28 |
29 | ### Added
30 |
31 | - Adminable behaviour and `__using__` macro
32 | - Basic ability to list, create, and edit schemas
33 |
34 | ## [0.1.1] - 2019-05-23
35 |
36 | ### Fixed
37 |
38 | - [Pagination not working](https://github.com/revelrylabs/adminable/pull/1)
39 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at support@revelry.co. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing and Development
2 |
3 | ## Development Setup
4 |
5 | - Clone repository
6 | - `mix deps.get` to get dependencies
7 | - `mix compile` to compile project
8 | - `mix test` to run tests
9 |
10 | ## Submitting Changes
11 |
12 | 1. Fork the project
13 | 2. Create a new topic branch to contain your feature, change, or fix.
14 | 3. Make sure all the tests are still passing.
15 | 4. Implement your feature, change, or fix. Make sure to write tests, update and/or add documentation.
16 | 5. Push your topic branch up to your fork.
17 | 6. Open a Pull Request with a clear title and description.
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Revelry Labs LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation
5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
6 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions
9 | of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
12 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
13 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
14 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
15 | DEALINGS IN THE SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Adminable
2 |
3 | Create admin interfaces for Ecto Schemas in Phoenix apps
4 |
5 | Based on blog post [here](https://lytedev.io/blog/ecto-reflection-for-simple-admin-crud-forms/)
6 |
7 | ## Installation
8 |
9 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed
10 | by adding `adminable` to your list of dependencies in `mix.exs`:
11 |
12 | ```elixir
13 | def deps do
14 | [
15 | {:adminable, "~> 0.3.1"}
16 | ]
17 | end
18 | ```
19 |
20 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
21 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
22 | be found at [https://hexdocs.pm/adminable](https://hexdocs.pm/adminable).
23 |
24 | ## Configuration
25 |
26 | - Add `use Adminable` to your Ecto Schema
27 |
28 | ```elixir
29 | defmodule MyApp.User do
30 | use Ecto.Schema
31 | import Ecto.{Query, Changeset}, warn: false
32 | use Adminable
33 |
34 | ...
35 | end
36 | ```
37 |
38 | - optionally implement fields/0, create_changeset/2 and edit_changeset/2
39 |
40 | - Forward to `Adminable.Router`
41 |
42 | ```elixir
43 | scope "/admin" do
44 | pipe_through [:browser, :my, :other, :pipelines]
45 |
46 | forward("/", Adminable.Plug, [
47 | otp_app: :my_app,
48 | repo: MyApp.Repo,
49 | schemas: [MyApp.User],
50 | view_module: MyAppWeb.Adminable.AdminView,
51 | layout: {MyAppWeb.LayoutView, "app.html"}
52 | ])
53 | end
54 | ```
55 |
56 | Arguments
57 |
58 | - `otp_app` - Your app
59 | - `repo` - Your app's Repo
60 | - `schemas` - The schemas to make Admin sections for
61 | - `view_module` - (Optional) The view_module to use to display pages. Uses Adminable's view module by default. You can export the view to modify using `mix adminable.gen.view MyWebModule`
62 | - `layout` - (Optional) The layout to use
63 |
64 | ## Exporting View and Templates
65 |
66 | To export Adminable's AdminView and templates for modification, run:
67 |
68 | ```bash
69 | mix adminable.gen.view MyWebModule
70 | ```
71 |
--------------------------------------------------------------------------------
/bin/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if which asdf >/dev/null; then
4 | echo "Running asdf install"
5 | asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
6 | asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
7 | asdf install
8 | else
9 | echo "asdf not found. Please make sure it is installed by following directions below and try again"
10 | echo "https://github.com/asdf-vm/asdf"
11 | exit 1
12 | fi
13 |
14 | echo "Installing mix dependencies"
15 | mix local.hex --force
16 | mix local.rebar --force
17 | mix deps.get
18 |
--------------------------------------------------------------------------------
/bin/cibuild:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sh ./bin/test
4 |
5 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | iex -S mix
4 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sh ./bin/bootstrap
4 |
5 | if [ ! -f config/dev.secret.exs ]; then
6 | echo "dev.secret.exs not found, creating"
7 | echo "use Mix.Config" >> config/dev.secret.exs
8 | echo "Make sure to add required secrets to this file"
9 | fi
10 |
11 | echo "===================================="
12 | echo "Setup complete."
13 | echo "===================================="
14 |
--------------------------------------------------------------------------------
/bin/teardown:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm _build
4 | rm deps
5 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mix test "$@"
4 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sh ./bin/bootstrap
4 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | config :phoenix, :json_library, Jason
6 |
7 | # This configuration is loaded before any dependency and is restricted
8 | # to this project. If another project depends on this project, this
9 | # file won't be loaded nor affect the parent project. For this reason,
10 | # if you want to provide default values for your application for
11 | # third-party users, it should be done in your "mix.exs" file.
12 |
13 | # You can configure your application as:
14 | #
15 | # config :adminable, key: :value
16 | #
17 | # and access this configuration in your application as:
18 | #
19 | # Application.get_env(:adminable, :key)
20 | #
21 | # You can also configure a third-party app:
22 | #
23 | # config :logger, level: :info
24 | #
25 |
26 | # It is also possible to import configuration files, relative to this
27 | # directory. For example, you can emulate configuration per environment
28 | # by uncommenting the line below and defining dev.exs, test.exs and such.
29 | # Configuration from the imported file will override the ones defined
30 | # here (which is why it is important to import them last).
31 | #
32 | # import_config "#{Mix.env()}.exs"
33 |
--------------------------------------------------------------------------------
/lib/adminable.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable do
2 | @moduledoc """
3 | Behaviour to capture how to build admin interfaces and which fields to allow to edit
4 |
5 | ## Configuration
6 |
7 | - Add `use Adminable` to your Ecto Schema. Optionally
8 |
9 | ```elixir
10 | defmodule MyApp.User do
11 | use Ecto.Schema
12 | import Ecto.{Query, Changeset}, warn: false
13 | use Adminable
14 |
15 | ...
16 | end
17 | ```
18 |
19 | - optionally implement `fields/0`, `create_changeset/2` and `edit_changeset/2`
20 |
21 | - Forward to `Adminable.Router`
22 |
23 | ```elixir
24 | scope "/admin" do
25 | pipe_through [:browser, :my, :other, :pipelines]
26 |
27 | forward("/", Adminable.Plug, [
28 | otp_app: :my_app,
29 | repo: MyApp.Repo,
30 | schemas: [MyApp.User],
31 | view_module: MyAppWeb.Adminable.AdminView
32 | layout: {MyAppWeb.LayoutView, "app.html"}
33 | ])
34 | end
35 | ```
36 |
37 | Arguments
38 |
39 | * `otp_app` - Your app
40 | * `repo` - Your app's Repo
41 | * `schemas` - The schemas to make Admin sections for
42 | * `view_module` - (Optional) The view_module to use to display pages. Uses Adminable's view module by default. You can export the view to modify using `mix adminable.gen.view MyWebModule`
43 | * `layout` - (Optional) The layout to use
44 | """
45 |
46 | @doc """
47 | A list of fields for to show and edit in Adminable. The primary key will be excluded from
48 | create and edit forms
49 | """
50 | @callback fields() :: [atom()]
51 |
52 | @doc """
53 | Returns a changeset used for creating new schemas
54 | """
55 | @callback create_changeset(any(), any()) :: Ecto.Changeset.t()
56 |
57 | @doc """
58 | Returns a changeset used for editing existing schemas
59 | """
60 | @callback edit_changeset(any(), any()) :: Ecto.Changeset.t()
61 |
62 | defmacro __using__(_) do
63 | quote do
64 | @behaviour Adminable
65 |
66 | def fields() do
67 | __MODULE__.__schema__(:fields)
68 | end
69 |
70 | def create_changeset(schema, data) do
71 | __MODULE__.changeset(schema, data)
72 | end
73 |
74 | def edit_changeset(schema, data) do
75 | __MODULE__.changeset(schema, data)
76 | end
77 |
78 | defoverridable fields: 0, create_changeset: 2, edit_changeset: 2
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/adminable/admin_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.AdminController do
2 | @moduledoc false
3 |
4 | use Phoenix.Controller, namespace: Adminable
5 | import Plug.Conn
6 |
7 | def dashboard(conn, _params) do
8 | schemas = Map.keys(conn.private.adminable_schemas)
9 |
10 | opts = [
11 | schemas: schemas
12 | ]
13 |
14 | conn
15 | |> put_layout(conn.private.adminable_layout)
16 | |> put_view(conn.private.adminable_view_module)
17 | |> render("dashboard.html", opts)
18 | end
19 |
20 | def index(conn, %{"schema" => schema} = params) do
21 | schema_module = conn.private.adminable_schemas[schema]
22 |
23 | paginate_config = [
24 | page_size: 20,
25 | page: Map.get(params, "page", 1),
26 | module: conn.private.adminable_repo
27 | ]
28 |
29 | page = Scrivener.paginate(schema_module, paginate_config)
30 |
31 | opts = [
32 | schema_module: schema_module,
33 | schema: schema,
34 | schemas: page.entries,
35 | page_number: page.page_number,
36 | page_size: page.page_size,
37 | total_pages: page.total_pages,
38 | total_entries: page.total_entries
39 | ]
40 |
41 | conn
42 | |> put_layout(conn.private.adminable_layout)
43 | |> put_view(conn.private.adminable_view_module)
44 | |> render("index.html", opts)
45 | end
46 |
47 | def new(conn, %{"schema" => schema}) do
48 | schema_module = conn.private.adminable_schemas[schema]
49 |
50 | model = struct(schema_module)
51 |
52 | opts = [
53 | changeset: schema_module.create_changeset(model, %{}),
54 | schema_module: schema_module,
55 | schema: schema
56 | ]
57 |
58 | conn
59 | |> put_layout(conn.private.adminable_layout)
60 | |> put_view(conn.private.adminable_view_module)
61 | |> render("new.html", opts)
62 | end
63 |
64 | def create(conn, %{"schema" => schema, "data" => data}) do
65 | schema_module = conn.private.adminable_schemas[schema]
66 |
67 | new_schema = struct(schema_module)
68 |
69 | changeset = schema_module.create_changeset(new_schema, data)
70 |
71 | case conn.private.adminable_repo.insert(changeset) do
72 | {:ok, _created} ->
73 | conn
74 | |> put_flash(:info, "#{String.capitalize(schema)} created!")
75 | |> redirect(to: Adminable.Router.Helpers.admin_path(conn, :index, schema))
76 |
77 | {:error, changeset} ->
78 | opts = [
79 | changeset: changeset,
80 | schema_module: schema_module,
81 | schema: schema
82 | ]
83 |
84 | conn
85 | |> put_flash(:error, "#{String.capitalize(schema)} failed to create!")
86 | |> put_status(:unprocessable_entity)
87 | |> put_layout(conn.private.adminable_layout)
88 | |> put_view(conn.private.adminable_view_module)
89 | |> render("new.html", opts)
90 | end
91 | end
92 |
93 | def edit(conn, %{"schema" => schema, "pk" => pk}) do
94 | schema_module = conn.private.adminable_schemas[schema]
95 |
96 | model =
97 | schema_module.__schema__(:associations)
98 | |> Enum.reduce(conn.private.adminable_repo.get(schema_module, pk), fn a, m ->
99 | conn.private.adminable_repo.preload(m, a)
100 | end)
101 |
102 | opts = [
103 | changeset: schema_module.edit_changeset(model, %{}),
104 | schema_module: schema_module,
105 | schema: schema,
106 | pk: pk
107 | ]
108 |
109 | conn
110 | |> put_layout(conn.private.adminable_layout)
111 | |> put_view(conn.private.adminable_view_module)
112 | |> render("edit.html", opts)
113 | end
114 |
115 | def update(conn, %{"schema" => schema, "pk" => pk, "data" => data}) do
116 | schema_module = conn.private.adminable_schemas[schema]
117 |
118 | item = conn.private.adminable_repo.get!(schema_module, pk)
119 |
120 | changeset = schema_module.edit_changeset(item, data)
121 |
122 | case conn.private.adminable_repo.update(changeset) do
123 | {:ok, _updated_model} ->
124 | conn
125 | |> put_flash(:info, "#{String.capitalize(schema)} ID #{pk} updated!")
126 | |> redirect(to: Adminable.Router.Helpers.admin_path(conn, :index, schema))
127 |
128 | {:error, changeset} ->
129 | opts = [
130 | changeset: changeset,
131 | schema_module: schema_module,
132 | schema: schema,
133 | pk: pk
134 | ]
135 |
136 | conn
137 | |> put_flash(:error, "#{String.capitalize(schema)} ID #{pk} failed to update!")
138 | |> put_status(:unprocessable_entity)
139 | |> put_layout(conn.private.adminable_layout)
140 | |> put_view(conn.private.adminable_view_module)
141 | |> render("edit.html", opts)
142 | end
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/lib/adminable/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.ErrorHelpers do
2 | @moduledoc false
3 |
4 | use Phoenix.HTML
5 |
6 | @doc """
7 | Generates tag for inlined form input errors.
8 | """
9 | def error_tag(form, field) do
10 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
11 | content_tag(:span, translate_error(error), class: "help-block")
12 | end)
13 | end
14 |
15 | @doc """
16 | Translates an error message using gettext.
17 | """
18 | def translate_error({msg, opts}) do
19 | # Because error messages were defined within Ecto, we must
20 | # call the Gettext module passing our Gettext backend. We
21 | # also use the "errors" domain as translations are placed
22 | # in the errors.po file.
23 | # Ecto will pass the :count keyword if the error message is
24 | # meant to be pluralized.
25 | # On your own code and templates, depending on whether you
26 | # need the message to be pluralized or not, this could be
27 | # written simply as:
28 | #
29 | # dngettext "errors", "1 file", "%{count} files", count
30 | # dgettext "errors", "is invalid"
31 | #
32 | if count = opts[:count] do
33 | Gettext.dngettext(Adminable.Gettext, "errors", msg, msg, count, opts)
34 | else
35 | Gettext.dgettext(Adminable.Gettext, "errors", msg, opts)
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/adminable/exporter.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.Exporter do
2 | @moduledoc """
3 | Exports templates and a view module.
4 | This allows for apps that use Adminable to modify templates
5 | to their liking.
6 | """
7 |
8 | template_paths = "lib/adminable/templates/admin/**/*.html.eex" |> Path.wildcard() |> Enum.sort()
9 |
10 | templates =
11 | for template_path <- template_paths do
12 | @external_resource Path.relative_to_cwd(template_path)
13 | {Path.basename(template_path), File.read!(template_path)}
14 | end
15 |
16 | @templates templates
17 |
18 | def list_templates do
19 | @templates
20 | end
21 |
22 | def view_module(web_module) do
23 | """
24 | defmodule #{inspect web_module}.Adminable.AdminView do
25 | use #{inspect web_module}, :view
26 | use Adminable.ViewHelpers
27 | end
28 | """
29 | end
30 |
31 | def templates_path(web_module) do
32 | Path.join(web_module_path(web_module), "templates")
33 | end
34 |
35 | def views_path(web_module) do
36 | Path.join(web_module_path(web_module), "views")
37 | end
38 |
39 | defp web_module_path(web_module) do
40 | name = Phoenix.Naming.underscore(web_module)
41 | Path.join(["lib", name])
42 | end
43 |
44 | def export(web_module) do
45 | templates_path = templates_path(web_module)
46 | views_path = views_path(web_module)
47 |
48 | view_module = view_module(web_module)
49 | templates = list_templates()
50 |
51 | exported_view_module_path = Path.join([views_path, "adminable", "admin_view.ex"])
52 | File.mkdir_p!(Path.dirname(exported_view_module_path))
53 | File.write!(exported_view_module_path, view_module)
54 |
55 | exported_templates_path = Path.join([templates_path, "adminable", "admin"])
56 | File.mkdir_p!(exported_templates_path)
57 | Enum.each(templates, fn({name, data}) ->
58 | File.write!(Path.join(exported_templates_path, name), data)
59 | end)
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/adminable/plug.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.Plug do
2 | @moduledoc """
3 | Plug for admin routes. Add this to your phoenix router
4 |
5 | ```elixir
6 | scope "/admin" do
7 | pipe_through [:browser, :my, :other, :pipelines]
8 |
9 | forward("/", Adminable.Plug, [
10 | otp_app: :my_app,
11 | repo: MyApp.Repo,
12 | schemas: [MyApp.User],
13 | view_module: MyAppWeb.Adminable.AdminView
14 | layout: {MyAppWeb.LayoutView, "app.html"}
15 | ])
16 | end
17 | ```
18 |
19 | Arguments
20 |
21 | * `otp_app` - Your app
22 | * `repo` - Your app's Repo
23 | * `schemas` - The schemas to make Admin sections for
24 | * `view_module` - (Optional) The view_module to use to display pages. Uses Adminable's view module by default. You can export the view to modify using `mix adminable.gen.view MyWebModule`
25 | * `layout` - (Optional) The layout to use
26 |
27 | """
28 |
29 | def init(opts) do
30 | opts
31 | end
32 |
33 | def call(conn, opts) do
34 | repo = Keyword.fetch!(opts, :repo)
35 | otp_app = Keyword.fetch!(opts, :otp_app)
36 | schemas = Keyword.get(opts, :schemas, [])
37 | view_module = Keyword.get(opts, :view_module, Adminable.AdminView)
38 | layout = Keyword.get(opts, :layout, {Adminable.LayoutView, "app.html"})
39 |
40 | schemas =
41 | Enum.map(schemas, fn schema ->
42 | {
43 | schema.__schema__(:source),
44 | schema
45 | }
46 | end)
47 | |> Enum.into(%{})
48 |
49 | conn
50 | |> Plug.Conn.put_private(:adminable_otp_app, otp_app)
51 | |> Plug.Conn.put_private(:adminable_repo, repo)
52 | |> Plug.Conn.put_private(:adminable_schemas, schemas)
53 | |> Plug.Conn.put_private(:adminable_layout, layout)
54 | |> Plug.Conn.put_private(:adminable_view_module, view_module)
55 | |> Adminable.Router.call(opts)
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/adminable/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.Router do
2 | @moduledoc false
3 |
4 | use Phoenix.Router
5 |
6 | scope "/", Adminable do
7 | get("/", AdminController, :dashboard)
8 | get("/:schema/", AdminController, :index)
9 | get("/:schema/new/", AdminController, :new)
10 | post("/:schema/new/", AdminController, :create)
11 | get("/:schema/edit/:pk", AdminController, :edit)
12 | put("/:schema/edit/:pk", AdminController, :update)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/adminable/templates/admin/_pagination.html.eex:
--------------------------------------------------------------------------------
1 | <%= if Adminable.AdminView.show_pagination?(@total_pages) do %>
2 |
55 | <% end %>
56 |
--------------------------------------------------------------------------------
/lib/adminable/templates/admin/dashboard.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Admin Dashboard
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%= for schema <- @schemas do %>
18 |
19 |
20 | <%= schema %>
21 | |
22 |
23 | <% end %>
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/lib/adminable/templates/admin/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Edit <%= String.capitalize(@schema) %>
6 |
7 |
8 | <%= form_for @changeset, Adminable.Router.Helpers.admin_path(@conn, :update, @schema, @pk), [as: :data], fn f -> %>
9 |
10 | <%= for field <- form_fields(@changeset) do %>
11 | <%= field(f, @schema_module, field) %>
12 | <% end %>
13 |
14 |
15 |
22 |
23 | <% end %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/adminable/templates/admin/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
<%= String.capitalize(@schema) %>
4 |
5 |
6 |
7 |
8 |
9 | <%= link "New #{String.capitalize(@schema)}", to: Adminable.Router.Helpers.admin_path(@conn, :new, @schema), class: "rev-Button" %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <%= for field <- index_fields(@schema_module) do %>
20 |
21 | <% end %>
22 | Edit |
23 |
24 |
25 |
26 | <%= for schema <- @schemas do %>
27 |
28 | <%= for field <- index_fields(@schema_module) do %>
29 | <%= Map.get(schema, field) %> |
30 | <% end %>
31 |
32 | <%= link "Edit", to: Adminable.Router.Helpers.admin_path(@conn, :edit, @schema, schema.id), class: "rev-Button rev-Button--secondary" %>
33 | |
34 |
35 | <% end %>
36 |
37 |
38 |
39 |
40 | <%= render "_pagination.html",
41 | conn: @conn,
42 | page_number: @page_number,
43 | page_size: @page_size,
44 | total_entries: @total_entries,
45 | total_pages: @total_pages,
46 | url: Adminable.Router.Helpers.admin_path(@conn, :index, @schema)
47 | %>
48 |
49 |
50 |
--------------------------------------------------------------------------------
/lib/adminable/templates/admin/new.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
New <%= String.capitalize(@schema) %>
6 |
7 |
8 | <%= form_for @changeset, Adminable.Router.Helpers.admin_path(@conn, :create, @schema), [as: :data], fn f -> %>
9 |
10 | <%= for field <- form_fields(@changeset) do %>
11 | <%= field(f, @schema_module, field) %>
12 | <% end %>
13 |
14 |
15 |
22 |
23 | <% end %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/adminable/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Adminable
11 |
12 |
13 |
14 |
15 |
16 | <%= render @view_module, @view_template, assigns %>
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/adminable/views/admin_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.AdminView do
2 | @moduledoc false
3 |
4 | use Phoenix.View,
5 | root: "lib/adminable/templates",
6 | namespace: Adminable
7 |
8 | use Adminable.ViewHelpers
9 | end
10 |
--------------------------------------------------------------------------------
/lib/adminable/views/field.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.Field do
2 | @moduledoc false
3 |
4 | import Harmonium
5 | use Phoenix.HTML
6 |
7 | def field(form, schema_module, field, opts \\ []) do
8 | field_type = schema_module.__schema__(:type, field)
9 | field_html(form, field, field_type, opts)
10 | end
11 |
12 | defp field_html(form, field, :boolean, opts) do
13 | ~E"""
14 | <%= col do %>
15 | <%= single_checkbox(form, field, Keyword.merge([label: Phoenix.Naming.humanize(field)], opts)) %>
16 | <% end %>
17 | """
18 | end
19 |
20 | defp field_html(form, field, number, opts) when number in [:integer, :float] do
21 | ~E"""
22 | <%= col do %>
23 | <%= number_input_stack(
24 | form,
25 | field,
26 | label: Phoenix.Naming.humanize(field),
27 | input: Keyword.merge([], opts))
28 | %>
29 | <% end %>
30 | """
31 | end
32 |
33 | defp field_html(form, field, _field_type, opts) do
34 | ~E"""
35 | <%= col do %>
36 | <%= text_input_stack(
37 | form,
38 | field,
39 | label: Phoenix.Naming.humanize(field),
40 | input: Keyword.merge([placeholder: Phoenix.Naming.humanize(field)], opts))
41 | %>
42 | <% end %>
43 | """
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/adminable/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.LayoutView do
2 | @moduledoc false
3 |
4 | use Phoenix.View,
5 | root: "lib/adminable/templates",
6 | namespace: Adminable
7 |
8 | use Phoenix.HTML
9 | end
10 |
--------------------------------------------------------------------------------
/lib/adminable/views/view_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Adminable.ViewHelpers do
2 | @moduledoc false
3 |
4 | defmacro __using__(_opts) do
5 | quote do
6 | use Phoenix.HTML
7 | @page_links_to_show 2
8 |
9 | def index_fields(schema_module) do
10 | schema_module.fields()
11 | end
12 |
13 | def form_fields(changeset) do
14 | schema = changeset.data
15 |
16 | fields = schema.__struct__.fields() -- [:inserted_at, :updated_at]
17 | fields -- schema.__struct__.__schema__(:primary_key)
18 | end
19 |
20 | defdelegate field(form, schema_module, field, opts \\ []), to: Adminable.Field
21 |
22 | def make_next_page_link(conn, current_page, url) do
23 | make_page_link(conn, current_page + 1, url)
24 | end
25 |
26 | def make_previous_page_link(conn, current_page, url) do
27 | make_page_link(conn, current_page - 1, url)
28 | end
29 |
30 | def make_page_link(conn, page, url) do
31 | query_params = conn.query_params
32 |
33 | query_params = Map.put(query_params, "page", page)
34 |
35 | uri = URI.parse(url)
36 |
37 | query_params =
38 | if is_nil(uri.query) do
39 | query_params
40 | else
41 | URI.decode_query(uri.query, query_params)
42 | end
43 |
44 | uri = %{uri | query: Plug.Conn.Query.encode(query_params)}
45 |
46 | URI.to_string(uri)
47 | end
48 |
49 | def first_page?(1), do: true
50 | def first_page?(_), do: false
51 |
52 | def last_page?(page_number, total_pages) when page_number == total_pages, do: true
53 | def last_page?(_, _), do: false
54 |
55 | def show_pagination?(1), do: false
56 | def show_pagination?(_), do: true
57 |
58 | def selected_page_class(page_number, current_page) when page_number == current_page do
59 | "rev-Pagination-number--selected"
60 | end
61 |
62 | def selected_page_class(_, _) do
63 | ""
64 | end
65 |
66 | def show_previous_ellipsis?(1, _) do
67 | false
68 | end
69 |
70 | def show_previous_ellipsis?(page, page_number) do
71 | page == page_number - @page_links_to_show
72 | end
73 |
74 | def show_next_ellipsis?(page, page_number, total_pages) do
75 | page == page_number + @page_links_to_show and page != total_pages
76 | end
77 |
78 | def show_page_link?(page, page_number) do
79 | page >= page_number - @page_links_to_show and page <= page_number + @page_links_to_show
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/mix/tasks/adminable.gen.view.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Adminable.Gen.View do
2 | @shortdoc "Generates views and templates"
3 |
4 | @moduledoc """
5 | Generates views and templates so that they can be modifed.
6 | mix adminable.gen.view my_web_module
7 |
8 | ## Arguments
9 | * `my_web_module` - web_module to use for path and module names
10 | """
11 | use Mix.Task
12 |
13 | @impl true
14 | def run([web_module_string]) do
15 | web_module = Module.concat([web_module_string])
16 | Adminable.Exporter.export(web_module)
17 | end
18 |
19 | def run(_) do
20 | Mix.Shell.IO.error("Invalid args. Usage mix adminable.gen.view my_web_module")
21 | exit({:shutdown, 1})
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Adminable.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :adminable,
7 | version: "0.5.0",
8 | elixir: "~> 1.8",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | description: description(),
12 | elixirc_paths: elixirc_paths(Mix.env()),
13 | compilers: [:phoenix, :gettext] ++ Mix.compilers(),
14 | package: package(),
15 | test_coverage: [tool: ExCoveralls],
16 | preferred_cli_env: [
17 | coveralls: :test,
18 | "coveralls.detail": :test,
19 | "coveralls.post": :test,
20 | "coveralls.html": :test
21 | ],
22 |
23 | # Docs
24 | name: "Adminable",
25 | source_url: "https://github.com/revelrylabs/adminable",
26 | homepage_url: "https://github.com/revelrylabs/adminable",
27 | # The main page in the docs
28 | docs: [main: "Adminable", extras: ["README.md"]]
29 | ]
30 | end
31 |
32 | # Run "mix help compile.app" to learn about applications.
33 | def application do
34 | [
35 | extra_applications: [:logger]
36 | ]
37 | end
38 |
39 | defp elixirc_paths(:test), do: ["lib", "test/support"]
40 | defp elixirc_paths(_), do: ["lib"]
41 |
42 | # Run "mix help deps" to learn about dependencies.
43 | defp deps do
44 | [
45 | {:phoenix, "~> 1.6"},
46 | {:phoenix_html, "~> 3.1"},
47 | {:gettext, "~> 0.11"},
48 | {:ecto, "~> 3.0"},
49 | {:scrivener_ecto, "~> 2.0"},
50 | {:harmonium, "~> 2.3"},
51 | {:jason, "~> 1.0"},
52 | {:ex_doc, "~> 0.20", only: :dev},
53 | {:excoveralls, "~> 0.10", only: :test}
54 | ]
55 | end
56 |
57 | defp description do
58 | """
59 | Create admin interfaces for Ecto schemas in Phoenix apps
60 | """
61 | end
62 |
63 | defp package do
64 | [
65 | files: ["lib", "mix.exs", "README.md", "LICENSE", "CHANGELOG.md"],
66 | maintainers: ["Revelry Labs"],
67 | licenses: ["MIT"],
68 | links: %{
69 | "GitHub" => "https://github.com/revelrylabs/adminable"
70 | },
71 | build_tools: ["mix"]
72 | ]
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
3 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
4 | "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"},
5 | "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
6 | "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"},
7 | "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
8 | "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
9 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
10 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
11 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
12 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
13 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
14 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
16 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
17 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
18 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"},
19 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
20 | "phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"},
21 | "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
22 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.16.4", "5692edd0bac247a9a816eee7394e32e7a764959c7d0cf9190662fc8b0cd24c97", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "754ba49aa2e8601afd4f151492c93eb72df69b0b9856bab17711b8397e43bba0"},
23 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
24 | "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
25 | "plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"},
26 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
27 | "scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
28 | "scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
29 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
30 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
31 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
32 | }
33 |
--------------------------------------------------------------------------------
/test/adminable_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AdminableTest do
2 | use ExUnit.Case
3 | doctest Adminable
4 | end
5 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------