",
6 | {TestAutoform.User, exclude: [:age, :biography]},
7 | {TestAutoform.User, exclude: [:name, :address], custom_labels: %{age: "Years on Earth"}}
8 | ], path: "/endpoint") %>
--------------------------------------------------------------------------------
/lib/views/fields_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform.AutoformView do
2 | use Phoenix.View, root: Path.join(__DIR__, "../templates"), pattern: "**/*"
3 | use Phoenix.HTML
4 | import Phoenix.HTML.Form
5 | import Autoform.ErrorHelpers
6 |
7 | def input(form, field, opts) do
8 | type = Phoenix.HTML.Form.input_type(form, field)
9 | apply(Phoenix.HTML.Form, type, [form, field, opts])
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :autoform, TestAutoformWeb.Endpoint,
6 | secret_key_base: "QeeCG58mpWuM3J1UPRVIA47KTVkFyUWS9SrWYHdJzAmtYYPZSqGZ/R1KBIf/80B+",
7 | http: [port: 4001],
8 | server: false
9 |
10 | # Print only warnings and errors during test
11 | config :logger, level: :warn
12 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform/DoWhatYouLove.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.DoWhatYouLove do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "do_what_you_love" do
6 | field(:name, :string)
7 |
8 | timestamps()
9 | end
10 |
11 | @doc false
12 | def changeset(user, attrs) do
13 | user
14 | |> cast(attrs, [:name])
15 | |> validate_required([:name])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform/sample_product.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.SampleProduct do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "sample_products" do
6 | field(:price, :float)
7 |
8 | timestamps()
9 | end
10 |
11 | @doc false
12 | def changeset(product, attrs) do
13 | product
14 | |> cast(attrs, [:price])
15 | |> validate_required([:price])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/support/test_autoform/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :autoform, TestAutoformWeb.Endpoint,
6 | secret_key_base: "QeeCG58mpWuM3J1UPRVIA47KTVkFyUWS9SrWYHdJzAmtYYPZSqGZ/R1KBIf/80B+",
7 | http: [port: 4001],
8 | server: false
9 |
10 | # Print only warnings and errors during test
11 | config :logger, level: :warn
12 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform/address.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.Address do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "addresses" do
6 | field(:line_1, :string)
7 | field(:postcode, :string)
8 |
9 | timestamps()
10 | end
11 |
12 | @doc false
13 | def changeset(user, attrs) do
14 | user
15 | |> cast(attrs, [:line_1, :postcode])
16 | |> validate_required([:line_1, :postcode])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/controllers/do_what_you_love_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.DoWhatYouLoveController do
2 | use TestAutoformWeb, :controller
3 | use Autoform
4 |
5 | alias TestAutoform.DoWhatYouLove
6 |
7 | def new(conn, _params) do
8 | changeset = DoWhatYouLove.changeset(%DoWhatYouLove{}, %{})
9 |
10 | render_autoform(conn, :create, DoWhatYouLove, assigns: [changeset: changeset])
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/controllers/address_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.AddressController do
2 | use TestAutoformWeb, :controller
3 | alias TestAutoform.Address
4 |
5 | def index(conn, _params) do
6 | changeset = Address.changeset(%Address{}, %{})
7 |
8 | render(conn, "index.html", changeset: changeset)
9 | end
10 |
11 | def new(conn, _params) do
12 | changeset = Address.changeset(%Address{}, %{})
13 |
14 | render(conn, "new.html", changeset: changeset)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform/user.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.User do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "users" do
6 | field(:age, :integer)
7 | field(:name, :string)
8 | field(:address, :string)
9 | field(:biography, Fields.DescriptionPlaintextUnlimited)
10 |
11 | timestamps()
12 | end
13 |
14 | @doc false
15 | def changeset(user, attrs) do
16 | user
17 | |> cast(attrs, [:name, :age, :biography])
18 | |> validate_required([:name, :age])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform.Supervisor do
2 | @moduledoc false
3 | use Application
4 |
5 | def start(_type, _args) do
6 | import Supervisor.Spec
7 |
8 | # Run Autoform Test App endpoint when running tests
9 | children =
10 | case Code.ensure_compiled(TestAutoform) do
11 | {:error, _} ->
12 | []
13 |
14 | {:module, TestAutoform} ->
15 | [supervisor(TestAutoformWeb.Endpoint, [])]
16 | end
17 |
18 | opts = [strategy: :one_for_one, name: Autoform.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.ErrorView do
2 | use TestAutoformWeb, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/controllers/user_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.UserController do
2 | use TestAutoformWeb, :controller
3 | use Autoform
4 |
5 | alias TestAutoform.User
6 |
7 | def new(conn, _params) do
8 | changeset = User.changeset(%User{}, %{})
9 |
10 | render_autoform(conn, :create, User, assigns: [changeset: changeset])
11 | end
12 |
13 | def edit(conn, %{"id" => id}) do
14 | user = %User{name: "Test User", age: "55", address: "12 Test Road", id: id}
15 | changeset = User.changeset(user, %{})
16 |
17 | conn
18 | |> render_autoform(:update, User, assigns: [user: user, changeset: changeset], exclude: [:age])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/controllers/custom_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.CustomController do
2 | use TestAutoformWeb, :controller
3 | alias TestAutoform.{Address, SampleProduct}
4 |
5 | def new(conn, _params) do
6 | changeset = Address.changeset(%Address{}, %{})
7 |
8 | render(conn, "new.html", changeset: changeset)
9 | end
10 |
11 | def new_no_path(conn, _params) do
12 | changeset = Address.changeset(%Address{}, %{})
13 |
14 | render(conn, "new_no_path.html", changeset: changeset)
15 | end
16 |
17 | def new_product(conn, _params) do
18 | changeset = SampleProduct.changeset(%SampleProduct{}, %{})
19 |
20 | render(conn, "new_sample_product.html", changeset: changeset)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import CsGuideWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :autoform
24 | end
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | matrix:
3 | include:
4 | - name: tests
5 | elixir:
6 | - 1.7
7 | otp_release:
8 | - 20.2.4
9 | env:
10 | - MIX_ENV=test
11 | - MODE=test
12 | - name: accessibility
13 | elixir:
14 | - 1.7
15 | otp_release:
16 | - 20.2.4
17 | sudo: required
18 | addons:
19 | chrome: stable
20 | env:
21 | - MODE=accessibility
22 | before_script:
23 | - . $HOME/.nvm/nvm.sh
24 | - nvm install stable
25 | - nvm use stable
26 | - npm install -g pa11y-ci
27 | - 'if [ $MODE == "accessibility" ]; then cd test/support/test_autoform && mix deps.get && mix compile; fi'
28 | script:
29 | - 'if [ $MODE == "test" ]; then mix test; fi'
30 | - 'if [ $MODE == "accessibility" ]; then mix phx.server & sleep 5; pa11y-ci; fi'
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import TestAutoformWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :autoform
24 | end
25 |
--------------------------------------------------------------------------------
/lib/templates/custom/field_inputs.html.eex:
--------------------------------------------------------------------------------
1 | <%= for field <- @element.fields do %>
2 |
3 | <%= if Map.has_key?(@element, :input_first) && Enum.any?(@element.input_first, &(&1 == field)) do %>
4 | <%= input @f, field, @element.schema, class: "form-control", required: (field in @element.required), value: Map.get(@changeset.data, field) %>
5 | <%= error_tag @f, field %>
6 | <%= label @f, field, class: "control-label #{if field in @element.required do "required" end}" do %><%=Map.get(@element.custom_labels, field, humanize(field))%><% end %>
7 | <% else %>
8 | <%= label @f, field, class: "control-label #{if field in @element.required do "required" end}" do %><%=Map.get(@element.custom_labels, field, humanize(field))%><% end %>
9 | <%= input @f, field, @element.schema, class: "form-control", required: (field in @element.required), value: Map.get(@changeset.data, field) %>
10 | <%= error_tag @f, field %>
11 | <% end %>
12 |
13 | <% end %>
--------------------------------------------------------------------------------
/test/support/test_autoform/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 | import TestAutoformWeb.Router.Helpers
23 |
24 | # The default endpoint for testing
25 | @endpoint TestAutoformWeb.Endpoint
26 | end
27 | end
28 |
29 | setup tags do
30 | {:ok, conn: Phoenix.ConnTest.build_conn()}
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.Router do
2 | use TestAutoformWeb, :router
3 |
4 | pipeline :browser do
5 | plug(:accepts, ["html"])
6 | plug(:fetch_session)
7 | plug(:fetch_flash)
8 | plug(:protect_from_forgery)
9 | plug(:put_secure_browser_headers)
10 | end
11 |
12 | pipeline :api do
13 | plug(:accepts, ["json"])
14 | end
15 |
16 | scope "/", TestAutoformWeb do
17 | # Use the default browser stack
18 | pipe_through(:browser)
19 |
20 | resources("/users", UserController)
21 | resources("/addresses", AddressController)
22 | resources("/custom", CustomController)
23 | resources("/sample_products", SampleProductController)
24 | get("/custom_no_path", CustomController, :new_no_path)
25 | get("/custom_new_product", CustomController, :new_product)
26 | resources("/do_what_you_love", DoWhatYouLoveController)
27 | end
28 |
29 | # Other scopes may use custom stacks.
30 | # scope "/api", TestAutoformWeb do
31 | # pipe_through :api
32 | # end
33 | end
34 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform/application.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the endpoint when the application starts
12 | supervisor(TestAutoformWeb.Endpoint, [])
13 | # Start your own worker by calling: TestAutoform.Worker.start_link(arg1, arg2, arg3)
14 | # worker(TestAutoform.Worker, [arg1, arg2, arg3]),
15 | ]
16 |
17 | # See https://hexdocs.pm/elixir/Supervisor.html
18 | # for other strategies and supported options
19 | opts = [strategy: :one_for_one, name: TestAutoform.Supervisor]
20 | Supervisor.start_link(children, opts)
21 | end
22 |
23 | # Tell Phoenix to update the endpoint configuration
24 | # whenever the application is updated.
25 | def config_change(changed, _new, removed) do
26 | TestAutoformWeb.Endpoint.config_change(changed, removed)
27 | :ok
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/support/test_autoform/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | # The default endpoint for testing
24 | @endpoint TestAutoformWeb.Endpoint
25 | end
26 | end
27 |
28 |
29 | setup tags do
30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TestAutoform.Repo)
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(TestAutoform.Repo, {:shared, self()})
33 | end
34 | :ok
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Autoform.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :autoform,
7 | version: "0.6.7",
8 | elixir: "~> 1.8",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | start_permanent: Mix.env() == :prod,
11 | compilers: [:phoenix, :gettext] ++ Mix.compilers(),
12 | deps: deps()
13 | ]
14 | end
15 |
16 | # Run "mix help compile.app" to learn about applications.
17 | def application do
18 | [
19 | mod: {Autoform.Supervisor, []},
20 | extra_applications: [:logger]
21 | ]
22 | end
23 |
24 | # Specifies which paths to compile per environment.
25 | defp elixirc_paths(:test), do: ["lib", "test/support", "test"]
26 | defp elixirc_paths(_), do: ["lib"]
27 |
28 | # Run "mix help deps" to learn about dependencies.
29 | defp deps do
30 | [
31 | {:phoenix, "~> 1.3.4"},
32 | {:phoenix_ecto, "~> 3.2"},
33 | {:phoenix_html, "~> 2.10"},
34 | {:phoenix_live_reload, "~> 1.0", only: :dev},
35 | {:cowboy, "~> 1.0"},
36 | {:gettext, "~> 0.11"},
37 | {:fields, git: "https://github.com/dwyl/fields.git", tag: "0.1.7"}
38 | ]
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/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 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :autoform, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:autoform, :key)
18 | #
19 | # You can also configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | import_config "#{Mix.env()}.exs"
31 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello TestAutoform!
11 | ">
12 |
13 |
14 |
15 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", TestAutoformWeb.RoomChannel
6 |
7 | ## Transports
8 | transport :websocket, Phoenix.Transports.WebSocket
9 | # transport :longpoll, Phoenix.Transports.LongPoll
10 |
11 | # Socket params are passed from the client and can
12 | # be used to verify and authenticate a user. After
13 | # verification, you can put default assigns into
14 | # the socket that will be set for all channels, ie
15 | #
16 | # {:ok, assign(socket, :user_id, verified_user_id)}
17 | #
18 | # To deny connection, return `:error`.
19 | #
20 | # See `Phoenix.Token` documentation for examples in
21 | # performing token verification on connect.
22 | def connect(_params, socket) do
23 | {:ok, socket}
24 | end
25 |
26 | # Socket id's are topics that allow you to identify all sockets for a given user:
27 | #
28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
29 | #
30 | # Would allow you to broadcast a "disconnect" event and terminate
31 | # all active sockets and channels for a given user:
32 | #
33 | # TestAutoformWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
34 | #
35 | # Returning `nil` makes this socket anonymous.
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/test/support/test_autoform/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 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :autoform, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:autoform, :key)
18 | #
19 | # You can also configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | config :autoform, TestAutoformWeb.Endpoint,
31 | secret_key_base: "QeeCG58mpWuM3J1UPRVIA47KTVkFyUWS9SrWYHdJzAmtYYPZSqGZ/R1KBIf/80B+"
32 |
33 | import_config "#{Mix.env()}.exs"
34 |
--------------------------------------------------------------------------------
/lib/views/custom_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform.CustomView do
2 | use Phoenix.View, root: Path.join(__DIR__, "../templates"), pattern: "**/*"
3 | use Phoenix.HTML
4 | import Phoenix.HTML.Form
5 | import Autoform.ErrorHelpers
6 |
7 | def input(form, field, schema, opts) do
8 | type =
9 | with "Elixir.Fields." <> field_type <- to_string(schema.__schema__(:type, field)),
10 | {:module, module} <- Code.ensure_loaded(Module.concat(Fields, field_type)),
11 | true <- function_exported?(module, :input_type, 0) do
12 | Module.concat(Fields, field_type).input_type
13 | else
14 | _ ->
15 | Phoenix.HTML.Form.input_type(form, field)
16 | end
17 |
18 | # Apply any sensible defaults here, but we should allow configuration of individual input options
19 | opts =
20 | opts
21 | |> Keyword.merge(
22 | case schema.__schema__(:type, field) do
23 | :float -> [step: 0.01]
24 | _ -> []
25 | end
26 | )
27 | |> Keyword.merge(
28 | cond do
29 | map_size(form.source.changes) > 0 -> [value: form.source.changes[field]]
30 | true -> []
31 | end
32 | )
33 |
34 | apply(Phoenix.HTML.Form, type, [form, field, opts])
35 | end
36 |
37 | def one_of(list_1, list_2) do
38 | Enum.reduce(list_1, nil, fn el, acc ->
39 | if el in list_2, do: el
40 | end)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/autoform_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AutoformViewTest do
2 | use TestAutoformWeb.ConnCase
3 | doctest Autoform
4 |
5 | describe "GET /addresses/new - call render_autoform from within template" do
6 | test "Correct fields exist", %{conn: conn} do
7 | assert response =
8 | conn
9 | |> get(address_path(conn, :new))
10 | |> html_response(200)
11 |
12 | assert response =~ "postcode"
13 | end
14 |
15 | test "Other html exists", %{conn: conn} do
16 | assert response =
17 | conn
18 | |> get(address_path(conn, :new))
19 | |> html_response(200)
20 |
21 | assert response =~ "Addresses"
22 | end
23 |
24 | test "Excluded fields do not display", %{conn: conn} do
25 | assert response =
26 | conn
27 | |> get(address_path(conn, :new))
28 | |> html_response(200)
29 |
30 | refute response =~ "Line 1"
31 | end
32 |
33 | test "Reverse order of :line_1 input box and label", %{conn: conn} do
34 | response = conn |> get(address_path(conn, :index)) |> html_response(200)
35 | [h, t] = Regex.split(~r{id=\"address_line_1\"}, response)
36 |
37 | assert response =~ "id=\"address_line_1\""
38 | refute h =~ "for=\"address_line_1\""
39 | assert t =~ "for=\"address_line_1\""
40 | end
41 |
42 | test "Submit button text says Create", %{conn: conn} do
43 | response = conn |> get(address_path(conn, :new)) |> html_response(200)
44 |
45 | assert response =~ "Create"
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
13 | content_tag(:span, translate_error(error), class: "help-block")
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # When using gettext, we typically pass the strings we want
22 | # to translate as a static argument:
23 | #
24 | # # Translate "is invalid" in the "errors" domain
25 | # dgettext "errors", "is invalid"
26 | #
27 | # # Translate the number of files with plural rules
28 | # dngettext "errors", "1 file", "%{count} files", count
29 | #
30 | # Because the error messages we show in our forms and APIs
31 | # are defined inside Ecto, we need to translate them dynamically.
32 | # This requires us to call the Gettext module passing our gettext
33 | # backend as first argument.
34 | #
35 | # Note we use the "errors" domain, which means translations
36 | # should be written to the errors.po file. The :count option is
37 | # set by Ecto and indicates we should also apply plural rules.
38 | if count = opts[:count] do
39 | Gettext.dngettext(Autoform.Gettext, "errors", msg, msg, count, opts)
40 | else
41 | Gettext.dgettext(Autoform.Gettext, "errors", msg, opts)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/support/test_autoform/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoform.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias TestAutoform.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import TestAutoform.DataCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TestAutoform.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(TestAutoform.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | A helper that transform changeset errors to a map of messages.
40 |
41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
42 | assert "password is too short" in errors_on(changeset).password
43 | assert %{password: ["password is too short"]} = errors_on(changeset)
44 |
45 | """
46 | def errors_on(changeset) do
47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
48 | Enum.reduce(opts, message, fn {key, value}, acc ->
49 | String.replace(acc, "%{#{key}}", to_string(value))
50 | end)
51 | end)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/support/test_autoform/lib/test_autoform_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule TestAutoformWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # When using gettext, we typically pass the strings we want
22 | # to translate as a static argument:
23 | #
24 | # # Translate "is invalid" in the "errors" domain
25 | # dgettext "errors", "is invalid"
26 | #
27 | # # Translate the number of files with plural rules
28 | # dngettext "errors", "1 file", "%{count} files", count
29 | #
30 | # Because the error messages we show in our forms and APIs
31 | # are defined inside Ecto, we need to translate them dynamically.
32 | # This requires us to call the Gettext module passing our gettext
33 | # backend as first argument.
34 | #
35 | # Note we use the "errors" domain, which means translations
36 | # should be written to the errors.po file. The :count option is
37 | # set by Ecto and indicates we should also apply plural rules.
38 | if count = opts[:count] do
39 | Gettext.dngettext(TestAutoformWeb.Gettext, "errors", msg, msg, count, opts)
40 | else
41 | Gettext.dgettext(TestAutoformWeb.Gettext, "errors", msg, opts)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/templates/autoform/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, fn f -> %>
2 | <%= if @changeset.action do %>
3 |
4 |
Oops, something went wrong! Please check the errors below.
5 |
6 | <% end %>
7 | <%= for field <- @fields do %>
8 |
9 | <%= label f, field, class: "control-label #{if field in @required do "required" end}" %>
10 | <%= input f, field, class: "form-control", required: (field in @required) %>
11 | <%= error_tag f, field %>
12 |
13 | <% end %>
14 | <%= for assoc <- @associations do %>
15 |
16 | <%= label f, assoc[:name], class: "control-label #{if assoc[:name] in @required do "required" end}" %>
17 | <%= for a <- assoc[:associations] do %>
18 | Enum.find(fn l -> Map.get(l, :name) == Map.get(a, :display) || Map.get(l, :type) == Map.get(a, :display) end) do "checked" end%>
24 | >
25 |
26 | <% end %>
27 | <%= error_tag f, assoc[:name] %>
28 |
47 | <% end %>
48 |
--------------------------------------------------------------------------------
/test/custom_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CustomTest do
2 | use TestAutoformWeb.ConnCase
3 | doctest Autoform
4 |
5 | describe "Using custom_render_autoform" do
6 | test "Page succesfully renders", %{conn: conn} do
7 | assert response =
8 | conn
9 | |> get(custom_path(conn, :new))
10 | |> html_response(200)
11 |
12 | assert response =~ "Custom Form"
13 | end
14 |
15 | test "Schema fields are correctly rendered", %{conn: conn} do
16 | assert response =
17 | conn
18 | |> get(custom_path(conn, :new))
19 | |> html_response(200)
20 |
21 | assert response =~ "Line 1"
22 | assert response =~ "Postcode"
23 | end
24 |
25 | test "Exclude Schema fields are not rendered", %{conn: conn} do
26 | assert response =
27 | conn
28 | |> get(custom_path(conn, :new))
29 | |> html_response(200)
30 |
31 | assert response =~ "Name"
32 | assert response =~ "Address"
33 | refute response =~ "Age"
34 | end
35 |
36 | test "HTML string is rendered", %{conn: conn} do
37 | assert response =
38 | conn
39 | |> get(custom_path(conn, :new))
40 | |> html_response(200)
41 |
42 | assert response =~ "Between address and user"
43 | end
44 |
45 | test "Elements are ordered correctly", %{conn: conn} do
46 | assert response =
47 | conn
48 | |> get(custom_path(conn, :new))
49 | |> html_response(200)
50 |
51 | Enum.reduce(["Custom Form", "Line 1", "Between", "Name"], -1, fn a, b ->
52 | {index, _} = :binary.match(response, a)
53 | assert b < index
54 | index
55 | end)
56 | end
57 |
58 | test "Form goes to correct endpoint", %{conn: conn} do
59 | assert response =
60 | conn
61 | |> get(custom_path(conn, :new))
62 | |> html_response(200)
63 |
64 | assert response =~ "/endpoint"
65 | end
66 |
67 | test "no path defaults to calling module's name", %{conn: conn} do
68 | assert response =
69 | conn
70 | |> get(custom_path(conn, :new_no_path))
71 | |> html_response(200)
72 |
73 | assert response =~ "/custom"
74 | end
75 |
76 | test "labels are replaced with custom options", %{conn: conn} do
77 | assert response =
78 | conn
79 | |> get(custom_path(conn, :new))
80 | |> html_response(200)
81 |
82 | assert response =~ "Years on Earth"
83 | refute response =~ "Age"
84 | end
85 |
86 | test "Correct input type rendered", %{conn: conn} do
87 | assert response =
88 | conn
89 | |> get(custom_path(conn, :new))
90 | |> html_response(200)
91 |
92 | assert response =~ "textarea"
93 | end
94 | end
95 |
96 | describe "Product" do
97 | test "renders number input with step of 0.01", %{conn: conn} do
98 | assert response =
99 | conn
100 | |> get(custom_path(conn, :new_product))
101 | |> html_response(200)
102 |
103 | assert response =~ "type=\"number\""
104 | assert response =~ "step=\"0.01\""
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "argon2_elixir": {:hex, :argon2_elixir, "1.3.3", "487ffa071ef78c51d9b16e50ff3cf30cf8204e0aa4bdc8afd3765fdd8195e213", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, optional: false]}]},
3 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]},
4 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
5 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], []},
6 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]},
7 | "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], []},
8 | "fields": {:git, "https://github.com/dwyl/fields.git", "a979abbf65aff2e3a2d5e95185ea084e754019c7", [tag: "0.1.4"]},
9 | "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], []},
10 | "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], []},
11 | "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, optional: false]}]},
12 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], []},
13 | "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], []},
14 | "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]},
15 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
16 | "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, optional: false]}]},
17 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.5", "8d4c9b1ef9ca82deee6deb5a038d6d8d7b34b9bb909d99784a49332e0d15b3dc", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, optional: false]}]},
18 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], []},
19 | "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
20 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []},
21 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
22 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []},
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # af
2 | Auto Form ("af") is a way of generating standards/WCAG compliant HTML forms based on an Ecto Schema.
3 |
4 | ## Usage
5 |
6 | Add autoform to your mix.exs
7 |
8 | ``` elixir
9 | defp deps do
10 | [
11 | {:autoform, "~> 0.1.0"}
12 | ]
13 | end
14 | ```
15 |
16 | Autoform can be used from either a Phoenix View, or Phoenix Controller.
17 |
18 | You'll need to add
19 | ``` elixir
20 | use Autoform
21 | ```
22 |
23 | to the top of the module you want to use it in.
24 |
25 | You'll be able to call either `render_autoform/4` or `custom_render_autoform/4` in the controller, or the template that corresponds to the view.
26 |
27 | Call `render_autoform` in your controller with:
28 |
29 | ``` elixir
30 | render_autoform(conn, :update, User, assigns: [user: get_user_from_db()])
31 | ```
32 |
33 | or in your template:
34 | ``` elixir
35 | <%= render_autoform(@conn, :create, User, assigns: [changeset: @changeset)], exclude: :date_of_birth %>
36 | ```
37 |
38 | `custom_render_autoform` is called in a very similar way, but can be given extra arguments which will change the generated form.
39 |
40 | An example of this in a template:
41 | ``` elixir
42 | <%= custom_render_autoform(@conn, :create, [{User, custom_labels: %{date_of_birth: "DOB"}, input_first: [:date_of_birth]}], assigns: [changeset: @changeset)] %>
43 | ```
44 | This will now render the form with a custom label for the `date_of_birth` input field.
45 |
46 | ### Arguments
47 |
48 | ``` elixir
49 | render_autoform(conn, action, schema, options)
50 | ```
51 |
52 | - conn : Your Phoenix Plug.Conn
53 |
54 | - action : Either `:update` or `:create`. This will be used to create the endpoint for your form.
55 |
56 | - schema : Your Ecto schema
57 |
58 | - options : A list of options you can use to customise your forms.
59 | - `:assigns` - The assigns for the form template, for example a changeset for update forms. Will default to empty list
60 | - `:exclude` - A list of any fields in your schema you don't want to display on the form
61 | - `:update_field` - The field from your schema you want to use in your update path (/users/some-id), defaults to `id`
62 | - `:assoc_query` - An ecto query you want to use when loading your associations
63 |
64 | ``` elixir
65 | custom_render_autoform(conn, action, [{schema, opts}], options)
66 | ```
67 |
68 | - conn : Your Phoenix Plug.Conn
69 |
70 | - action : Either `:update` or `:create`. This will be used to create the endpoint for your form.
71 |
72 | - list {schema, opts} : A list of tuples which contain your Ecto schemas and the options for the form.
73 | - opts include:
74 | - `:custom_labels` - A map with a key of the field from your schema and a value of text you would like to label the input with.
75 | - `:input_first` - A list of fields where you would like to display the label followed by the input.
76 |
77 | - options : A list of options you can use to customise your forms.
78 | - `:assigns` - The assigns for the form template, for example a changeset for update forms. Will default to empty list
79 | - `:exclude` - A list of any fields in your schema you don't want to display on the form
80 | - `:update_field` - The field from your schema you want to use in your update path (/users/some-id), defaults to `id`
81 | - `:assoc_query` - An ecto query you want to use when loading your associations
82 |
83 | ### Associations
84 |
85 | `:many_to_many` associations are rendered as checkboxes.
86 |
87 | `:belongs_to` associations are rendered as a `select` element.
88 |
89 | If you don't want the associations to be rendered in your form, you can add them to your exclude list (see options `above`).
90 |
91 | If you are using the `custom_render_autoform` function and one of the schemas you pass in is an association of the `changeset`'s schema, it will be rendered within an `inputs_for` call, so your changes and any errors will be correctly rendered in the form.
92 |
93 | ### Fields
94 |
95 | This module can be used in conjuction with https://github.com/dwyl/fields.
96 |
97 | If you use one of these Fields in your schema, and that field has an `input_type/0` callback. The result of that callback will be the input type that is rendered in your form.
98 |
99 | For example, the field `Fields.DescriptionPlaintextUnlimited` returns `:textarea` from its `input_type` function, so `autoform` will render a textarea using [`Phoenix.HTML.Form.textarea/3`](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#textarea/3)
--------------------------------------------------------------------------------
/test/support/test_autoform/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "argon2_elixir": {:hex, :argon2_elixir, "1.3.3", "487ffa071ef78c51d9b16e50ff3cf30cf8204e0aa4bdc8afd3765fdd8195e213", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
4 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
6 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
7 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
8 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
9 | "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
10 | "fields": {:git, "https://github.com/dwyl/fields.git", "e7bad4c1fb768462e70a9b2ee11824645a2b5ef1", [tag: "0.1.3"]},
11 | "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
12 | "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
13 | "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
14 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
15 | "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
16 | "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
17 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
18 | "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
19 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.6", "7280f4dd88d38ee07ca70a2974ca12b9bfdbb9fa8137e4692889cf097c1bb232", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
20 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"},
21 | "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
22 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
23 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
24 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
25 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
26 | }
27 |
--------------------------------------------------------------------------------
/lib/autoform.ex:
--------------------------------------------------------------------------------
1 | defmodule Autoform do
2 | @excludes [:id, :inserted_at, :updated_at]
3 |
4 | @moduledoc """
5 | Documentation for Autoform.
6 | """
7 |
8 | @doc """
9 | Autoform provides functions that can be used in a Phoenix Controller or View
10 | to generate a form using an Ecto Schema.
11 |
12 | Usage:
13 |
14 | At the top of your controller or view file, call:
15 |
16 | use Autoform
17 |
18 | then, when you want to render your form, call `render_autoform`/4, or `custom_render_autoform/4`
19 | """
20 | defmacro __using__(_opts) do
21 | quote location: :keep do
22 | import Ecto.Query, only: [from: 2]
23 |
24 | @doc """
25 | Renders a 'create' or 'update' form based on the schema passed to it.
26 | Can be used in a Phoenix Controller or View.
27 |
28 | ## Examples
29 |
30 | In a controller:
31 |
32 | render_autoform(conn, :update, User, assigns: [user: get_user_from_db()])
33 |
34 | In a template:
35 |
36 | <%= render_autoform(@conn, :create, User, assigns: [changeset: @changeset)], exclude: [:date_of_birth] %>
37 |
38 | ## Options
39 |
40 | * `:assigns` - The assigns for the form template, for example a changeset for update forms. Will default to empty list
41 | * `:exclude` - A list of any fields in your schema you don't want to display on the form
42 | * `:update_field` - The field from your schema you want to use in your update path (/users/some-id), defaults to id
43 | * `:assoc_query` - An ecto query you want to use when loading your associations
44 | * `:btn_txt` - A string that will be used in the form submission button
45 |
46 | """
47 | @spec render_autoform(
48 | Plug.Conn.t(),
49 | atom(),
50 | Ecto.Schema.t(),
51 | Keyword.t() | map()
52 | ) :: Plug.Conn.t() | {atom(), String.t()} | no_return()
53 | def render_autoform(conn, action, schema, options \\ [])
54 | when action in [:update, :create] do
55 | assigns = create_assigns(conn, action, schema, options)
56 |
57 | cond do
58 | Regex.match?(~r/.+Controller$/, to_string(__MODULE__)) ->
59 | conn
60 | |> Phoenix.Controller.put_view(Autoform.AutoformView)
61 | |> Phoenix.Controller.render("form.html", assigns)
62 |
63 | Regex.match?(~r/.+View$/, to_string(__MODULE__)) ->
64 | Phoenix.View.render(Autoform.AutoformView, "form.html", assigns)
65 |
66 | true ->
67 | raise "This function must be called from a Controller or a View"
68 | end
69 | end
70 |
71 | defp create_assigns(conn, action, schema, options) do
72 | assigns = Keyword.get(options, :assigns, [])
73 |
74 | required = Map.get(schema.changeset(struct(schema), %{}), :required)
75 |
76 | assigns =
77 | assigns
78 | |> submit_btn_txt(options)
79 | |> Enum.into(%{})
80 | |> Map.put_new(:changeset, schema.changeset(struct(schema), %{}))
81 | |> Map.put(:action, path(conn, action, schema, options))
82 | |> Map.put(:fields, fields(schema, options))
83 | |> Map.put(:required, required)
84 | |> (fn a ->
85 | Map.put(a, :associations, associations(conn, schema, a.changeset, options))
86 | end).()
87 | |> Map.put(:schema_name, schema_name(schema))
88 | end
89 |
90 | @doc """
91 | Takes a list of Ecto Schemas, HTML strings or a tuple of Ecto Schemas and options, and renders a form using them.
92 |
93 | ## Examples
94 |
95 | <%= custom_render_autoform(@conn, :create, [
96 | User,
97 | "
Title goes here
",
98 | {Address, exclude: [:line_2], custom_labels: %{line_1: "Street Name"}}
99 | ], assigns: [changeset: @changeset)], exclude: [:entry_id] %>
100 |
101 | <%= custom_render_autoform(@conn, :update, [{Address, input_first: [:line_1, :postcode],}], assigns: [changeset: @changeset)] %>
102 |
103 |
104 | ## Schema Options
105 |
106 | Pass these as the second element in a tuple, where the schema is the first.
107 |
108 | * `:exclude` - A list of any fields from this schema you don't want to display.
109 | * `:custom_labels` - A map where the key is your field name, and the value is what you want to display as the label for that field.
110 | * `:input_first` - A list of any fields from this schema where you would like to display the input followed by the label.
111 |
112 | ## Global Options
113 |
114 | Pass these as the final argument to `custom_render_autoform`
115 |
116 | * `:exclude` - A list of any fields in any of your schemas you don't want to display on the form
117 | * `:update_field` - The field from your schema you want to use in your update path (/users/some-id), defaults to id
118 | * `:assoc_query` - An ecto query you want to use when loading your associations
119 | * `:path` - The endpoint you want your form to post to. Defaults using the calling modules schema name plus :update_field. If you are rendering multiple schemas, you will almost certainly need to use this field
120 | * `:btn_txt` - A string that will be used in the form submission button
121 |
122 | """
123 | @spec custom_render_autoform(
124 | Plug.Conn.t(),
125 | atom(),
126 | list(Ecto.Schema.t() | tuple() | String.t()),
127 | Keyword.t() | map()
128 | ) :: {atom(), String.t()} | no_return()
129 | def custom_render_autoform(conn, action, elements, options \\ []) do
130 | element_assigns =
131 | Enum.map(elements, fn e ->
132 | case is_binary(e) do
133 | true ->
134 | %{html: Phoenix.HTML.raw(e)}
135 |
136 | false ->
137 | {schema, schema_options} =
138 | case e do
139 | {schema, opts} -> {schema, opts}
140 | _ -> {e, []}
141 | end
142 |
143 | excludes = Keyword.get(schema_options, :exclude, [])
144 | custom_labels = Keyword.get(schema_options, :custom_labels, %{})
145 | input_first = Keyword.get(schema_options, :input_first, [])
146 |
147 | %{
148 | fields:
149 | fields(
150 | schema,
151 | Keyword.update(options, :exclude, excludes, fn v ->
152 | v ++ excludes
153 | end)
154 | ),
155 | required: Map.get(schema.changeset(struct(schema), %{}), :required),
156 | associations:
157 | associations(
158 | conn,
159 | schema,
160 | Keyword.get(
161 | Keyword.get(options, :assigns, []),
162 | :changeset,
163 | schema.changeset(struct(schema), %{})
164 | ),
165 | Keyword.update(options, :exclude, [], fn v -> v ++ excludes end)
166 | ),
167 | schema_name: schema_name(schema),
168 | custom_labels: custom_labels,
169 | input_first: input_first,
170 | schema: schema
171 | }
172 | end
173 | end)
174 |
175 | first_schema =
176 | Enum.find(elements, fn e -> not is_binary(e) end)
177 | |> case do
178 | {schema, _} -> schema
179 | schema -> schema
180 | end
181 |
182 | Phoenix.View.render(
183 | Autoform.CustomView,
184 | "custom.html",
185 | Keyword.get(options, :assigns, [])
186 | |> submit_btn_txt(options)
187 | |> Map.new()
188 | |> Map.put_new(:changeset, first_schema.changeset(struct(first_schema), %{}))
189 | |> (fn m ->
190 | Map.merge(m, %{
191 | elements: element_assigns,
192 | action: Keyword.get(options, :path, path(conn, action, first_schema, options)),
193 | assoc_list: Map.get(m, :changeset).data.__struct__.__schema__(:associations)
194 | })
195 | end).()
196 | )
197 | end
198 |
199 | defp path(conn, action, schema, options) do
200 | assigns = Keyword.get(options, :assigns, [])
201 |
202 | path_data =
203 | case action do
204 | :create ->
205 | []
206 |
207 | :update ->
208 | {key, schema_data} =
209 | Enum.find(assigns, fn {k, v} ->
210 | k != :changeset && match?(v, schema)
211 | end)
212 |
213 | case Keyword.get(options, :update_field) do
214 | nil -> schema_data
215 | field -> Map.get(schema_data, field)
216 | end
217 | end
218 |
219 | regex = ~r/(?.+)\.(?.+)(?Controller|View)/
220 |
221 | %{"web_name" => web_name, "schema_name" => schema_name} =
222 | Regex.named_captures(regex, to_string(__MODULE__))
223 |
224 | apply(
225 | String.to_existing_atom(web_name <> ".Router.Helpers"),
226 | String.to_existing_atom(Macro.underscore(schema_name) <> "_path"),
227 | [conn, action, path_data]
228 | )
229 | end
230 |
231 | defp fields(schema, options) do
232 | excludes = Keyword.get(options, :exclude, []) ++ unquote(@excludes)
233 |
234 | schema.__schema__(:fields)
235 | |> Enum.reject(&(&1 in excludes))
236 | end
237 |
238 | defp associations(conn, schema, changeset, options) do
239 | excludes = Keyword.get(options, :exclude, []) ++ unquote(@excludes)
240 |
241 | assoc_query =
242 | Keyword.get(options, :assoc_query, fn schema -> from(s in schema, select: s) end)
243 |
244 | repo =
245 | Module.concat(
246 | Macro.camelize(to_string(Phoenix.Controller.endpoint_module(conn).config(:otp_app))),
247 | "Repo"
248 | )
249 |
250 | associations =
251 | schema.__schema__(:associations)
252 | |> Enum.reject(&(&1 in excludes))
253 | |> Enum.map(fn a ->
254 | assoc = Map.get(schema.__schema__(:association, a), :queryable)
255 |
256 | query =
257 | case is_function(assoc_query) do
258 | true -> assoc_query.(assoc)
259 | false -> nil
260 | end
261 |
262 | %{
263 | cardinality: Map.get(schema.__schema__(:association, a), :cardinality),
264 | name: a,
265 | associations:
266 | Enum.map(
267 | apply(repo, :all, [query]),
268 | fn a ->
269 | Map.put(a, :display, Map.get(a, :name))
270 | end
271 | ),
272 | loaded_associations:
273 | apply(repo, :preload, [Map.get(changeset, :data, %{}), [{a, query}]])
274 | }
275 | end)
276 |
277 | associations
278 | end
279 |
280 | defp schema_name(schema) do
281 | schema |> to_string() |> String.split(".") |> List.last() |> Macro.underscore()
282 | end
283 |
284 | defp submit_btn_txt(assigns, options) do
285 | btn_txt_str = Keyword.get(options, :btn_txt, "Save")
286 | Keyword.put_new(assigns, :btn_txt, btn_txt_str)
287 | end
288 | end
289 | end
290 | end
291 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------