Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed bump of consent revision and external signin #484

Merged
merged 1 commit into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading