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