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

2589 retention period events #2599

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ and this project adheres to
Arcade videos [#2563](https://github.com/OpenFn/lightning/issues/2563)
- Store user preferences in database
[#2564](https://github.com/OpenFn/lightning/issues/2564)
- Create audit events when the retention periods for a project's dataclips
and history are modified.
[#2589](https://github.com/OpenFn/lightning/issues/2589)

### Changed

Expand Down
29 changes: 24 additions & 5 deletions lib/lightning/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Lightning.Projects do
alias Lightning.ExportUtils
alias Lightning.Invocation.Dataclip
alias Lightning.Invocation.Step
alias Lightning.Projects.Audit
alias Lightning.Projects.Events
alias Lightning.Projects.File
alias Lightning.Projects.Project
Expand Down Expand Up @@ -286,22 +287,40 @@ defmodule Lightning.Projects do
{:error, %Ecto.Changeset{}}

"""
def update_project(%Project{} = project, attrs) do
def update_project(%Project{} = project, attrs, user \\ nil) do
changeset = Project.changeset(project, attrs)

case Repo.update(changeset) do
{:ok, updated_project} ->
Multi.new()
|> Multi.update(:project, changeset)
|> maybe_audit_changes(changeset, user)
|> Repo.transaction()
|> case do
{:ok, %{project: updated_project}} ->
if retention_setting_updated?(changeset) do
send_data_retention_change_email(updated_project)
end

{:ok, updated_project}

error ->
error
{:error, :project, changeset, _changes_so_far} ->
{:error, changeset}

# 2024-10-29 Not tested at module-level
# due to the difficulty of simulating a failure
# without mocking
{:error, _operation, changeset, _changes_so_far} ->
{:audit_creation_error, changeset}
end
end

defp maybe_audit_changes(multi, _changeset, nil), do: multi

defp maybe_audit_changes(multi, changeset, user) do
multi
|> Audit.history_retention_period_updated(changeset, user)
|> Audit.dataclip_retention_period_updated(changeset, user)
end

@spec update_project_with_users(Project.t(), map(), boolean()) ::
{:ok, Project.t()} | {:error, Ecto.Changeset.t()}
def update_project_with_users(
Expand Down
41 changes: 41 additions & 0 deletions lib/lightning/projects/audit.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Lightning.Projects.Audit do
@moduledoc """
Generate Audit changesets for selected changes to project settings.
"""
use Lightning.Auditing.Audit,
repo: Lightning.Repo,
item: "project",
events: [
"dataclip_retention_period_updated",
"history_retention_period_updated"
]

alias Ecto.Multi

require Logger

def history_retention_period_updated(multi, changeset, user) do
event_changeset(:history_retention_period, changeset, user)
|> maybe_extend_multi(multi, :audit_history_retention)
end

def dataclip_retention_period_updated(multi, changeset, user) do
event_changeset(:dataclip_retention_period, changeset, user)
|> maybe_extend_multi(multi, :audit_dataclip_retention)
end

defp event_changeset(field, %{data: %{id: project_id}} = changeset, user) do
"#{field}_updated"
|> event(project_id, user.id, filter_changes(changeset, field))
end

defp maybe_extend_multi(:no_changes, multi, _op_name), do: multi

defp maybe_extend_multi(audit_changeset, multi, op_name) do
multi |> Multi.insert(op_name, audit_changeset)
end

defp filter_changes(%{changes: changes} = changeset, field) do
changeset |> Map.merge(%{changes: changes |> Map.take([field])})
end
end
9 changes: 8 additions & 1 deletion lib/lightning_web/live/project_live/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,9 @@ defmodule LightningWeb.ProjectLive.Settings do
end

defp save_project(socket, project_params) do
case Projects.update_project(socket.assigns.project, project_params) do
socket.assigns.project
|> Projects.update_project(project_params, socket.assigns.current_user)
|> case do
{:ok, project} ->
{:noreply,
socket
Expand All @@ -599,6 +601,11 @@ defmodule LightningWeb.ProjectLive.Settings do

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :project_changeset, changeset)}

{:audit_creation_error, _changeset} ->
{:noreply,
socket
|> put_flash(:error, "Changes couldn't be saved, please try again")}
end
end

Expand Down
154 changes: 154 additions & 0 deletions test/lightning/projects/audit_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
defmodule Lightning.Projects.AuditTest do
use Lightning.DataCase, async: true

alias Ecto.Multi

alias Lightning.Projects.Audit
alias Lightning.Projects.Project

describe ".history_retention_period_updated" do
setup do
project =
insert(
:project,
dataclip_retention_period: 14,
history_retention_period: 90,
retention_policy: :retain_all
)

user = insert(:user)

%{
project: project,
user: user
}
end

test "adds operation to multi if history retention period is updated", %{
project: %{id: project_id} = project,
user: %{id: user_id} = user
} do
attrs = %{
dataclip_retention_period: 7,
history_retention_period: 30,
retention_policy: :retain_with_errors
}

changeset = project |> Project.changeset(attrs)

[audit_history_retention: {:insert, changeset, []}] =
Multi.new()
|> Audit.history_retention_period_updated(changeset, user)
|> Multi.to_list()

assert %{
changes: %{
event: "history_retention_period_updated",
item_type: "project",
item_id: ^project_id,
actor_id: ^user_id,
changes: %{
changes: %{
before: %{history_retention_period: 90},
after: %{history_retention_period: 30}
}
}
},
valid?: true
} = changeset
end

test "does not add operation if history retention period is unchanged", %{
project: project,
user: user
} do
attrs = %{
dataclip_retention_period: 7,
history_retention_period: 90,
retention_policy: :retain_with_errors
}

changeset = project |> Project.changeset(attrs)

updated_multi =
Multi.new()
|> Audit.history_retention_period_updated(changeset, user)
|> Multi.to_list()

assert updated_multi == []
end
end

describe ".dataclip_retention_period_updated" do
setup do
project =
insert(
:project,
dataclip_retention_period: 14,
history_retention_period: 90,
retention_policy: :retain_all
)

user = insert(:user)

%{
project: project,
user: user
}
end

test "adds operation to multi if dataclip retention period is updated", %{
project: %{id: project_id} = project,
user: %{id: user_id} = user
} do
attrs = %{
dataclip_retention_period: 7,
history_retention_period: 30,
retention_policy: :retain_with_errors
}

changeset = project |> Project.changeset(attrs)

[audit_dataclip_retention: {:insert, changeset, []}] =
Multi.new()
|> Audit.dataclip_retention_period_updated(changeset, user)
|> Multi.to_list()

assert %{
changes: %{
event: "dataclip_retention_period_updated",
item_type: "project",
item_id: ^project_id,
actor_id: ^user_id,
changes: %{
changes: %{
before: %{dataclip_retention_period: 14},
after: %{dataclip_retention_period: 7}
}
}
},
valid?: true
} = changeset
end

test "does not add operation if dataclip retention period is unchanged", %{
project: project,
user: user
} do
attrs = %{
dataclip_retention_period: 14,
history_retention_period: 30,
retention_policy: :retain_with_errors
}

changeset = project |> Project.changeset(attrs)

updated_multi =
Multi.new()
|> Audit.dataclip_retention_period_updated(changeset, user)
|> Multi.to_list()

assert updated_multi == []
end
end
end
Loading