diff --git a/Dockerfile b/Dockerfile index 0ad975874..ca85207bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \ autoconf \ libssl-dev \ libncurses-dev \ + inotify-tools \ && rm -rf /var/lib/apt/lists/* RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen diff --git a/core/priv/gettext/de/LC_MESSAGES/link-advert.po b/core/priv/gettext/de/LC_MESSAGES/link-advert.po index 4292b0829..5952677cf 100644 --- a/core/priv/gettext/de/LC_MESSAGES/link-advert.po +++ b/core/priv/gettext/de/LC_MESSAGES/link-advert.po @@ -94,3 +94,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "Hier findest du die aktuellsten Studien, die im panl-Pool veröffentlicht wurden. Klicke auf eine Studie, um weitere Details zu lesen." + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "Mindestgeburtsjahr" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "Höchstgeburtsjahr" diff --git a/core/priv/gettext/en/LC_MESSAGES/link-advert.po b/core/priv/gettext/en/LC_MESSAGES/link-advert.po index b4a42cf74..9e6ed1d50 100644 --- a/core/priv/gettext/en/LC_MESSAGES/link-advert.po +++ b/core/priv/gettext/en/LC_MESSAGES/link-advert.po @@ -93,3 +93,11 @@ msgstr "Go to Settings" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "Here you can find the most recent studies published in the panl pool. Click on a study to read more details." + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "Minimum birth year" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "Maximum birth year" diff --git a/core/priv/gettext/es/LC_MESSAGES/link-advert.po b/core/priv/gettext/es/LC_MESSAGES/link-advert.po index 6d2677b0c..f2909f31d 100644 --- a/core/priv/gettext/es/LC_MESSAGES/link-advert.po +++ b/core/priv/gettext/es/LC_MESSAGES/link-advert.po @@ -94,3 +94,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "Aquí encontrarás los estudios más recientes publicados en el pool de panl. Haz clic en un estudio para leer más detalles." + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "Año mínimo de nacimiento" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "Año máximo de nacimiento" diff --git a/core/priv/gettext/it/LC_MESSAGES/link-advert.po b/core/priv/gettext/it/LC_MESSAGES/link-advert.po index 1a384ccde..d12e937ab 100644 --- a/core/priv/gettext/it/LC_MESSAGES/link-advert.po +++ b/core/priv/gettext/it/LC_MESSAGES/link-advert.po @@ -94,3 +94,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "Qui di seguito trovi i più recenti studi pubblicati nel pool di panl. Clicca su uno studio per leggere ulteriori dettagli." + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "Anno di nascita minimo" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "Anno di nascita massimo" diff --git a/core/priv/gettext/link-advert.pot b/core/priv/gettext/link-advert.pot index 9128e172e..92beff3f2 100644 --- a/core/priv/gettext/link-advert.pot +++ b/core/priv/gettext/link-advert.pot @@ -93,3 +93,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/link-advert.po b/core/priv/gettext/nl/LC_MESSAGES/link-advert.po index eccffcb7c..c26d7c875 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/link-advert.po +++ b/core/priv/gettext/nl/LC_MESSAGES/link-advert.po @@ -93,3 +93,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submission.available.sub_heading" msgstr "Hieronder vind je de meest recente studies die in panl pool gepubliceerd zijn. Klik op een studie om meer details te lezen." + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.min_label" +msgstr "Minimaal geboortejaar" + +#, elixir-autogen, elixir-format +msgid "submission.criteria.birth_years.max_label" +msgstr "Maximaal geboortejaar" diff --git a/core/priv/repo/migrations/20250918140818_add_birth_year_fields_to_eligibility_criteria.exs b/core/priv/repo/migrations/20250918140818_add_birth_year_fields_to_eligibility_criteria.exs new file mode 100644 index 000000000..176ae76ff --- /dev/null +++ b/core/priv/repo/migrations/20250918140818_add_birth_year_fields_to_eligibility_criteria.exs @@ -0,0 +1,17 @@ +defmodule Core.Repo.Migrations.AddBirthYearFieldsToEligibilityCriteria do + use Ecto.Migration + + def up do + alter table(:eligibility_criteria) do + add(:min_birth_year, :integer) + add(:max_birth_year, :integer) + end + end + + def down do + alter table(:eligibility_criteria) do + remove(:min_birth_year) + remove(:max_birth_year) + end + end +end diff --git a/core/systems/advert/_presenter.ex b/core/systems/advert/_presenter.ex index 2f6f2ee0e..6a2a36714 100644 --- a/core/systems/advert/_presenter.ex +++ b/core/systems/advert/_presenter.ex @@ -26,4 +26,5 @@ defmodule Systems.Advert.Presenter do defp builder(Advert.ContentPage), do: Advert.ContentPageBuilder defp builder(Promotion.LandingPage), do: Advert.PromotionLandingPageBuilder + defp builder(Advert.SubmissionView), do: Advert.SubmissionViewBuilder end diff --git a/core/systems/advert/_switch.ex b/core/systems/advert/_switch.ex index 9b29c0774..3b7da8fdb 100644 --- a/core/systems/advert/_switch.ex +++ b/core/systems/advert/_switch.ex @@ -27,6 +27,15 @@ defmodule Systems.Advert.Switch do :ok end + @impl true + def intercept({:submission, _} = signal, %{submission: submission} = message) do + if advert = Advert.Public.get_by_submission(submission, Advert.Model.preload_graph(:down)) do + dispatch!({:advert, signal}, Map.merge(message, %{advert: advert})) + end + + :ok + end + @impl true def intercept({:advert, _} = signal, message) do handle(signal, message) diff --git a/core/systems/advert/content_page_builder.ex b/core/systems/advert/content_page_builder.ex index d0dfad1a8..345018a91 100644 --- a/core/systems/advert/content_page_builder.ex +++ b/core/systems/advert/content_page_builder.ex @@ -79,14 +79,13 @@ defmodule Systems.Advert.ContentPageBuilder do defp create_tab( :pool, - %{submission: submission}, + %Advert.Model{submission: _} = advert, show_errors, %{fabric: fabric, current_user: user} ) do child = Fabric.prepare_child(fabric, :submission_form, Advert.SubmissionView, %{ - entity: submission, - user: user + vm: Advert.SubmissionViewBuilder.view_model(advert, %{current_user: user}) }) %{ diff --git a/core/systems/advert/submission_view.ex b/core/systems/advert/submission_view.ex index e9223d249..3d3ef738e 100644 --- a/core/systems/advert/submission_view.ex +++ b/core/systems/advert/submission_view.ex @@ -1,66 +1,31 @@ defmodule Systems.Advert.SubmissionView do use CoreWeb.LiveForm - alias Core.Enums.{Genders, NativeLanguages} alias Frameworks.Pixel.Selector alias Frameworks.Pixel.Text - alias Frameworks.Concept.Directable - alias Systems.Advert - alias Systems.Project - alias Systems.Assignment alias Systems.Pool - @enums_mapping %{ - genders: Genders, - native_languages: NativeLanguages - } - - # Update adverts only @impl true - def update( - _, - %{assigns: %{entity: _}} = socket - ) do + def update(%{vm: vm}, socket) do { :ok, socket - |> update_adverts() - |> compose_child(:exclude_adverts) - |> update_ui() + |> assign(vm: vm) + |> build_children() } end - # Initial update - @impl true - def update( - %{ - id: id, - entity: %{criteria: criteria} = submission, - user: user - }, - socket - ) do - { - :ok, - socket - |> assign(id: id) - |> assign(entity: criteria) - |> assign(submission: submission) - |> assign(user: user) - |> update_adverts() - |> update_ui() - } - end - - defp compose_inclusion_selectors(%{assigns: %{inclusion_labels: inclusion_labels}} = socket) do - inclusion_labels + defp compose_inclusion_selectors( + %{assigns: %{vm: %{selector_option_labels: selector_option_labels}}} = socket + ) do + selector_option_labels |> Map.keys() |> Enum.reduce(socket, fn key, socket -> compose_child(socket, key) end) end @impl true - def compose(:exclude_adverts, %{advert_labels: items}) do + def compose(:exclude_adverts, %{vm: %{advert_labels: items}}) do %{ module: Selector, params: %{ @@ -72,8 +37,8 @@ defmodule Systems.Advert.SubmissionView do end @impl true - def compose(key, %{inclusion_labels: inclusion_labels}) do - items = Map.get(inclusion_labels, key) + def compose(:genders, %{vm: %{selector_option_labels: selector_option_labels}}) do + items = Map.get(selector_option_labels, :genders) %{ module: Selector, @@ -85,107 +50,28 @@ defmodule Systems.Advert.SubmissionView do } end - defp update_adverts(%{assigns: %{user: user, submission: submission}} = socket) do - %{id: advert_id, assignment: %{excluded: excluded_assignments} = assignment} = - Advert.Public.get_by_submission(submission, assignment: [:excluded]) - - excluded_assignment_ids = - excluded_assignments - |> Enum.map(& &1.id) - - advert_labels = - Project.Public.list_owned_projects(user, preload: Project.Model.preload_graph(:down)) - |> Enum.flat_map(& &1.root.items) - |> Enum.reject(&(&1.advert == nil)) - |> Enum.map(& &1.advert) - |> Enum.filter(&(&1.id != advert_id)) - |> Enum.map(&to_label(&1, excluded_assignment_ids)) - - excluded_user_ids = Assignment.Public.list_user_ids(excluded_assignment_ids) - - socket - |> assign(assignment: assignment) - |> assign(advert_labels: advert_labels) - |> assign(excluded_user_ids: excluded_user_ids) - end - - defp to_label( - %Advert.Model{ - id: id, - promotion: %{title: title}, - assignment_id: assignment_id - }, - excluded_assignment_ids + defp persist_criteria_changes( + %{assigns: %{vm: %{entity: %{criteria: criteria}}}} = socket, + attrs ) do - excluded = excluded_assignment_ids |> Enum.member?(assignment_id) - %{id: id, value: title, active: excluded} - end + changeset = Pool.CriteriaModel.changeset(criteria, attrs) - defp update_ui(%{assigns: %{entity: criteria}} = socket) do socket - |> compose_child(:exclude_adverts) - |> update_ui(criteria) - |> compose_inclusion_selectors() + |> save(changeset) + |> update_vm_changeset() end - defp update_ui( - %{ - assigns: %{ - submission: %{pool: %{name: pool_name} = pool}, - excluded_user_ids: excluded_user_ids - } - } = socket, - criteria - ) do - inclusion_labels = - Directable.director(pool).inclusion_criteria() - |> Enum.map(&get_inclusion_labels(&1, criteria)) - |> Map.new() - - included_user_ids = - pool - |> Pool.Public.list_participants() - |> Enum.map(& &1.id) - - pool_size = Enum.count(included_user_ids) - pool_title = Pool.Model.title(pool_name) - - sample_size = - Pool.Public.count_eligitable_users(criteria, included_user_ids, excluded_user_ids) - + defp update_vm_changeset(%{assigns: %{vm: vm, changeset: changeset}} = socket) do socket - |> assign( - inclusion_labels: inclusion_labels, - sample_size: sample_size, - pool_size: pool_size, - pool_title: pool_title - ) - end - - defp get_inclusion_labels(field, %Pool.CriteriaModel{} = criteria) when is_atom(field) do - case Map.get(@enums_mapping, field) do - nil -> - nil - - enum_module -> - values = Map.get(criteria, field) - {field, enum_module.labels(values)} - end + |> assign(vm: Map.put(vm, :changeset, changeset)) end - # Saving - def save(socket, %Pool.CriteriaModel{} = entity, attrs) do - changeset = Pool.CriteriaModel.changeset(entity, attrs) - - socket - |> save(changeset) - |> update_ui() - end + defp update_vm_changeset(socket), do: socket - defp inclusion_title(:genders), do: dgettext("eyra-account", "features.gender.title") + defp inclusion_criterium_title(:genders), do: dgettext("eyra-account", "features.gender.title") - defp inclusion_title(:native_languages), - do: dgettext("eyra-account", "features.nativelanguage.title") + defp inclusion_criterium_title(:birth_years), + do: dgettext("eyra-account", "features.birthyear.title") @impl true def handle_event( @@ -195,21 +81,17 @@ defmodule Systems.Advert.SubmissionView do source: %{name: :exclude_adverts}, current_items: current_items }, - %{assigns: %{assignment: assignment}} = socket + %{assigns: %{vm: %{assignment: assignment}}} = socket ) do - socket = - save_closure(socket, fn socket -> - Advert.Public.handle_exclusion(assignment, current_items) + Advert.Public.handle_exclusion(assignment, current_items) + excluded_user_ids = Advert.Public.list_excluded_user_ids(excluded_advert_ids) - excluded_user_ids = Advert.Public.list_excluded_user_ids(excluded_advert_ids) - - socket - |> assign(excluded_user_ids: excluded_user_ids) - |> update_ui() - |> flash_persister_saved() - end) - - {:noreply, socket} + { + :noreply, + socket + |> assign(excluded_user_ids: excluded_user_ids) + |> flash_persister_saved() + } end @impl true @@ -219,17 +101,25 @@ defmodule Systems.Advert.SubmissionView do active_item_ids: selected_values, source: %{name: criteria_field} }, - %{assigns: %{entity: criteria}} = socket + socket ) - when criteria_field in [:genders, :native_languages, :dominant_hands] do + when criteria_field == :genders do attrs = %{criteria_field => selected_values} - socket = - save_closure(socket, fn socket -> - save(socket, criteria, attrs) - end) + { + :noreply, + socket + |> persist_criteria_changes(attrs) + } + end - {:noreply, socket} + @impl true + def handle_event("change", %{"criteria_model" => attrs}, socket) do + { + :noreply, + socket + |> persist_criteria_changes(attrs) + } end @impl true @@ -243,9 +133,9 @@ defmodule Systems.Advert.SubmissionView do <%= raw( dgettext("link-advert", "submission.criteria.status", - sample: "#{@sample_size}", - total: @pool_size, - pool: @pool_title + sample: "#{@vm.sample_size}", + total: @vm.pool_size, + pool: @vm.pool_title ) ) %> @@ -257,14 +147,11 @@ defmodule Systems.Advert.SubmissionView do <.spacing value="M" />
- <%= for field <- Map.keys(@inclusion_labels) do %> -
- <%= inclusion_title(field) %> - <.spacing value="S" /> - <.child name={field} fabric={@fabric} /> -
- <% end %> + <.render_inclusion_selectors selector_option_labels={@vm.selector_option_labels} fabric={@fabric} /> + + <.render_age_inputs changeset={@vm.changeset} myself={@myself} />
+ <.spacing value="XL" />
@@ -274,11 +161,10 @@ defmodule Systems.Advert.SubmissionView do <%= dgettext("link-advert", "exclusion.select.label") %> <.spacing value="S" /> - <%= if Enum.count(@advert_labels) == 0 do %> + <%= if Enum.count(@vm.advert_labels) == 0 do %> <%= dgettext("link-advert", "no.previous.adverts.available") %> <% else %> <.child name={:exclude_adverts} fabric={@fabric} /> - <% end %> <.spacing value="XL" />
@@ -287,4 +173,41 @@ defmodule Systems.Advert.SubmissionView do """ end + + defp render_age_inputs(assigns) do + ~H""" +
+ + <%= inclusion_criterium_title(:birth_years) %> + + <.spacing value="S" /> + <.form :let={form} for={@changeset} phx-change="change" phx-target={@myself}> +
+ <.number_input form={form} field={:min_birth_year} label_text={dgettext("link-advert", "submission.criteria.birth_years.min_label")} /> + <.number_input form={form} field={:max_birth_year} label_text={dgettext("link-advert", "submission.criteria.birth_years.max_label")} /> +
+ +
+ """ + end + + defp render_inclusion_selectors(assigns) do + ~H""" +
+ <%= for field <- Map.keys(@selector_option_labels) do %> +
+ <%= inclusion_criterium_title(field) %> + <.spacing value="S" /> + <.child name={field} fabric={@fabric} /> +
+ <% end %> +
+ """ + end + + defp build_children(socket) do + socket + |> compose_child(:exclude_adverts) + |> compose_inclusion_selectors() + end end diff --git a/core/systems/advert/submission_view_builder.ex b/core/systems/advert/submission_view_builder.ex new file mode 100644 index 000000000..c82aea692 --- /dev/null +++ b/core/systems/advert/submission_view_builder.ex @@ -0,0 +1,132 @@ +defmodule Systems.Advert.SubmissionViewBuilder do + alias Frameworks.Concept.Directable + alias Systems.{Advert, Project, Assignment, Pool} + + @enum_map %{ + genders: Core.Enums.Genders, + native_languages: Core.Enums.NativeLanguages + } + + def view_model(%Advert.Model{submission: submission} = advert, %{current_user: user}) do + criteria = submission.criteria + pool = submission.pool + selector_option_labels = get_inclusion_criteria_labels(pool, criteria) + changeset = Pool.CriteriaModel.changeset(criteria, %{}) + + %{ + assignment: assignment, + advert_labels: advert_labels, + excluded_user_ids: excluded_user_ids + } = adverts_state(user, submission) + + %{ + sample_size: sample_size, + pool_size: pool_size, + pool_title: pool_title + } = pool_stats(pool, criteria, excluded_user_ids) + + %{ + advert: advert, + entity: submission, + user: user, + assignment: assignment, + advert_labels: advert_labels, + excluded_user_ids: excluded_user_ids, + selector_option_labels: selector_option_labels, + sample_size: sample_size, + pool_size: pool_size, + pool_title: pool_title, + changeset: changeset + } + end + + defp adverts_state(user, submission) do + %{ + id: advert_id, + assignment: + %{ + excluded: excluded_assignments + } = assignment + } = Advert.Public.get_by_submission(submission, assignment: [:excluded]) + + excluded_assignment_ids = Enum.map(excluded_assignments, & &1.id) + advert_labels = advert_labels_for_users_projects(user, advert_id, excluded_assignment_ids) + excluded_user_ids = Assignment.Public.list_user_ids(excluded_assignment_ids) + + %{ + assignment: assignment, + advert_labels: advert_labels, + excluded_user_ids: excluded_user_ids + } + end + + # Build labels for adverts from the user's owned projects, excluding the + # advert currently being edited and adverts without an assignment. + defp advert_labels_for_users_projects(user, advert_id, excluded_assignment_ids) do + Project.Public.list_owned_projects(user, preload: Project.Model.preload_graph(:down)) + |> Enum.flat_map(& &1.root.items) + |> Enum.reject(&(&1.advert == nil)) + |> Enum.map(& &1.advert) + |> Enum.reject(&(is_nil(&1) or &1.assignment_id == nil or &1.id == advert_id)) + |> Enum.map(&to_advert_label(&1, excluded_assignment_ids)) + end + + defp to_advert_label( + %Advert.Model{ + id: id, + promotion: %{title: title}, + assignment_id: assignment_id + }, + excluded_assignment_ids + ) do + excluded = Enum.member?(excluded_assignment_ids, assignment_id) + %{id: to_string(id), value: title, active: excluded} + end + + def get_inclusion_criteria_labels(pool, %Pool.CriteriaModel{} = criteria) do + Directable.director(pool).inclusion_criteria() + |> Enum.map(&labels_for_field(&1, criteria)) + |> Enum.reject(&is_nil/1) + |> Map.new() + end + + # birth_years are handled with number inputs, not selectors + defp labels_for_field(:birth_years, %Pool.CriteriaModel{}), do: nil + + # genders: filter out "prefer_not_to_say" as it is not a valid inclusion criterium + defp labels_for_field(:genders, %Pool.CriteriaModel{} = criteria) do + enum_module = Map.fetch!(@enum_map, :genders) + + allowed_values = + enum_module.values() + |> Enum.reject(&(&1 == :prefer_not_to_say)) + + {:genders, enum_module.labels(Map.get(criteria, :genders, []), allowed_values)} + end + + defp labels_for_field(field, %Pool.CriteriaModel{} = criteria) when is_atom(field) do + case Map.get(@enum_map, field) do + nil -> nil + enum_module -> {field, enum_module.labels(Map.get(criteria, field))} + end + end + + defp pool_stats( + %Pool.Model{name: pool_name} = pool, + %Pool.CriteriaModel{} = criteria, + excluded_user_ids + ) do + user_ids_in_pool = + pool + |> Pool.Public.list_participants() + |> Enum.map(& &1.id) + + pool_size = Enum.count(user_ids_in_pool) + pool_title = Pool.Model.title(pool_name) + + sample_size = + Pool.Public.count_eligitable_users(criteria, user_ids_in_pool, excluded_user_ids) + + %{sample_size: sample_size, pool_size: pool_size, pool_title: pool_title} + end +end diff --git a/core/systems/pool/_public.ex b/core/systems/pool/_public.ex index dafaf28c0..789b448a8 100644 --- a/core/systems/pool/_public.ex +++ b/core/systems/pool/_public.ex @@ -276,6 +276,7 @@ defmodule Systems.Pool.Public do |> Multi.update(:criteria, changeset) |> Multi.run(:dispatch, fn _, %{criteria: criteria} -> Signal.Public.dispatch!({:criteria, :updated}, %{criteria: criteria}) + {:ok, true} end) |> Repo.transaction() end @@ -297,7 +298,9 @@ defmodule Systems.Pool.Public do def count_eligitable_users( %Pool.CriteriaModel{ - genders: genders + genders: genders, + min_birth_year: min_birth_year, + max_birth_year: max_birth_year }, include, exclude @@ -306,6 +309,7 @@ defmodule Systems.Pool.Public do query_count_users(include, exclude) |> optional_where(:gender, genders) + |> optional_where_birth_year(min_birth_year, max_birth_year) |> Repo.one() end @@ -331,6 +335,24 @@ defmodule Systems.Pool.Public do where(query, [user, features], field(features, ^field_name) in ^values) end + defp optional_where_birth_year(query, nil, nil), do: query + + defp optional_where_birth_year(query, min_year, nil) do + where(query, [user, features], field(features, :birth_year) >= ^min_year) + end + + defp optional_where_birth_year(query, nil, max_year) do + where(query, [user, features], field(features, :birth_year) <= ^max_year) + end + + defp optional_where_birth_year(query, min_year, max_year) do + where( + query, + [user, features], + field(features, :birth_year) >= ^min_year and field(features, :birth_year) <= ^max_year + ) + end + def target_achieved?( %Pool.Model{target: target}, %{balance_credit: balance_credit} diff --git a/core/systems/pool/criteria_model.ex b/core/systems/pool/criteria_model.ex index 1e3560531..b345cdd12 100644 --- a/core/systems/pool/criteria_model.ex +++ b/core/systems/pool/criteria_model.ex @@ -12,28 +12,59 @@ defmodule Systems.Pool.CriteriaModel do schema "eligibility_criteria" do field(:genders, {:array, Ecto.Enum}, values: Genders.schema_values()) + field(:min_birth_year, :integer) + field(:max_birth_year, :integer) belongs_to(:submission, SubmissionModel) timestamps() end - @fields ~w(genders)a + @fields ~w(genders min_birth_year max_birth_year)a @doc false def changeset(profile, attrs) do profile |> cast(attrs, @fields) + |> validate_min_max() + end + + defp validate_min_max(changeset) do + min_year = get_field(changeset, :min_birth_year) + max_year = get_field(changeset, :max_birth_year) + + if min_year && max_year && min_year > max_year do + add_error(changeset, :max_birth_year, "must be greater than or equal to min birth year") + else + changeset + end end def eligitable?(nil, nil), do: true def eligitable?(criteria, nil) do - meets?(criteria.genders, nil) + meets?(criteria.genders, nil) and meets_birth_year?(criteria, nil) + end + + def eligitable?(criteria, %{gender: gender, birth_year: birth_year}) do + meets?(criteria.genders, gender) and meets_birth_year?(criteria, birth_year) end def eligitable?(criteria, %{gender: gender}) do - meets?(criteria.genders, gender) + meets?(criteria.genders, gender) and meets_birth_year?(criteria, nil) + end + + defp meets_birth_year?(criteria, birth_year) do + min_year = criteria.min_birth_year + max_year = criteria.max_birth_year + + cond do + min_year == nil and max_year == nil -> true + birth_year == nil -> false + min_year != nil and birth_year < min_year -> false + max_year != nil and birth_year > max_year -> false + true -> true + end end defp meets?(field, value) when is_list(value) do @@ -44,3 +75,14 @@ defmodule Systems.Pool.CriteriaModel do field == nil || Enum.empty?(field) || Enum.member?(field, value) end end + +defimpl Core.Persister, for: Systems.Pool.CriteriaModel do + alias Systems.Pool + + def save(criteria, changeset) do + case Pool.Public.update(criteria, changeset) do + {:ok, %{criteria: criteria}} -> {:ok, criteria} + _ -> {:error, changeset} + end + end +end diff --git a/core/systems/student/_director.ex b/core/systems/student/_director.ex index 593dd26aa..090ff8bf6 100644 --- a/core/systems/student/_director.ex +++ b/core/systems/student/_director.ex @@ -27,7 +27,7 @@ defmodule Systems.Student.Director do @impl true def inclusion_criteria() do - [:genders, :native_languages] + [:genders] end @impl true