Permette agli admin di prolungare la scadenza dei replay.

Aggiunge data personalizzata e pulsanti rapidi nell'archivio admin così il job di purge non elimina i video fino alla nuova scadenza.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 09:11:56 +02:00
parent 89854869c7
commit dc309fbeb0
6 changed files with 230 additions and 1 deletions

View File

@@ -13,7 +13,12 @@ module Recordings
end end
def update def update
permitted = params.require(:recording).permit(:privacy_status, :title) if admin_expiry_update_requested?
apply_admin_expiry_update!
return
end
permitted = recording_update_params
attrs = {} attrs = {}
privacy = permitted[:privacy_status] privacy = permitted[:privacy_status]
attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES) attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES)
@@ -25,6 +30,8 @@ module Recordings
redirect_to archive_index_path, notice: "Replay aggiornato" redirect_to archive_index_path, notice: "Replay aggiornato"
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
redirect_to archive_index_path, alert: e.record.errors.full_messages.join(", ") redirect_to archive_index_path, alert: e.record.errors.full_messages.join(", ")
rescue Recordings::ExtendExpiry::Error => e
redirect_to archive_index_path, alert: e.message
end end
def destroy def destroy
@@ -87,5 +94,30 @@ module Recordings
def archive_namespace def archive_namespace
raise NotImplementedError raise NotImplementedError
end end
def recording_update_params
params.require(:recording).permit(:privacy_status, :title, :expires_at, :extend_days)
end
def admin_expiry_update_requested?
return false unless archive_namespace == :admin
permitted = recording_update_params
permitted[:extend_days].present? || permitted[:expires_at].present?
end
def apply_admin_expiry_update!
permitted = recording_update_params
extender = Recordings::ExtendExpiry.new(@recording)
if permitted[:extend_days].present?
extender.by_days(permitted[:extend_days])
redirect_to archive_index_path, notice: "Scadenza replay prolungata di #{permitted[:extend_days].to_i} giorni"
else
expires_at = Time.zone.parse(permitted[:expires_at].to_s)&.end_of_day
extender.to_time(expires_at)
redirect_to archive_index_path, notice: "Scadenza replay aggiornata"
end
end
end end
end end

View File

@@ -0,0 +1,39 @@
module Recordings
class ExtendExpiry
class Error < StandardError; end
def initialize(recording)
@recording = recording
end
def to_time(expires_at)
raise Error, "Data scadenza obbligatoria" if expires_at.blank?
parsed = expires_at.is_a?(Time) || expires_at.is_a?(ActiveSupport::TimeWithZone) ? expires_at : Time.zone.parse(expires_at.to_s)
raise Error, "Data non valida" unless parsed
apply(parsed)
end
def by_days(days)
count = days.to_i
raise Error, "Giorni non validi" if count <= 0
base = [@recording.expires_at, Time.current].compact.max
apply(base + count.days)
end
private
def apply(new_expires_at)
raise Error, "Replay già eliminato" if @recording.deleted?
raise Error, "La scadenza deve essere nel futuro" unless new_expires_at > Time.current
@recording.update!(
expires_at: new_expires_at,
expiry_warning_sent_at: nil
)
@recording
end
end
end

View File

@@ -112,6 +112,26 @@
Nessuna scadenza Nessuna scadenza
<% end %> <% end %>
</span> </span>
<% if local_assigns[:admin_mode] && !rec.deleted? && rec.expires_at.present? %>
<%= form_with url: paths.update.call(rec), method: :patch, local: true, class: "replay-archive__expiry-form" do %>
<% filter_params.each { |key, value| concat hidden_field_tag(key, value) } %>
<%= date_field_tag "recording[expires_at]",
rec.expires_at.to_date,
min: Date.tomorrow,
class: "replay-archive__expiry-input",
title: "Nuova data di scadenza" %>
<%= submit_tag "Salva", class: "replay-archive__expiry-save" %>
<% end %>
<div class="replay-archive__expiry-quick">
<% [[30, "+30 gg"], [90, "+90 gg"], [180, "+180 gg"]].each do |days, label| %>
<%= button_to label, paths.update.call(rec),
method: :patch,
params: filter_params.merge(recording: { extend_days: days }),
class: "replay-archive__expiry-extend",
title: "Prolunga la scadenza di #{days} giorni" %>
<% end %>
</div>
<% end %>
</td> </td>
<td class="replay-archive-table__col-status"> <td class="replay-archive-table__col-status">
<span class="replay-archive__status replay-archive__status--<%= rec.status %>"><%= rec.status_label %></span> <span class="replay-archive__status replay-archive__status--<%= rec.status %>"><%= rec.status_label %></span>

