diff --git a/core/lib/external_sign_in.ex b/core/lib/external_sign_in.ex index ee5885c70..410dda794 100644 --- a/core/lib/external_sign_in.ex +++ b/core/lib/external_sign_in.ex @@ -11,7 +11,9 @@ defmodule ExternalSignIn do register_user(organisation, external_id) end - CoreWeb.UserAuth.log_in_user_without_redirect(conn, user) + conn + |> CoreWeb.UserAuth.log_in_user_without_redirect(user) + |> Plug.Conn.assign(:current_user, user) end def get_user_by_external_id(external_id) do diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-consent.po b/core/priv/gettext/en/LC_MESSAGES/eyra-consent.po index 514c23985..4434c6ee2 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-consent.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-consent.po @@ -22,3 +22,11 @@ msgstr "
Write here your custom consent terms and conditions..
" #, elixir-autogen, elixir-format msgid "onboarding.consent.checkbox" msgstr "I have read and agree with the above terms" + +#, elixir-autogen, elixir-format +msgid "locked.error.message" +msgstr "This version of the consent text has already been signed by participants. Please refresh the page to continue changing the text." + +#, elixir-autogen, elixir-format +msgid "out_of_sync.error.message" +msgstr "Someone made changes to the consent text. Please refresh the page to continue." diff --git a/core/priv/gettext/eyra-consent.pot b/core/priv/gettext/eyra-consent.pot index 3c1fbcac2..d789ea5e9 100644 --- a/core/priv/gettext/eyra-consent.pot +++ b/core/priv/gettext/eyra-consent.pot @@ -22,3 +22,11 @@ msgstr "" #, elixir-autogen, elixir-format msgid "onboarding.consent.checkbox" msgstr "" + +#, elixir-autogen, elixir-format +msgid "locked.error.message" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "out_of_sync.error.message" +msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-consent.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-consent.po index 37ca4e1e4..ae40207e9 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-consent.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-consent.po @@ -22,3 +22,11 @@ msgstr "
Beschrijf hier de consent voorwaarden
" #, elixir-autogen, elixir-format msgid "onboarding.consent.checkbox" msgstr "Ik heb de bovenstaande voorwaarden gelezen en ga ermee akkoord" + +#, elixir-autogen, elixir-format +msgid "locked.error.message" +msgstr "Deze versie van de consent tekst is al ondertekend door participanten. Ververs de pagina om verdere aanpassingen te maken aan de tekst." + +#, elixir-autogen, elixir-format +msgid "out_of_sync.error.message" +msgstr "Iemand heeft de consent tekst is aangepast. Ververs de pagina om verder te gaan." diff --git a/core/systems/assignment/crew_page_builder.ex b/core/systems/assignment/crew_page_builder.ex index a1d79b86e..1206d2daf 100644 --- a/core/systems/assignment/crew_page_builder.ex +++ b/core/systems/assignment/crew_page_builder.ex @@ -13,44 +13,54 @@ defmodule Systems.Assignment.CrewPageBuilder do end defp flow(%{status: status} = assignment, assigns) do - if is_tester?(assignment, assigns) or status == :online do - flow(assignment, assigns, current_flow(assigns)) + is_tester? = is_tester?(assignment, assigns) + + if is_tester? or status == :online do + flow(assignment, assigns, current_flow(assigns), is_tester?) else [] end end - defp flow(assignment, assigns, nil), do: full_flow(assignment, assigns) + defp flow(assignment, assigns, nil, is_tester?), do: full_flow(assignment, assigns, is_tester?) - defp flow(assignment, assigns, current_flow) do - full_flow(assignment, assigns) + defp flow(assignment, assigns, current_flow, is_tester?) do + full_flow(assignment, assigns, is_tester?) |> Enum.filter(fn %{ref: %{id: id}} -> Enum.find(current_flow, &(&1.ref.id == id)) != nil end) end - defp full_flow(assignment, assigns) do + defp full_flow(assignment, assigns, is_tester?) do [ - consent_view(assignment, assigns), - work_view(assignment, assigns) + consent_view(assignment, assigns, is_tester?), + work_view(assignment, assigns, is_tester?) ] |> Enum.filter(&(&1 != nil)) end defp current_flow(%{fabric: %{children: children}}), do: children - defp consent_view(%{consent_agreement: nil}, _), do: nil + defp consent_view(%{consent_agreement: nil}, _, _), do: nil - defp consent_view(%{consent_agreement: consent_agreement}, %{current_user: user, fabric: fabric}) do - revision = Consent.Public.latest_revision(consent_agreement, [:signatures]) + defp consent_view( + %{consent_agreement: consent_agreement}, + %{current_user: user, fabric: fabric}, + is_tester? + ) do + if Consent.Public.has_signature(consent_agreement, user) and not is_tester? do + nil + else + revision = Consent.Public.latest_revision(consent_agreement, [:signatures]) - Fabric.prepare_child(fabric, :onboarding_view, Assignment.OnboardingConsentView, %{ - revision: revision, - user: user - }) + Fabric.prepare_child(fabric, :onboarding_view, Assignment.OnboardingConsentView, %{ + revision: revision, + user: user + }) + end end - defp work_view(assignment, %{fabric: fabric} = assigns) do + defp work_view(assignment, %{fabric: fabric} = assigns, _) do work_items = work_items(assignment, assigns) Fabric.prepare_child(fabric, :work_view, Assignment.CrewWorkView, %{ @@ -84,7 +94,7 @@ defmodule Systems.Assignment.CrewPageBuilder do if task = Crew.Public.get_task(crew, identifier) do task else - Crew.Public.create_task(crew, [member], identifier) + Crew.Public.create_task!(crew, [member], identifier) end end end diff --git a/core/systems/assignment/gdpr_form.ex b/core/systems/assignment/gdpr_form.ex index 856fb6784..fd60d3600 100644 --- a/core/systems/assignment/gdpr_form.ex +++ b/core/systems/assignment/gdpr_form.ex @@ -48,7 +48,7 @@ defmodule Systems.Assignment.GdprForm do %{ module: Consent.RevisionForm, params: %{ - entity: Consent.Public.latest_unlocked_revision_safe(consent_agreement) + entity: Consent.Public.latest_revision(consent_agreement) } } end diff --git a/core/systems/consent/_public.ex b/core/systems/consent/_public.ex index 3a16c0ce1..87d95a68f 100644 --- a/core/systems/consent/_public.ex +++ b/core/systems/consent/_public.ex @@ -21,12 +21,37 @@ defmodule Systems.Consent.Public do |> Ecto.Changeset.put_assoc(:auth_node, auth_node) end - def create_revision(source, agreement) do - prepare_revision(source, agreement) + def bump_revision_if_needed(agreement_id) when is_integer(agreement_id) do + agreement_id + |> get_agreement!() + |> bump_revision_if_needed() + end + + def bump_revision_if_needed(agreement) do + Multi.new() + |> Multi.run(:revision, fn _, _ -> + case latest_revision(agreement, [:signatures]) do + nil -> create_revision(agreement, dgettext("eyra-consent", "default.consent.text")) + %{source: source, signatures: [_ | _]} -> create_revision(agreement, source) + revision -> {:ok, revision} + end + end) + |> Repo.transaction() + end + + def bump_revision_if_needed!(agreement) do + case bump_revision_if_needed(agreement) do + {:ok, %{revision: revision}} -> revision + _ -> nil + end + end + + def create_revision(agreement, source) do + prepare_revision(agreement, source) |> Repo.insert() end - def prepare_revision(source, agreement) when is_binary(source) do + def prepare_revision(agreement, source) when is_binary(source) do %Consent.RevisionModel{} |> Consent.RevisionModel.changeset(%{source: source}) |> Ecto.Changeset.put_assoc(:agreement, agreement) @@ -52,8 +77,19 @@ defmodule Systems.Consent.Public do Repo.get!(Consent.RevisionModel, id) |> Repo.preload(preload) end - def has_signature(revision, user) do - get_signature(revision, user) != nil + def has_signature(context, user) do + get_signature(context, user) != nil + end + + def get_signature(%Consent.AgreementModel{id: agreement_id}, %Core.Accounts.User{id: user_id}) do + from(s in Consent.SignatureModel, + join: r in Consent.RevisionModel, + on: r.id == s.revision_id, + where: s.user_id == ^user_id, + where: r.agreement_id == ^agreement_id + ) + |> Repo.all() + |> List.first() end def get_signature(%Consent.RevisionModel{id: revision_id}, %Core.Accounts.User{id: user_id}) do @@ -74,25 +110,6 @@ defmodule Systems.Consent.Public do |> Repo.all() end - def latest_unlocked_revision_safe(agreement, preload \\ []) do - if revision = latest_unlocked_revision(agreement, preload) do - revision - else - source = - if revision = latest_revision(agreement, preload) do - revision.source - else - dgettext("eyra-consent", "default.consent.text") - end - - create_revision(source, agreement) - end - - query_unlocked_revisions(agreement, preload) - |> Repo.all() - |> List.first() - end - def latest_unlocked_revision(agreement, preload \\ []) do query_unlocked_revisions(agreement, preload) |> Repo.all() @@ -134,13 +151,19 @@ defmodule Systems.Consent.Public do %Ecto.Changeset{data: %Consent.RevisionModel{id: id, updated_at: updated_at}} = changeset ) do Multi.new() - |> Multi.run(:validate_timestamp, fn _, _ -> - %{updated_at: stored_updated_at} = Consent.Public.get_revision!(id) + |> Multi.run(:validate, fn _, _ -> + %{updated_at: stored_updated_at, signatures: signatures} = + Consent.Public.get_revision!(id, [:signatures]) + + cond do + stored_updated_at != updated_at -> + {:error, :out_of_sync} - if stored_updated_at == updated_at do - {:ok, :valid} - else - {:error, "Revision out of sync"} + not Enum.empty?(signatures) -> + {:error, :locked} + + true -> + {:ok, :valid} end end) |> Multi.update(:consent_revision, changeset) @@ -150,10 +173,32 @@ defmodule Systems.Consent.Public do end defimpl Core.Persister, for: Systems.Consent.RevisionModel do + import CoreWeb.Gettext + def save(_revision, changeset) do case Systems.Consent.Public.update_revision(changeset) do - {:ok, %{consent_revision: revision}} -> {:ok, revision} - _ -> {:error, changeset} + {:ok, %{consent_revision: revision}} -> + {:ok, revision} + + {:error, _, _, _} = error -> + {:error, changeset |> handle_error(error)} end end + + defp handle_error(changeset, {:error, :validate, :locked, _}) do + Systems.Consent.Public.bump_revision_if_needed!(changeset.data.agreement_id) + + changeset + |> Ecto.Changeset.add_error(:locked, dgettext("eyra-consent", "locked.error.message")) + end + + defp handle_error(changeset, {:error, :validate, :out_of_sync, _}) do + changeset + |> Ecto.Changeset.add_error( + :out_of_sync, + dgettext("eyra-consent", "out_of_sync.error.message") + ) + end + + defp handle_error(changeset, _), do: changeset end diff --git a/core/systems/consent/revision_form.ex b/core/systems/consent/revision_form.ex index 419359bb3..cad4c5035 100644 --- a/core/systems/consent/revision_form.ex +++ b/core/systems/consent/revision_form.ex @@ -48,12 +48,23 @@ defmodule Systems.Consent.RevisionForm do |> assign(entity: entity) |> flash_persister_saved() - {:error, _} -> - socket - |> flash_persister_error(dgettext("eyra-consent", "consent-out-of-sync-error")) + {:error, changeset} -> + socket |> handle_save_errors(changeset) end end + defp handle_save_errors(socket, %{errors: errors}) do + handle_save_errors(socket, errors) + end + + defp handle_save_errors(socket, [{_, {message, _}} | _]) do + socket |> flash_persister_error(message) + end + + defp handle_save_errors(socket, _) do + socket |> flash_persister_error() + end + @impl true def render(assigns) do ~H""" diff --git a/core/test/systems/consent/_public_test.exs b/core/test/systems/consent/_public_test.exs index 2426f3e92..b41ed419f 100644 --- a/core/test/systems/consent/_public_test.exs +++ b/core/test/systems/consent/_public_test.exs @@ -18,10 +18,10 @@ defmodule Systems.Consent.PublicTest do Multi.new() |> Multi.insert(:agreement, Consent.Public.prepare_agreement(Authorization.prepare_node())) |> Multi.insert(:revision1, fn %{agreement: agreement} -> - Consent.Public.prepare_revision("revision1", agreement) + Consent.Public.prepare_revision(agreement, "revision1") end) |> Multi.insert(:revision2, fn %{agreement: agreement} -> - Consent.Public.prepare_revision("revision2", agreement) + Consent.Public.prepare_revision(agreement, "revision2") end) |> Repo.transaction() @@ -48,7 +48,7 @@ defmodule Systems.Consent.PublicTest do Multi.new() |> Multi.insert(:agreement, Consent.Public.prepare_agreement(Authorization.prepare_node())) |> Multi.insert(:revision1, fn %{agreement: agreement} -> - Consent.Public.prepare_revision("revision1", agreement) + Consent.Public.prepare_revision(agreement, "revision1") end) |> Multi.insert(:signatureA1, fn %{revision1: revision1} -> Consent.Public.prepare_signature(revision1, user_a) @@ -57,7 +57,7 @@ defmodule Systems.Consent.PublicTest do Consent.Public.prepare_signature(revision1, user_b) end) |> Multi.insert(:revision2, fn %{agreement: agreement} -> - Consent.Public.prepare_revision("revision2", agreement) + Consent.Public.prepare_revision(agreement, "revision2") end) |> Multi.insert(:signatureA2, fn %{revision2: revision2} -> Consent.Public.prepare_signature(revision2, user_a) @@ -120,26 +120,25 @@ defmodule Systems.Consent.PublicTest do assert Consent.Public.latest_unlocked_revision(agreement, [:agreement]) == nil end - test "latest_unlocked_revision_safe/2 returns new revision in empty agreement" do + test "bump_revision_if_needed!/1 returns first revision" do agreement = Factories.insert!(:consent_agreement) assert %Systems.Consent.RevisionModel{ - source: "
Beschrijf hier de consent voorwaarden
", - signatures: [] - } = Consent.Public.latest_unlocked_revision_safe(agreement, [:signatures]) + source: "
Beschrijf hier de consent voorwaarden
" + } = Consent.Public.bump_revision_if_needed!(agreement) end - test "latest_unlocked_revision_safe/2 returns latest revision" do + test "bump_revision_if_needed!/1 returns latest revision" do agreement = Factories.insert!(:consent_agreement) - _revision = Factories.insert!(:consent_revision, %{agreement: agreement, source: "source"}) + %{id: id} = Factories.insert!(:consent_revision, %{agreement: agreement, source: "source"}) assert %Systems.Consent.RevisionModel{ - source: "source", - signatures: [] - } = Consent.Public.latest_unlocked_revision_safe(agreement, [:signatures]) + id: ^id, + source: "source" + } = Consent.Public.bump_revision_if_needed!(agreement) end - test "latest_unlocked_revision_safe/2 returns new revision on top of locked revision" do + test "bump_revision_if_needed!/1 returns new revision on top of locked revision" do user = Factories.insert!(:member) agreement = Factories.insert!(:consent_agreement) @@ -148,9 +147,8 @@ defmodule Systems.Consent.PublicTest do assert %Systems.Consent.RevisionModel{ id: revision_2_id, - source: "source", - signatures: [] - } = Consent.Public.latest_unlocked_revision_safe(agreement, [:signatures]) + source: "source" + } = Consent.Public.bump_revision_if_needed!(agreement) assert revision_1.id != revision_2_id end