diff --git a/web-app/django/VIM/apps/instruments/admin.py b/web-app/django/VIM/apps/instruments/admin.py index fa511e3c..64deea12 100644 --- a/web-app/django/VIM/apps/instruments/admin.py +++ b/web-app/django/VIM/apps/instruments/admin.py @@ -8,7 +8,7 @@ @admin.register(InstrumentName) class InstrumentNameAdmin(admin.ModelAdmin): - list_filter = ("verification_status", "on_wikidata") # Filter by status + list_filter = ("verification_status", "on_wikidata", "deleted") # Filter by status search_fields = ( "name", "source_name", @@ -25,8 +25,8 @@ def get_readonly_fields(self, request, obj=None): "language", "name", "source_name", - "umil_label", "contributor", "on_wikidata", + "deleted", ) return super().get_readonly_fields(request, obj) diff --git a/web-app/django/VIM/apps/instruments/migrations/0011_remove_instrumentname_unique_umil_label_per_instrument_language_and_more.py b/web-app/django/VIM/apps/instruments/migrations/0011_remove_instrumentname_unique_umil_label_per_instrument_language_and_more.py new file mode 100644 index 00000000..f309d78f --- /dev/null +++ b/web-app/django/VIM/apps/instruments/migrations/0011_remove_instrumentname_unique_umil_label_per_instrument_language_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.5 on 2025-08-20 15:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("instruments", "0010_alter_instrumentname_contributor"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="instrumentname", + name="unique_umil_label_per_instrument_language", + ), + migrations.AddField( + model_name="instrumentname", + name="deleted", + field=models.BooleanField( + default=False, + help_text="Soft delete flag. If true, this name is considered deleted but retained in the database.", + ), + ), + migrations.AddConstraint( + model_name="instrumentname", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("umil_label", True), _negated=True), + models.Q(("verification_status", "verified"), ("deleted", False)), + _connector="OR", + ), + name="umil_label=true only if name is Verified and not Deleted", + ), + ), + ] diff --git a/web-app/django/VIM/apps/instruments/models/instrument_name.py b/web-app/django/VIM/apps/instruments/models/instrument_name.py index b31af750..a2a0c7d4 100644 --- a/web-app/django/VIM/apps/instruments/models/instrument_name.py +++ b/web-app/django/VIM/apps/instruments/models/instrument_name.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Q class InstrumentName(models.Model): @@ -34,17 +35,80 @@ class InstrumentName(models.Model): default=False, help_text="Is this name already on Wikidata?", ) + deleted = models.BooleanField( + default=False, + help_text="Soft delete flag. If true, this name is considered deleted but retained in the database.", + ) - # Custom validation to ensure at most one UMIL label per instrument language + # Constrain umil_label to be true only if the name is verified and not deleted class Meta: constraints = [ - models.UniqueConstraint( - fields=["instrument", "language"], - condition=models.Q(umil_label=True), - name="unique_umil_label_per_instrument_language", - ) + models.CheckConstraint( + name="umil_label=true only if name is Verified and not Deleted", + check=~Q(umil_label=True) + | (Q(verification_status="verified") & Q(deleted=False)), + ), ] # TODO: add verified_by field to track who verified the name def __str__(self): return f"{self.name} ({self.language.en_label}) - {self.instrument.wikidata_id}" + + def save(self, *args, **kwargs): + + existing_umil_label_qs = InstrumentName.objects.filter( + instrument=self.instrument, + language=self.language, + umil_label=True, + ).exclude(id=self.id) + + existing_umil_label = existing_umil_label_qs.first() + print("Existing UMIL label:", existing_umil_label) + + replacement_umil_label = ( + InstrumentName.objects.filter( + instrument=self.instrument, + language=self.language, + verification_status="verified", + deleted=False, + ) + .exclude(id=self.id) + .order_by("id") + .first() + ) + print("Replacement UMIL label:", replacement_umil_label) + + # If setting umil_label=True + if self.umil_label: + if self.deleted: + # Assign replacement if possible, else just unset + self.umil_label = False + if replacement_umil_label: + replacement_umil_label.umil_label = True + replacement_umil_label.save() + + return super().save(*args, **kwargs) + else: + # Unset other umil_labels in one query + existing_umil_label_qs.update(umil_label=False) + + # If a verified name is removing itself as umil_label + if not self.umil_label: + # check if there is an existing label + if existing_umil_label: + pass + # If the existing label is not found, check for a replacement + elif replacement_umil_label: + print("Replacement label found.", flush=True) + replacement_umil_label.umil_label = True + replacement_umil_label.save() + # If there is no replacement umil_label, only set umil_label=False if the current name is also deleted + else: + # Otherwise, a viewer will see a verified name on the detail page without a label + if self.verification_status == "verified" and not self.deleted: + # raise ValueError( + # "This is the only verified, non-deleted name for this instrument and language, it must be set as umil_label." + # ) + self.umil_label = True # Comment this out and uncomment the above after initial instrument upload. + + super().save(*args, **kwargs) diff --git a/web-app/django/VIM/apps/instruments/views/instrument_detail.py b/web-app/django/VIM/apps/instruments/views/instrument_detail.py index edc674ba..69170368 100644 --- a/web-app/django/VIM/apps/instruments/views/instrument_detail.py +++ b/web-app/django/VIM/apps/instruments/views/instrument_detail.py @@ -15,10 +15,13 @@ class InstrumentDetail(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Query the instrument names in all languages - instrument_names = context["instrument"].instrumentname_set.select_related( - "language" + # Query the instrument names in all languages, excluding deleted names + instrument_names = ( + context["instrument"] + .instrumentname_set.select_related("language") + .filter(deleted=False) ) + if self.request.user.is_authenticated: # Show all names for authenticated users context["instrument_names"] = instrument_names.all() @@ -37,15 +40,12 @@ def get_context_data(self, **kwargs): ) # Get the instrument label in the active language - # Set label to the first instrument name added in the language if there is no "umil_label" set active_labels = context["instrument_names"].filter( language=context["active_language"] ) - umil_label = active_labels.filter(umil_label=True).first() - if umil_label: - context["active_instrument_label"] = umil_label - else: - context["active_instrument_label"] = active_labels.first() + context["active_instrument_label"] = active_labels.filter( + umil_label=True + ).first() # Get all languages for the dropdown context["languages"] = Language.objects.all() diff --git a/web-app/django/VIM/apps/instruments/views/update_umil_db.py b/web-app/django/VIM/apps/instruments/views/update_umil_db.py index 1a2ed493..005112df 100644 --- a/web-app/django/VIM/apps/instruments/views/update_umil_db.py +++ b/web-app/django/VIM/apps/instruments/views/update_umil_db.py @@ -51,10 +51,6 @@ def add_name(request: HttpRequest) -> JsonResponse: # create dictionary to map language codes to Language objects language = {lang.wikidata_code: lang for lang in Language.objects.all()} - # considering entries with multiple of the same language, create a dictionary to track if a label has - # been assigned to a previous entry - entry_labels = {entry["language"]: False for entry in entries} - instrument_names_to_create = [] for entry in entries: @@ -75,19 +71,6 @@ def add_name(request: HttpRequest) -> JsonResponse: # Find language object from language code dictionary language_obj: Language = language.get(language_code) - # Within the entries, check if the language already has a name - # if it does, set umil_label to False - # otherwise, check against the UMILdb - if entry_labels[language_code]: - umil_label = False - else: - umil_label: bool = not ( - instrument.instrumentname_set.filter( - language__wikidata_code=language_code - ).exists() - ) - entry_labels[language_code] = True # Mark that this language now has a name - # Prepare the InstrumentName object instrument_names_to_create.append( InstrumentName( @@ -95,7 +78,6 @@ def add_name(request: HttpRequest) -> JsonResponse: language=language_obj, name=name, source_name=source, - umil_label=umil_label, contributor=request.user, ) ) @@ -133,7 +115,9 @@ def delete_name(request: HttpRequest) -> JsonResponse: # If user is a superuser or created the name, allow deletion if request.user.is_superuser or instrument_name.contributor == request.user: - instrument_name.delete() + instrument_name.deleted = True # Soft delete the name + instrument_name.save() # Don't forget to save the changes + return JsonResponse( { "status": "success", diff --git a/web-app/django/VIM/templates/instruments/detail.html b/web-app/django/VIM/templates/instruments/detail.html index b1ba2aaf..32b9c322 100644 --- a/web-app/django/VIM/templates/instruments/detail.html +++ b/web-app/django/VIM/templates/instruments/detail.html @@ -23,7 +23,13 @@
- The following will be deleted from the UMIL database. The entry will persist on Wikidata. + The following will be deleted from UMIL. The entry will persist on Wikidata.