View File

@@ -1593,6 +1593,55 @@ a.replay-archive__thumb:hover {
margin: 0; margin: 0;
} }
.replay-archive__expiry-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: 8px;
}
.replay-archive__expiry-input {
font-size: 0.8rem;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid #444;
background: #1a1a1a;
color: #eee;
}
.replay-archive__expiry-save {
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #555;
background: #2a2a2a;
color: #ddd;
cursor: pointer;
}
.replay-archive__expiry-quick {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.replay-archive__expiry-extend {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #444;
background: #222;
color: #aaa;
cursor: pointer;
}
.replay-archive__expiry-extend:hover {
color: #fff;
border-color: #666;
}
.visually-hidden { .visually-hidden {
position: absolute; position: absolute;
width: 1px; width: 1px;

View File

@@ -47,4 +47,29 @@ RSpec.describe "Admin club replays", type: :request do
expect(recording.reload.status).to eq("expired") expect(recording.reload.status).to eq("expired")
expect(recording.deleted_at).to be_present expect(recording.deleted_at).to be_present
end end
it "prolunga la scadenza con giorni aggiuntivi" do
original = recording.expires_at
recording.update!(expires_at: 1.hour.ago, expiry_warning_sent_at: Time.current)
patch admin_club_recording_path(club, recording), params: { recording: { extend_days: 30 } }
expect(response).to redirect_to(admin_club_recordings_path(club))
recording.reload
expect(recording.expires_at).to be > Time.current
expect(recording.expires_at).to be_within(2.seconds).of(30.days.from_now)
expect(recording.expiry_warning_sent_at).to be_nil
expect(Recording.expired_pending_purge).not_to include(recording)
end
it "imposta una nuova data di scadenza" do
new_date = 60.days.from_now.to_date
patch admin_club_recording_path(club, recording), params: { recording: { expires_at: new_date } }
expect(response).to redirect_to(admin_club_recordings_path(club))
recording.reload
expect(recording.expires_at.to_date).to eq(new_date)
expect(recording.expiry_warning_sent_at).to be_nil
end
end end

View File

@@ -0,0 +1,64 @@
require "rails_helper"
RSpec.describe Recordings::ExtendExpiry do
let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let!(:team) { club.teams.create!(name: "Tigers", sport: "volleyball") }
let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") }
let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.ago) }
let!(:session) do
StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago)
end
let!(:recording) do
Recording.create!(
stream_session: session, team: team, status: "ready", privacy_status: "public",
storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4",
expires_at: 2.days.from_now,
expiry_warning_sent_at: 1.day.ago
)
end
it "imposta una nuova data di scadenza e resetta l'avviso" do
new_expiry = 45.days.from_now
described_class.new(recording).to_time(new_expiry)
recording.reload
expect(recording.expires_at).to be_within(1.second).of(new_expiry)
expect(recording.expiry_warning_sent_at).to be_nil
end
it "prolunga dalla scadenza corrente" do
original = recording.expires_at
described_class.new(recording).by_days(30)
recording.reload
expect(recording.expires_at).to be_within(1.second).of(original + 30.days)
expect(recording.expiry_warning_sent_at).to be_nil
end
it "prolunga da oggi se la scadenza è già passata" do
recording.update!(expires_at: 2.days.ago)
described_class.new(recording).by_days(10)
recording.reload
expect(recording.expires_at).to be_within(1.second).of(10.days.from_now)
end
it "esclude il replay dalla coda di purge dopo il prolungamento" do
recording.update!(expires_at: 1.hour.ago)
expect(Recording.expired_pending_purge).to include(recording)
described_class.new(recording).by_days(30)
expect(Recording.expired_pending_purge).not_to include(recording.reload)
end
it "rifiuta date nel passato" do
expect do
described_class.new(recording).to_time(1.day.ago)
end.to raise_error(Recordings::ExtendExpiry::Error, /futuro/)
end
end