mirror of
https://github.com/ash-project/ash_phoenix.git
synced 2024-09-20 07:12:49 +12:00
improvement: expose paths for filters
improvement: simple error handling patterns for filter forms
This commit is contained in:
parent
a61a7763cc
commit
0687568299
4 changed files with 244 additions and 77 deletions
|
@ -2,6 +2,7 @@ defmodule AshPhoenix.FilterForm do
|
||||||
defstruct [
|
defstruct [
|
||||||
:id,
|
:id,
|
||||||
:resource,
|
:resource,
|
||||||
|
:transform_errors,
|
||||||
valid?: false,
|
valid?: false,
|
||||||
negated?: false,
|
negated?: false,
|
||||||
params: %{},
|
params: %{},
|
||||||
|
@ -19,6 +20,16 @@ defmodule AshPhoenix.FilterForm do
|
||||||
doc: "Initial parameters to create the form with",
|
doc: "Initial parameters to create the form with",
|
||||||
default: %{}
|
default: %{}
|
||||||
],
|
],
|
||||||
|
transform_errors: [
|
||||||
|
type: :any,
|
||||||
|
doc: """
|
||||||
|
Allows for manual manipulation and transformation of errors.
|
||||||
|
|
||||||
|
If possible, try to implement `AshPhoenix.FormData.Error` for the error (if it as a custom one, for example).
|
||||||
|
If that isn't possible, you can provide this function which will get the predicate and the error, and should
|
||||||
|
return a list of ash phoenix formatted errors, e.g `[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]`
|
||||||
|
"""
|
||||||
|
],
|
||||||
remove_empty_groups?: [
|
remove_empty_groups?: [
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
doc: """
|
doc: """
|
||||||
|
@ -55,17 +66,21 @@ defmodule AshPhoenix.FilterForm do
|
||||||
|> params_to_list()
|
|> params_to_list()
|
||||||
|> add_ids()
|
|> add_ids()
|
||||||
|
|
||||||
%__MODULE__{
|
form = %__MODULE__{
|
||||||
id: params["id"] || params[:id],
|
id: params["id"] || params[:id],
|
||||||
resource: resource,
|
resource: resource,
|
||||||
params: params,
|
params: params,
|
||||||
remove_empty_groups?: opts[:remove_empty_groups?],
|
remove_empty_groups?: opts[:remove_empty_groups?],
|
||||||
components:
|
|
||||||
parse_components(resource, params["components"] || params[:components],
|
|
||||||
remove_empty_groups?: opts[:remove_empty_groups?]
|
|
||||||
),
|
|
||||||
operator: to_existing_atom(params["operator"] || params[:operator] || :and)
|
operator: to_existing_atom(params["operator"] || params[:operator] || :and)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%{
|
||||||
|
form
|
||||||
|
| components:
|
||||||
|
parse_components(resource, form, params["components"] || params[:components],
|
||||||
|
remove_empty_groups?: opts[:remove_empty_groups?]
|
||||||
|
)
|
||||||
|
}
|
||||||
|> set_validity()
|
|> set_validity()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,11 +157,17 @@ defmodule AshPhoenix.FilterForm do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a flat list of all errors on all predicates in the filter.
|
Returns a flat list of all errors on all predicates in the filter.
|
||||||
"""
|
"""
|
||||||
def errors(%__MODULE__{components: components}) do
|
def errors(form, opts \\ [])
|
||||||
Enum.flat_map(components, &errors/1)
|
|
||||||
|
def errors(%__MODULE__{components: components, transform_errors: transform_errors}, opts) do
|
||||||
|
Enum.flat_map(
|
||||||
|
components,
|
||||||
|
&errors(&1, Keyword.put_new(opts, :handle_errors, transform_errors))
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def errors(%Predicate{errors: errors}), do: errors
|
def errors(%Predicate{} = predicate, opts),
|
||||||
|
do: AshPhoenix.FilterForm.Predicate.errors(predicate, opts[:transform_errors])
|
||||||
|
|
||||||
defp do_to_filter(%__MODULE__{components: []}, _), do: {:ok, true}
|
defp do_to_filter(%__MODULE__{components: []}, _), do: {:ok, true}
|
||||||
|
|
||||||
|
@ -186,11 +207,16 @@ defmodule AshPhoenix.FilterForm do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_to_filter(
|
defp do_to_filter(
|
||||||
%Predicate{field: field, value: value, operator: operator, negated?: negated?} =
|
%Predicate{
|
||||||
predicate,
|
field: field,
|
||||||
|
value: value,
|
||||||
|
operator: operator,
|
||||||
|
negated?: negated?,
|
||||||
|
path: path
|
||||||
|
} = predicate,
|
||||||
resource
|
resource
|
||||||
) do
|
) do
|
||||||
ref = Ash.Query.expr(ref(^field, []))
|
ref = Ash.Query.expr(ref(^field, ^path))
|
||||||
|
|
||||||
expr =
|
expr =
|
||||||
if Ash.Filter.get_function(operator, resource) do
|
if Ash.Filter.get_function(operator, resource) do
|
||||||
|
@ -202,18 +228,10 @@ defmodule AshPhoenix.FilterForm do
|
||||||
{:ok, operator}
|
{:ok, operator}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error,
|
{:error, error}
|
||||||
Ash.Error.Query.InvalidQuery.exception(
|
|
||||||
field: field,
|
|
||||||
message: "Error constructing operator: #{error}"
|
|
||||||
)}
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:error,
|
{:error, {:operator, "No such function or operator #{operator}"}, []}
|
||||||
Ash.Error.Query.InvalidQuery.exception(
|
|
||||||
field: field,
|
|
||||||
message: "No such function or operator #{operator}"
|
|
||||||
)}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -308,31 +326,54 @@ defmodule AshPhoenix.FilterForm do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_components(resource, component_params, form_opts) do
|
defp parse_components(resource, parent, component_params, form_opts) do
|
||||||
component_params
|
component_params
|
||||||
|> Kernel.||([])
|
|> Kernel.||([])
|
||||||
|> Enum.map(&parse_component(resource, &1, form_opts))
|
|> Enum.map(&parse_component(resource, parent, &1, form_opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_component(resource, params, form_opts) do
|
defp parse_component(resource, parent, params, form_opts) do
|
||||||
if is_operator?(params) do
|
if is_operator?(params) do
|
||||||
# Eventually, components may have references w/ paths
|
# Eventually, components may have references w/ paths
|
||||||
# also, we should validate references here
|
# also, we should validate references here
|
||||||
new_predicate(params)
|
new_predicate(params, parent)
|
||||||
else
|
else
|
||||||
new(resource, Keyword.put(form_opts, :params, params))
|
new(resource, Keyword.put(form_opts, :params, params))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp new_predicate(params) do
|
defp new_predicate(params, form) do
|
||||||
%AshPhoenix.FilterForm.Predicate{
|
predicate = %AshPhoenix.FilterForm.Predicate{
|
||||||
id: params[:id] || params["id"] || Ash.UUID.generate(),
|
id: params[:id] || params["id"] || Ash.UUID.generate(),
|
||||||
field: to_existing_atom(params["field"] || params[:field]),
|
field: to_existing_atom(params["field"] || params[:field]),
|
||||||
value: params["value"] || params[:value],
|
value: params["value"] || params[:value],
|
||||||
|
path: parse_path(params),
|
||||||
params: params,
|
params: params,
|
||||||
negated?: negated?(params),
|
negated?: negated?(params),
|
||||||
operator: to_existing_atom(params["operator"] || params[:operator] || :eq)
|
operator: to_existing_atom(params["operator"] || params[:operator] || :eq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%{predicate | errors: predicate_errors(predicate, form.resource)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_path(params) do
|
||||||
|
path = params[:path] || params["path"]
|
||||||
|
|
||||||
|
case path do
|
||||||
|
"" ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
path when is_list(path) ->
|
||||||
|
Enum.map(path, &to_existing_atom/1)
|
||||||
|
|
||||||
|
path ->
|
||||||
|
path
|
||||||
|
|> String.split()
|
||||||
|
|> Enum.map(&to_existing_atom/1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp negated?(params) do
|
defp negated?(params) do
|
||||||
|
@ -365,7 +406,7 @@ defmodule AshPhoenix.FilterForm do
|
||||||
)
|
)
|
||||||
|
|
||||||
%Predicate{} ->
|
%Predicate{} ->
|
||||||
new_predicate(params)
|
new_predicate(params, form)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
component
|
component
|
||||||
|
@ -375,7 +416,7 @@ defmodule AshPhoenix.FilterForm do
|
||||||
else
|
else
|
||||||
component =
|
component =
|
||||||
if is_operator?(params) do
|
if is_operator?(params) do
|
||||||
new_predicate(params)
|
new_predicate(params, form)
|
||||||
else
|
else
|
||||||
new(form.resource, params: params, remove_empty_groups?: form.remove_empty_groups?)
|
new(form.resource, params: params, remove_empty_groups?: form.remove_empty_groups?)
|
||||||
end
|
end
|
||||||
|
@ -389,7 +430,12 @@ defmodule AshPhoenix.FilterForm do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_existing_atom(value) when is_atom(value), do: value
|
defp to_existing_atom(value) when is_atom(value), do: value
|
||||||
defp to_existing_atom(value), do: String.to_existing_atom(value)
|
|
||||||
|
defp to_existing_atom(value) do
|
||||||
|
String.to_existing_atom(value)
|
||||||
|
rescue
|
||||||
|
_ -> value
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Returns the list of available predicates for the given resource, which may be functions or operators."
|
@doc "Returns the list of available predicates for the given resource, which may be functions or operators."
|
||||||
def predicates(resource) do
|
def predicates(resource) do
|
||||||
|
@ -410,12 +456,12 @@ defmodule AshPhoenix.FilterForm do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Returns the list of available fields, which may be attribuets, calculations, or aggregates."
|
@doc "Returns the list of available fields, which may be attributes, calculations, or aggregates."
|
||||||
def fields(form) do
|
def fields(resource) do
|
||||||
form.resource
|
resource
|
||||||
|> Ash.Resource.Info.public_aggregates()
|
|> Ash.Resource.Info.public_aggregates()
|
||||||
|> Enum.concat(Ash.Resource.Info.public_calculations(form.resource))
|
|> Enum.concat(Ash.Resource.Info.public_calculations(resource))
|
||||||
|> Enum.concat(Ash.Resource.Info.public_attributes(form.resource))
|
|> Enum.concat(Ash.Resource.Info.public_attributes(resource))
|
||||||
|> Enum.map(& &1.name)
|
|> Enum.map(& &1.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -439,14 +485,16 @@ defmodule AshPhoenix.FilterForm do
|
||||||
|
|
||||||
predicate_id = Ash.UUID.generate()
|
predicate_id = Ash.UUID.generate()
|
||||||
|
|
||||||
predicate = %Predicate{
|
predicate =
|
||||||
id: predicate_id,
|
new_predicate(
|
||||||
field: field,
|
%{
|
||||||
value: value,
|
id: predicate_id,
|
||||||
operator: operator_or_function
|
field: field,
|
||||||
}
|
value: value,
|
||||||
|
operator: operator_or_function
|
||||||
predicate = %{predicate | errors: predicate_errors(predicate, form.resource)}
|
},
|
||||||
|
form
|
||||||
|
)
|
||||||
|
|
||||||
if opts[:to] && opts[:to] != form.id do
|
if opts[:to] && opts[:to] != form.id do
|
||||||
{set_validity(%{
|
{set_validity(%{
|
||||||
|
@ -493,23 +541,35 @@ defmodule AshPhoenix.FilterForm do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp predicate_errors(predicate, resource) do
|
defp predicate_errors(predicate, resource) do
|
||||||
errors =
|
case Ash.Resource.Info.related(resource, predicate.path) do
|
||||||
case Ash.Resource.Info.public_field(resource, predicate.field) do
|
nil ->
|
||||||
nil ->
|
[
|
||||||
[Ash.Error.Query.NoSuchAttribute.exception(resource: resource, name: predicate.field)]
|
{:operator, "Invalid path #{Enum.join(predicate.path, ".")}", []}
|
||||||
|
]
|
||||||
|
|
||||||
_ ->
|
resource ->
|
||||||
[]
|
errors =
|
||||||
end
|
case Ash.Resource.Info.public_field(resource, predicate.field) do
|
||||||
|
nil ->
|
||||||
|
[
|
||||||
|
{:field, "No such field #{predicate.field}", []}
|
||||||
|
]
|
||||||
|
|
||||||
if Ash.Filter.get_function(predicate.operator, resource) do
|
_ ->
|
||||||
errors
|
[]
|
||||||
else
|
end
|
||||||
if Ash.Filter.get_operator(predicate.operator) do
|
|
||||||
errors
|
if Ash.Filter.get_function(predicate.operator, resource) do
|
||||||
else
|
errors
|
||||||
[Ash.Error.Query.NoSuchOperator.exception(name: predicate.operator) | errors]
|
else
|
||||||
end
|
if Ash.Filter.get_operator(predicate.operator) do
|
||||||
|
errors
|
||||||
|
else
|
||||||
|
[
|
||||||
|
{:operator, "No such operator #{predicate.operator}", []} | errors
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -626,7 +686,10 @@ defmodule AshPhoenix.FilterForm do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def to_form(form, _, :components, _opts) do
|
def to_form(form, _, :components, _opts) do
|
||||||
Enum.map(form.components, &Phoenix.HTML.Form.form_for(&1, "action"))
|
Enum.map(
|
||||||
|
form.components,
|
||||||
|
&Phoenix.HTML.Form.form_for(&1, "action", transform_errors: form.transform_errors)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_form(_, _, other, _) do
|
def to_form(_, _, other, _) do
|
||||||
|
|
|
@ -10,21 +10,65 @@ defmodule AshPhoenix.FilterForm.Predicate do
|
||||||
operator: :eq,
|
operator: :eq,
|
||||||
params: %{},
|
params: %{},
|
||||||
negated?: false,
|
negated?: false,
|
||||||
|
path: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
valid?: false
|
valid?: false
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def errors(predicate, transform_errors) do
|
||||||
|
predicate.errors
|
||||||
|
|> Enum.filter(fn
|
||||||
|
error when is_exception(error) ->
|
||||||
|
AshPhoenix.FormData.Error.impl_for(error)
|
||||||
|
|
||||||
|
{_key, _value, _vars} ->
|
||||||
|
true
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end)
|
||||||
|
|> Enum.flat_map(
|
||||||
|
&AshPhoenix.FormData.Helpers.transform_predicate_error(
|
||||||
|
predicate,
|
||||||
|
&1,
|
||||||
|
transform_errors
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Enum.map(fn
|
||||||
|
{field, message, vars} ->
|
||||||
|
vars =
|
||||||
|
vars
|
||||||
|
|> List.wrap()
|
||||||
|
|> Enum.flat_map(fn {key, value} ->
|
||||||
|
try do
|
||||||
|
if is_integer(value) do
|
||||||
|
[{key, value}]
|
||||||
|
else
|
||||||
|
[{key, to_string(value)}]
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
{field, {message || "", vars}}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defimpl Phoenix.HTML.FormData do
|
defimpl Phoenix.HTML.FormData do
|
||||||
@impl true
|
@impl true
|
||||||
def to_form(predicate, opts) do
|
def to_form(predicate, opts) do
|
||||||
hidden = [id: predicate.id]
|
hidden = [id: predicate.id]
|
||||||
|
|
||||||
|
errors = AshPhoenix.FilterForm.Predicate.errors(predicate, opts[:transform_errors])
|
||||||
|
|
||||||
%Phoenix.HTML.Form{
|
%Phoenix.HTML.Form{
|
||||||
source: predicate,
|
source: predicate,
|
||||||
impl: __MODULE__,
|
impl: __MODULE__,
|
||||||
id: predicate.id,
|
id: predicate.id,
|
||||||
name: predicate.id,
|
name: predicate.id,
|
||||||
errors: [],
|
errors: errors,
|
||||||
data: predicate,
|
data: predicate,
|
||||||
params: predicate.params,
|
params: predicate.params,
|
||||||
hidden: hidden,
|
hidden: hidden,
|
||||||
|
@ -43,9 +87,10 @@ defmodule AshPhoenix.FilterForm.Predicate do
|
||||||
def input_value(%{value: value}, _, :value), do: value
|
def input_value(%{value: value}, _, :value), do: value
|
||||||
def input_value(%{operator: operator}, _, :operator), do: operator
|
def input_value(%{operator: operator}, _, :operator), do: operator
|
||||||
def input_value(%{negated?: negated?}, _, :negated), do: negated?
|
def input_value(%{negated?: negated?}, _, :negated), do: negated?
|
||||||
|
def input_value(%{path: path}, _, :path), do: Enum.join(path, ".")
|
||||||
|
|
||||||
def input_value(_, _, field) do
|
def input_value(_, _, field) do
|
||||||
raise "Invalid filter form field #{field}. Only :negated, :operator, :field and :value are supported"
|
raise "Invalid filter form field #{field}. Only :negated, :operator, :field, :path, :value are supported"
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -150,7 +150,7 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
nil ->
|
nil ->
|
||||||
case error do
|
case error do
|
||||||
{_key, _value, _vars} = error ->
|
{_key, _value, _vars} = error ->
|
||||||
error
|
[error]
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
|
@ -162,22 +162,48 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# defp set_source_context(changeset, {relationship, original_changeset}) do
|
def transform_predicate_error(predicate, error, transform_errors) do
|
||||||
# case original_changeset.context[:manage_relationship_source] do
|
case transform_errors do
|
||||||
# nil ->
|
transformer when is_function(transformer, 2) ->
|
||||||
# Ash.Changeset.set_context(changeset, %{
|
case transformer.(predicate, error) do
|
||||||
# manage_relationship_source: [
|
error when is_exception(error) ->
|
||||||
# {relationship.source, relationship.name, original_changeset}
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
# ]
|
List.wrap(to_form_error(error))
|
||||||
# })
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
# value ->
|
{key, value, vars} ->
|
||||||
# Ash.Changeset.set_context(changeset, %{
|
[{key, value, vars}]
|
||||||
# manage_relationship_source:
|
|
||||||
# value ++ [{relationship.source, relationship.name, original_changeset}]
|
list when is_list(list) ->
|
||||||
# })
|
Enum.flat_map(list, fn
|
||||||
# end
|
error when is_exception(error) ->
|
||||||
# end
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
|
List.wrap(to_form_error(error))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
{key, value, vars} ->
|
||||||
|
[{key, value, vars}]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
case error do
|
||||||
|
{_key, _value, _vars} = error ->
|
||||||
|
[error]
|
||||||
|
|
||||||
|
error ->
|
||||||
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
|
List.wrap(to_form_error(error))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp to_form_error(exception) when is_exception(exception) do
|
defp to_form_error(exception) when is_exception(exception) do
|
||||||
case AshPhoenix.FormData.Error.to_form_error(exception) do
|
case AshPhoenix.FormData.Error.to_form_error(exception) do
|
||||||
|
|
|
@ -110,6 +110,23 @@ defmodule AshPhoenix.FilterFormTest do
|
||||||
contains(title, "new")
|
contains(title, "new")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "predicates can reference paths" do
|
||||||
|
form =
|
||||||
|
FilterForm.new(Post,
|
||||||
|
params: %{
|
||||||
|
field: :text,
|
||||||
|
operator: :contains,
|
||||||
|
path: "comments",
|
||||||
|
value: "new"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Ash.Query.equivalent_to?(
|
||||||
|
FilterForm.filter!(Post, form),
|
||||||
|
contains(comments.text, "new")
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "form_data implementation" do
|
describe "form_data implementation" do
|
||||||
|
@ -175,5 +192,21 @@ defmodule AshPhoenix.FilterFormTest do
|
||||||
assert input_value(predicate_form, :operator) == :eq
|
assert input_value(predicate_form, :operator) == :eq
|
||||||
assert input_value(predicate_form, :negated) == false
|
assert input_value(predicate_form, :negated) == false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "using an unknown operator shows an error" do
|
||||||
|
assert [predicate_form] =
|
||||||
|
Post
|
||||||
|
|> FilterForm.new(
|
||||||
|
params: %{
|
||||||
|
field: :title,
|
||||||
|
operator: "what_on_earth",
|
||||||
|
value: "new post"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> form_for("action")
|
||||||
|
|> inputs_for(:components)
|
||||||
|
|
||||||
|
assert [{:operator, {"No such operator what_on_earth", []}}] = predicate_form.errors
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue