1+ import structlog
12from django .conf import settings
2- from django .contrib import admin
3- from django .core .files . storage import default_storage
3+ from django .contrib import admin , messages
4+ from django .core .exceptions import ValidationError
45from django .db import transaction
56from django .db .models import F , Count , OuterRef , Window
67from django .db .models .functions import RowNumber
4344from .utils import generate_release_report_filename
4445
4546
47+ logger = structlog .get_logger ()
48+
49+
4650@admin .register (Commit )
4751class CommitAdmin (admin .ModelAdmin ):
4852 list_display = ["library_version" , "sha" , "author" ]
@@ -184,16 +188,39 @@ def get_context_data(self, **kwargs):
184188 return context
185189
186190 def generate_report (self ):
187- base_scheme = "http" if settings .LOCAL_DEVELOPMENT else "https "
191+ uri = f" { settings .ACCOUNT_DEFAULT_HTTP_PROTOCOL } :// { self . request . get_host () } "
188192 generate_release_report .delay (
189193 user_id = self .request .user .id ,
190194 params = self .request .GET ,
191- base_uri = f" { base_scheme } :// { self . request . get_host () } " ,
195+ base_uri = uri ,
192196 )
193197
198+ def locked_publish_check (self ):
199+ form = self .get_form ()
200+ form .is_valid ()
201+ publish = form .cleaned_data ["publish" ]
202+ report_configuration = form .cleaned_data ["report_configuration" ]
203+ if publish and ReleaseReport .latest_published_locked (report_configuration ):
204+ msg = (
205+ f"A release report already exists with locked status for "
206+ f"{ report_configuration .display_name } . Delete or unlock the most "
207+ f"recent report."
208+ )
209+ raise ValueError (msg )
210+
194211 def get (self , request , * args , ** kwargs ):
195212 form = self .get_form ()
196213 if form .is_valid ():
214+ try :
215+ self .locked_publish_check ()
216+ except ValueError as e :
217+ messages .error (request , str (e ))
218+ return TemplateResponse (
219+ request ,
220+ self .form_template ,
221+ self .get_context_data (),
222+ )
223+
197224 if form .cleaned_data ["no_cache" ]:
198225 params = request .GET .copy ()
199226 form .cache_clear ()
@@ -462,28 +489,92 @@ class Meta:
462489 def __init__ (self , * args , ** kwargs ):
463490 super ().__init__ (* args , ** kwargs )
464491
465- if self .instance .pk and not self .instance .published :
466- file_name = generate_release_report_filename (
467- self .instance .report_configuration .get_slug ()
492+ if not self .is_publish_editable ():
493+ # we require users to intentionally manually delete existing reports
494+ self .fields ["published" ].disabled = True
495+ self .fields ["published" ].help_text = (
496+ "⚠️ A published PDF already exists for this Report Configuration. See "
497+ '"Publishing" notes at the top of this page.'
468498 )
469- published_filename = f"{ ReleaseReport .upload_dir } { file_name } "
470- if default_storage .exists (published_filename ):
471- # we require users to intentionally manually delete existing reports
472- self .fields ["published" ].disabled = True
473- self .fields ["published" ].help_text = (
474- f"⚠️ A published '{ file_name } ' already exists. To prevent accidents "
475- "you must manually delete that file before publishing this report."
499+
500+ def is_publish_editable (self ) -> bool :
501+ # in play here are currently published and previously published rows because of
502+ # filename collision risk.
503+ if self .instance .published :
504+ return True
505+
506+ published_filename = generate_release_report_filename (
507+ version_slug = self .instance .report_configuration .get_slug (),
508+ published_format = True ,
509+ )
510+ reports = ReleaseReport .objects .filter (
511+ report_configuration = self .instance .report_configuration ,
512+ file = f"{ ReleaseReport .upload_dir } { published_filename } " ,
513+ )
514+
515+ if reports .count () == 0 or reports .latest ("created_at" ) == self .instance :
516+ return True
517+
518+ return False
519+
520+ def clean (self ):
521+ cleaned_data = super ().clean ()
522+ if not self .is_publish_editable ():
523+ raise ValidationError ("This file is not publishable." )
524+ if cleaned_data .get ("published" ):
525+ report_configuration = cleaned_data .get ("report_configuration" )
526+ if ReleaseReport .latest_published_locked (
527+ report_configuration , self .instance
528+ ):
529+ raise ValidationError (
530+ f"A release report already exists with locked status for "
531+ f"{ report_configuration .display_name } . Delete or unlock the most "
532+ f"recent report."
476533 )
477534
535+ return cleaned_data
536+
478537
479538@admin .register (ReleaseReport )
480539class ReleaseReportAdmin (admin .ModelAdmin ):
481540 form = ReleaseReportAdminForm
482- list_display = ["__str__" , "created_at" , "published" , "published_at" ]
483- list_filter = ["published" , ReportConfigurationFilter , StaffUserCreatedByFilter ]
541+ list_display = ["__str__" , "created_at" , "published" , "published_at" , "locked" ]
542+ list_filter = [
543+ "published" ,
544+ "locked" ,
545+ ReportConfigurationFilter ,
546+ StaffUserCreatedByFilter ,
547+ ]
484548 search_fields = ["file" ]
485549 readonly_fields = ["created_at" , "created_by" ]
486550 ordering = ["-created_at" ]
551+ change_list_template = "admin/releasereport_change_list.html"
552+ change_form_template = "admin/releasereport_change_form.html"
553+
554+ def get_urls (self ):
555+ urls = super ().get_urls ()
556+ my_urls = [
557+ path (
558+ "release_tasks/" ,
559+ self .admin_site .admin_view (self .release_tasks ),
560+ name = "release_tasks" ,
561+ ),
562+ ]
563+ return my_urls + urls
564+
565+ def release_tasks (self , request ):
566+ from libraries .tasks import release_tasks
567+
568+ release_tasks .delay (
569+ base_uri = f"{ settings .ACCOUNT_DEFAULT_HTTP_PROTOCOL } ://{ request .get_host ()} " ,
570+ user_id = request .user .id ,
571+ generate_report = False ,
572+ )
573+ self .message_user (
574+ request ,
575+ "release_tasks has started, you will receive an email when the task finishes." , # noqa: E501
576+ )
577+ return HttpResponseRedirect ("../" )
487578
488579 def has_add_permission (self , request ):
489580 return False
@@ -492,3 +583,25 @@ def save_model(self, request, obj, form, change):
492583 if not change :
493584 obj .created_by = request .user
494585 super ().save_model (request , obj , form , change )
586+
587+ @staticmethod
588+ def clear_other_report_files (release_report : ReleaseReport ):
589+ if release_report .file :
590+ other_reports = ReleaseReport .objects .filter (
591+ file = release_report .file .name
592+ ).exclude (pk = release_report .pk )
593+
594+ if other_reports .exists ():
595+ release_report .file = None
596+ release_report .save ()
597+
598+ def delete_model (self , request , obj ):
599+ # check if another report uses the same file
600+ self .clear_other_report_files (obj )
601+ super ().delete_model (request , obj )
602+
603+ def delete_queryset (self , request , queryset ):
604+ # clear file reference, prevents deletion of the file if it's linked elsewhere
605+ for obj in queryset :
606+ self .clear_other_report_files (obj )
607+ super ().delete_queryset (request , queryset )
0 commit comments