Skip to content

Commit

Permalink
feature: add delete after duration column that stores the time to wai…
Browse files Browse the repository at this point in the history
…t before deletion (#223)
  • Loading branch information
nwittstruck authored Mar 20, 2024
1 parent 13b3416 commit 7218ba3
Show file tree
Hide file tree
Showing 20 changed files with 257 additions and 221 deletions.
8 changes: 6 additions & 2 deletions lib/qrstorage/qr_codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ defmodule Qrstorage.QrCodes do
:ok
"""
def delete_old_qr_codes() do
now = Timex.now()
Repo.delete_all(from q in QrCode, where: ^now > q.delete_after, select: [:id, :content_type])
# delete all codes that are older than last access date + delete_after_months
Repo.delete_all(
from q in QrCode,
where: fragment("NOW() > ? + INTERVAL '1 month' * ?", q.last_accessed_at, q.delete_after_months),
select: [:id, :content_type]
)
end

def delete_qr_code(%QrCode{} = qr_code) do
Expand Down
24 changes: 22 additions & 2 deletions lib/qrstorage/qr_codes/qr_code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ defmodule Qrstorage.QrCodes.QrCode do
@text_length_limits %{link: 1500, audio: 2000, text: 4000, recording: 1}

@max_delete_after_year 9999
@valid_delete_after_months [0, 1, 6, 12, 36]

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "qrcodes" do
field :delete_after, :date
field :delete_after_months, :integer, default: 1
field :text, :string
field :translated_text, :string
field :audio_file, :binary
Expand All @@ -49,7 +51,7 @@ defmodule Qrstorage.QrCodes.QrCode do
qr_code
|> cast(attrs, [
:text,
:delete_after,
:delete_after_months,
:color,
:language,
:hide_text,
Expand All @@ -63,11 +65,13 @@ defmodule Qrstorage.QrCodes.QrCode do
|> scrub_text
|> validate_text_length(:text)
|> validate_inclusion(:color, @colors)
|> validate_inclusion(:delete_after_months, @valid_delete_after_months)
|> validate_delete_after_months_by_type(:delete_after_months)
|> validate_inclusion(:content_type, @content_types)
|> validate_inclusion(:dots_type, @dots_types)
|> validate_audio_type(:content_type)
|> validate_link(:text)
|> validate_required([:text, :delete_after, :content_type, :dots_type])
|> validate_required([:text, :delete_after_months, :content_type, :dots_type])
end

def store_audio_file(qr_code, attrs) do
Expand Down Expand Up @@ -158,6 +162,22 @@ defmodule Qrstorage.QrCodes.QrCode do
end)
end

def validate_delete_after_months_by_type(changeset, field) do
# We want to make sure that links and recordings are stored for 0 months and 1 month
validate_change(changeset, field, fn field, value ->
case get_field(changeset, :content_type) do
:recording ->
if value == 1, do: [], else: [{field, "Storage duration is invalid"}]

:link ->
if value == 0, do: [], else: [{field, "Storage duration is invalid"}]

_ ->
[]
end
end)
end

def translation_changed_text(qr_code) do
qr_code.text != qr_code.translated_text
end
Expand Down
19 changes: 19 additions & 0 deletions lib/qrstorage/qr_codes/qr_code_migration_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Qrstorage.QrCodes.QrCodeMigrationHelper do
@spec add_delete_after_months(Ecto.Repo.t()) :: any()
def add_delete_after_months(repo) do
repo.query!(
"UPDATE qrcodes
SET delete_after_months =
CASE
WHEN DATE_TRUNC('hour', delete_after) <= DATE_TRUNC('hour', inserted_at + INTERVAL '1 hour') THEN 0
WHEN DATE_TRUNC('day', delete_after) <= DATE_TRUNC('day', inserted_at + INTERVAL '1 month') THEN 1
WHEN DATE_TRUNC('day', delete_after) <= DATE_TRUNC('day', inserted_at + INTERVAL '6 months') THEN 6
WHEN DATE_TRUNC('day', delete_after) <= DATE_TRUNC('day', inserted_at + INTERVAL '12 months') THEN 12
ELSE 36
END;
",
[],
log: :info
)
end
end
33 changes: 3 additions & 30 deletions lib/qrstorage/services/qr_code_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ defmodule Qrstorage.Services.QrCodeService do
require Logger

def create_qr_code(qr_code_params) do
qr_code_params = qr_code_params |> convert_delete_after() |> convert_deltas()
qr_code_params = qr_code_params |> convert_deltas()

case QrCodes.create_qr_code(qr_code_params) do
{:ok, %QrCode{} = qr_code} ->
case handle_audio_types(qr_code, qr_code_params) do
case handle_types(qr_code, qr_code_params) do
{:error, error_message} ->
# delete qr code and show error
Logger.error("deleting qr code after creation, because the audio part could not be saved")
Expand All @@ -31,33 +31,6 @@ defmodule Qrstorage.Services.QrCodeService do
end
end

defp convert_delete_after(qr_code_params) do
# delete links after one day. We only need them to display the qr code properly and for preview purposes.
delete_after =
case Map.get(qr_code_params, "content_type") do
"link" ->
Timex.shift(Timex.now(), hours: 1)

"recording" ->
Timex.shift(Timex.now(), months: 1)

_ ->
months = Map.get(qr_code_params, "delete_after") |> Integer.parse() |> elem(0)

if months == 0 do
# Unfortunately, postgrex doesnt support postgres infinity type,
# so we have to fall back to date far away in the future:
# https://elixirforum.com/t/support-infinity-values-for-date-type/20713/17
Timex.end_of_year(QrCode.max_delete_after_year())
else
Timex.shift(Timex.now(), months: months)
end
end

qr_code_params = Map.put(qr_code_params, "delete_after", delete_after)
qr_code_params
end

defp convert_deltas(qr_code_params) do
# we need to convert the deltas to json:
deltas_json =
Expand Down Expand Up @@ -87,7 +60,7 @@ defmodule Qrstorage.Services.QrCodeService do
Qrstorage.Services.TranslationService.add_translation(qr_code)
end

defp handle_audio_types(qr_code, qr_code_params) do
defp handle_types(qr_code, qr_code_params) do
case qr_code.content_type do
:audio -> handle_audio_qr_code(qr_code)
:recording -> handle_recording_qr_code(qr_code, qr_code_params)
Expand Down
11 changes: 11 additions & 0 deletions lib/qrstorage/worker/remove_codes_worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Qrstorage.Worker.RemoveCodesWorker do
delete_recordings(deleted_codes)
delete_tts(deleted_codes)
Logger.info("Finished deleting old qr codes: #{deleted_count}")
print_frequencies(deleted_codes)

:ok
end
Expand Down Expand Up @@ -47,4 +48,14 @@ defmodule Qrstorage.Worker.RemoveCodesWorker do
code.id
end)
end

defp print_frequencies(deleted_codes) do
deleted_frequencies =
deleted_codes
|> Enum.frequencies_by(fn code ->
code.content_type
end)

Logger.info("Codes deleted by type: #{inspect(deleted_frequencies)}")
end
end
5 changes: 0 additions & 5 deletions lib/qrstorage_web/controllers/qr_code_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ defmodule QrstorageWeb.QrCodeController do
def create(conn, %{"qr_code" => qr_code_params}) do
case Qrstorage.Services.QrCodeService.create_qr_code(qr_code_params) do
{:ok, qr_code} ->
conn =
if QrCode.stored_indefinitely?(qr_code),
do: put_flash(conn, :admin_url_id, qr_code.admin_url_id),
else: conn

conn
|> redirect(to: Routes.qr_code_path(conn, :download, qr_code))

Expand Down
2 changes: 2 additions & 0 deletions lib/qrstorage_web/templates/qr_code/form_link.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
</div>
</div>

<%= radio_button(f, :delete_after_months, 0, class: "visually-hidden", autocomplete: "off", checked: true) %>

<div class="row mb-4">
<div class="col">
<%= submit(gettext("Save"), class: "btn btn-primary") %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
<div class="row m-2">
<div class="col-auto mx-auto">
<h6><%= gettext("delete after") %></h6>
<div class="d-flex justify-content-center">
<div class="btn-group" role="group">
<%= radio_button(@f, :delete_after_months, 1, class: "btn-check", autocomplete: "off") %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_months_1"}>1</label>

<div class="btn-group" role="group">
<%= radio_button(@f, :delete_after, 1,
class: "btn-check",
autocomplete: "off",
checked: true
) %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_1"}>1</label>
<%= radio_button(@f, :delete_after_months, 12, class: "btn-check", autocomplete: "off", checked: true) %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_months_12"}>12</label>

<%= radio_button(@f, :delete_after, 6, class: "btn-check", autocomplete: "off") %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_6"}>6</label>

<%= radio_button(@f, :delete_after, 12, class: "btn-check", autocomplete: "off") %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_12"}>12</label>

<%= radio_button(@f, :delete_after, 0, class: "btn-check", autocomplete: "off") %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_0"}>&infin;</label>
<%= radio_button(@f, :delete_after_months, 36, class: "btn-check", autocomplete: "off") %>
<label class="btn btn-secondary" for={@f.id <> "_delete_after_months_36"}>36</label>
</div>
</div>
</div>
</div>
13 changes: 4 additions & 9 deletions lib/qrstorage_web/templates/qr_code/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,15 @@
<% end %>

<div class="row mt-2">
<div class="col-6 col-md-4">
<strong><%= gettext("Deletion Date") %></strong> <br />
<%= show_delete_after_text(@qr_code) %>
</div>
<%= if @qr_code.content_type == :audio do %>
<div class="col-6 col-md-4">
<strong><%= gettext("Deletion Date") %></strong> <br />
<%= show_delete_after_text(@qr_code.delete_after) %>
</div>
<div class="col-6">
<strong><%= gettext("Audio Language") %></strong> <br />
<%= Gettext.dgettext(QrstorageWeb.Gettext, "languages", Atom.to_string(@qr_code.language)) %>
</div>
<% else %>
<div class="col">
<strong><%= gettext("Deletion Date") %></strong> <br />
<%= show_delete_after_text(@qr_code.delete_after) %>
</div>
<% end %>
</div>

Expand Down
12 changes: 6 additions & 6 deletions lib/qrstorage_web/views/qr_code_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ defmodule QrstorageWeb.QrCodeView do
end
end

def show_delete_after_text(delete_after) do
if delete_after.year == QrCode.max_delete_after_year() do
gettext("This qr code will be stored indefinitely.")
else
Timex.format!(delete_after, "{relative}", :relative)
end
def show_delete_after_text(qr_code) do
Timex.format!(deletion_date(qr_code), "{relative}", :relative)
end

def deletion_date(qr_code) do
Timex.shift(qr_code.last_accessed_at, months: qr_code.delete_after_months)
end

def dots_type_checked?(dots_type, changeset) do
Expand Down
17 changes: 17 additions & 0 deletions priv/repo/migrations/20240316123335_add_delete_after_months.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Qrstorage.Repo.Migrations.AddDeleteAfterMonths do
alias Qrstorage.QrCodes.QrCodeMigrationHelper
use Ecto.Migration

def change do
alter table(:qrcodes) do
add :delete_after_months, :integer, default: 1
end

execute(&execute_up/0, &execute_down/0)
end

defp execute_up,
do: QrCodeMigrationHelper.add_delete_after_months(repo())

defp execute_down, do: nil
end
45 changes: 45 additions & 0 deletions test/qrstorage/qr_codes/qr_code_migration_helper_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Qrstorage.QrCodes.QrCodeMigrationHelperTest do
use Qrstorage.DataCase

alias Qrstorage.QrCodes.QrCodeMigrationHelper
alias Qrstorage.QrCodes.QrCode

describe "add_deleter_after_months/1" do
test "calculates the delete after duration for qr_codes based on insert_at and delete_after date" do
Enum.each([1, 6, 12], fn month ->
qr_code = qr_code_fixture()

# we need to manually tore the old delete_after date. the delete_after date can no longer be set, since we are deprecating it:application
updateDeleteAfterDate(qr_code, Timex.shift(Timex.now(), months: month))
QrCodeMigrationHelper.add_delete_after_months(Qrstorage.Repo)

qr_code = QrCodes.get_qr_code!(qr_code.id)
assert qr_code.delete_after_months == month
end)
end

test "calculates the delete after duration for qr_codes based on insert_at and delete_after date for link codes" do
qr_code = qr_code_fixture()
updateDeleteAfterDate(qr_code, Timex.shift(Timex.now(), hours: 1))
QrCodeMigrationHelper.add_delete_after_months(Qrstorage.Repo)

qr_code = QrCodes.get_qr_code!(qr_code.id)
assert qr_code.delete_after_months == 0
end

test "calculates the delete after duration for qr_codes based on insert_at and delete_after date for infinity codes" do
qr_code = qr_code_fixture()
updateDeleteAfterDate(qr_code, Timex.end_of_year(QrCode.max_delete_after_year()))
QrCodeMigrationHelper.add_delete_after_months(Qrstorage.Repo)

qr_code = QrCodes.get_qr_code!(qr_code.id)
assert qr_code.delete_after_months == 36
end
end

defp updateDeleteAfterDate(qr_code, date) do
qr_code
|> cast(%{delete_after: date}, [:delete_after])
|> Repo.update!()
end
end
Loading

0 comments on commit 7218ba3

Please sign in to comment.