Skip to content

Commit

Permalink
Fixed bump of consent revision
Browse files Browse the repository at this point in the history
  • Loading branch information
mellelieuwes committed Nov 22, 2023
1 parent a6aca6a commit 693f420
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 71 deletions.
4 changes: 3 additions & 1 deletion core/lib/external_sign_in.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions core/priv/gettext/en/LC_MESSAGES/eyra-consent.po
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ msgstr "<div>Write here your custom consent terms and conditions..</div>"
#, 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."
8 changes: 8 additions & 0 deletions core/priv/gettext/eyra-consent.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
8 changes: 8 additions & 0 deletions core/priv/gettext/nl/LC_MESSAGES/eyra-consent.po
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ msgstr "<div>Beschrijf hier de consent voorwaarden</div>"
#, 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."
44 changes: 27 additions & 17 deletions core/systems/assignment/crew_page_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion core/systems/assignment/gdpr_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 77 additions & 32 deletions core/systems/consent/_public.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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
17 changes: 14 additions & 3 deletions core/systems/consent/revision_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
32 changes: 15 additions & 17 deletions core/test/systems/consent/_public_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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: "<div>Beschrijf hier de consent voorwaarden</div>",
signatures: []
} = Consent.Public.latest_unlocked_revision_safe(agreement, [:signatures])
source: "<div>Beschrijf hier de consent voorwaarden</div>"
} = 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)
Expand All @@ -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
Expand Down

0 comments on commit 693f420

Please sign in to comment.