AE
Ash Elixir•3y ago
hannes

Calculations and AshPhoenix.Form

First post! So thanks @Zach Daniel for this awesome library. Looks really promising! I'm working on a side project where I have to store employee contracts. For each contract I store the employment (as factor between 0 and 1) and the number of vacation days per year (when working full time).
defmodule MyApp.Users.Contract do
attributes do
# other attributes
attribute :emp_factor, :decimal, allow_nil?: false, default: Decimal.new(1)
attribute :vac_days, :decimal, allow_nil?: false, default: Decimal.new(25)
end

create :create do
primary? true
argument :user_id, :integer, allow_nil?: false
argument :emp_percent, :decimal, allow_nil?: false, default: Decimal.new(100)
end

changes do
change manage_relationship(:user_id, :user, type: :replace), on: :create
change &change_emp_factor/2
end

calculations do
calculate :vac_days_eff,
:decimal,
expr(vac_days * emp_factor)

calculate :emp_percent,
:decimal,
expr(emp_factor * 100)
end

defp change_emp_factor(changeset, _) do
case Ash.Changeset.get_argument(changeset, :emp_percent) do
nil ->
changeset

emp_percent ->
emp_factor = Decimal.div(emp_percent, 100)
Ash.Changeset.change_attribute(changeset, :emp_factor, emp_factor)
end
end

# relationships etc
end
defmodule MyApp.Users.Contract do
attributes do
# other attributes
attribute :emp_factor, :decimal, allow_nil?: false, default: Decimal.new(1)
attribute :vac_days, :decimal, allow_nil?: false, default: Decimal.new(25)
end

create :create do
primary? true
argument :user_id, :integer, allow_nil?: false
argument :emp_percent, :decimal, allow_nil?: false, default: Decimal.new(100)
end

changes do
change manage_relationship(:user_id, :user, type: :replace), on: :create
change &change_emp_factor/2
end

calculations do
calculate :vac_days_eff,
:decimal,
expr(vac_days * emp_factor)

calculate :emp_percent,
:decimal,
expr(emp_factor * 100)
end

defp change_emp_factor(changeset, _) do
case Ash.Changeset.get_argument(changeset, :emp_percent) do
nil ->
changeset

emp_percent ->
emp_factor = Decimal.div(emp_percent, 100)
Ash.Changeset.change_attribute(changeset, :emp_factor, emp_factor)
end
end

# relationships etc
end
I'm using AshPhoenix.Form.for_action() and LiveView to create a form (see screenshot) where the user can enter the employment as percentage (argument emp_percent) and the number of vacation days (attribute vac_days) and the hint should show the calculated effective vacation days based on the employment factor. I'm already validating the form on change but I would like vac_days_eff to be recalculated each time (so I can show it in the hint below the field). I might also be a bit confused about action attributes/arguments. Could this be solved using a private action argument?
5 Replies
hannes
hannesOP•3y ago
I already got it working with the following helper but I'm wondering if this is the right way:
defp vacation_days_hint(%AshPhoenix.Form{} = form) do
with {:ok, contract} <- Ash.Changeset.apply_attributes(form.source, force?: true),
{:ok, contract} <- MyApp.Users.load(contract, [:emp_percent, :vac_days_eff]),
%Decimal{} <- contract.vac_days_eff,
%Decimal{} <- contract.emp_percent do
"#{format(contract.vac_days_eff, :day)} at #{format(contract.emp_percent, :percent)}"
else
_ -> nil
end
end
defp vacation_days_hint(%AshPhoenix.Form{} = form) do
with {:ok, contract} <- Ash.Changeset.apply_attributes(form.source, force?: true),
{:ok, contract} <- MyApp.Users.load(contract, [:emp_percent, :vac_days_eff]),
%Decimal{} <- contract.vac_days_eff,
%Decimal{} <- contract.emp_percent do
"#{format(contract.vac_days_eff, :day)} at #{format(contract.emp_percent, :percent)}"
else
_ -> nil
end
end
hannes
hannesOP•3y ago
Sorry, forgot to add the screenshot
Screenshot of the form
ZachDaniel
ZachDaniel•3y ago
Ah, yeah so there aren't any helpers for recalculating on submit, but there is a helper for running calculations without needing the underlying record. Here is how you'd use it:
MyApp.Users.calculate(
MyApp.Users.Contract,
:vac_days_off,
refs: [
vac_days: 10, emp_factor: 10
]
)
MyApp.Users.calculate(
MyApp.Users.Contract,
:vac_days_off,
refs: [
vac_days: 10, emp_factor: 10
]
)
So then in your UI you could do something like this:
vac_days = AshPhoenix.Form.get_value(form, :vac_days)
emp_factor = AshPhoenix.Form.get_value(form, :emp_factor)
# probably make sure the above are integers
MyApp.Users.calculate(
MyApp.Users.Contract,
:vac_days_off,
refs: [
vac_days: vac_days, emp_factor: emp_factor
]
)
vac_days = AshPhoenix.Form.get_value(form, :vac_days)
emp_factor = AshPhoenix.Form.get_value(form, :emp_factor)
# probably make sure the above are integers
MyApp.Users.calculate(
MyApp.Users.Contract,
:vac_days_off,
refs: [
vac_days: vac_days, emp_factor: emp_factor
]
)
To clean this up a bit, you can use the code interface
code_interface do
define_for MyApp.Users # This is required, we need to know what API to call functions on
define_calculation :vac_days_off, args: [:vac_days, :emp_factor]
end
code_interface do
define_for MyApp.Users # This is required, we need to know what API to call functions on
define_calculation :vac_days_off, args: [:vac_days, :emp_factor]
end
And then the above looks like this:
vac_days = AshPhoenix.Form.get_value(form, :vac_days)
emp_factor = AshPhoenix.Form.get_value(form, :emp_factor)
# probably make sure the above are integers
MyApp.Users.Contract.vac_days_off(vac_days, emp_factor)
vac_days = AshPhoenix.Form.get_value(form, :vac_days)
emp_factor = AshPhoenix.Form.get_value(form, :emp_factor)
# probably make sure the above are integers
MyApp.Users.Contract.vac_days_off(vac_days, emp_factor)
hannes
hannesOP•3y ago
Thanks for the quick reply! It works! 🙂
ZachDaniel
ZachDaniel•3y ago
🥳 that helper is relatively new, so there may be some rough edges. The main thing to remember: it can only do expressions that can be done without doing a sql query (or other data layer operations) So if you want to use that helper, you can't have things like fragment/1, for example

Did you find this page helpful?