Skip to content
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
28 changes: 27 additions & 1 deletion web-app/django/VIM/apps/instruments/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from django.contrib import admin
from VIM.apps.instruments.models import Instrument, InstrumentName, Language, AVResource
from VIM.apps.instruments.models import (
Instrument,
InstrumentName,
Language,
AVResource,
HornbostelSachs,
)

admin.site.register(Instrument)
admin.site.register(Language)
Expand Down Expand Up @@ -30,3 +36,23 @@ def get_readonly_fields(self, request, obj=None):
"on_wikidata",
)
return super().get_readonly_fields(request, obj)


@admin.register(HornbostelSachs)
class HornbostelSachsAdmin(admin.ModelAdmin):
list_filter = ("review_status",)
search_fields = (
"instrument__wikidata_id",
"hbs_class",
)

def get_readonly_fields(self, request, obj=None):
"""
For users in the 'reviewer' group, allow only 'review_status', 'hbs_class', and 'is_main' to be editable.
"""
if request.user.groups.filter(name="reviewer").exists():
return (
"instrument",
"contributor",
)
return super().get_readonly_fields(request, obj)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db import transaction
from VIM.apps.instruments.models import Instrument, InstrumentName, Language, AVResource
from VIM.apps.instruments.models import (
Instrument,
InstrumentName,
Language,
AVResource,
HornbostelSachs,
)


class Command(BaseCommand):
Expand Down Expand Up @@ -124,7 +130,6 @@ def create_database_objects(
instrument, _ = Instrument.objects.update_or_create(
wikidata_id=instrument_attrs["wikidata_id"],
defaults={
"hornbostel_sachs_class": instrument_attrs["hornbostel_sachs_class"],
"mimo_class": instrument_attrs["mimo_class"],
},
)
Expand Down Expand Up @@ -179,6 +184,19 @@ def create_database_objects(
},
)

hbs_value = (
instrument_attrs["hornbostel_sachs_class"] or settings.EMPTY_HBS_CATEGORY
)
hbs_obj = None
if hbs_value and hbs_value != settings.EMPTY_HBS_CATEGORY:
hbs_obj = HornbostelSachs.objects.create(
instrument=instrument,
hbs_class=hbs_value,
is_main=True,
review_status="verified",
contributor=self.default_contributor,
)
instrument.hornbostel_sachs_class = hbs_obj
img_obj = AVResource.objects.create(
instrument=instrument,
type="image",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def handle(self, *args, **options):
Instrument.objects.annotate(
sid=Concat(V("instrument-"), "id", output_field=CharField()),
wikidata_id_s=F("wikidata_id"),
hornbostel_sachs_class_s=F("hornbostel_sachs_class"),
hbs_prim_cat_s=Left(F("hornbostel_sachs_class"), 1),
hornbostel_sachs_class_s=F("hornbostel_sachs_class__hbs_class"),
hbs_prim_cat_s=Left(F("hornbostel_sachs_class__hbs_class"), 1),
mimo_class_s=F("mimo_class"),
type=V("instrument"),
thumbnail_url=F("thumbnail__url"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Generated by Django 4.2.5 on 2026-01-29 23:13

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("instruments", "0011_language_html_direction"),
]

operations = [
migrations.CreateModel(
name="HornbostelSachs",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"hbs_class",
models.CharField(
help_text="Hornbostel-Sachs classification",
max_length=50,
null=True,
),
),
(
"is_main",
models.BooleanField(
default=False,
help_text="Is this the main HBS classification for this instrument?",
),
),
(
"review_status",
models.CharField(
choices=[
("verified", "Verified"),
("unverified", "Unverified"),
("under_review", "Under Review"),
("needs_additional_review", "Needs Additional Review"),
("rejected", "Rejected"),
],
default="unverified",
max_length=50,
),
),
(
"contributor",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"instrument",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hbs_entries",
to="instruments.instrument",
),
),
],
),
migrations.AlterField(
model_name="instrument",
name="hornbostel_sachs_class",
field=models.ForeignKey(
blank=True,
help_text="Currently selected Hornbostel–Sachs classification",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="main_for",
to="instruments.hornbostelsachs",
),
),
]
1 change: 1 addition & 0 deletions web-app/django/VIM/apps/instruments/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from VIM.apps.instruments.models.instrument_name import InstrumentName
from VIM.apps.instruments.models.language import Language
from VIM.apps.instruments.models.avresource import AVResource
from VIM.apps.instruments.models.hornbostel_sachs import HornbostelSachs
57 changes: 57 additions & 0 deletions web-app/django/VIM/apps/instruments/models/hornbostel_sachs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.db import models


class HornbostelSachs(models.Model):
instrument = models.ForeignKey(
"Instrument",
on_delete=models.CASCADE,
related_name="hbs_entries",
)

hbs_class = models.CharField(
max_length=50, null=True, help_text="Hornbostel-Sachs classification"
)

is_main = models.BooleanField(
default=False,
help_text="Is this the main HBS classification for this instrument?",
)

review_status = models.CharField(
max_length=50,
choices=[
("verified", "Verified"),
("unverified", "Unverified"),
("under_review", "Under Review"),
("needs_additional_review", "Needs Additional Review"),
("rejected", "Rejected"),
],
default="unverified",
)

contributor = models.ForeignKey(
"auth.User",
null=True,
blank=True,
on_delete=models.SET_NULL,
)

# TODO: add verified_by field to track who verified the name

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_main:
Instrument = self._meta.get_field("instrument").related_model
instrument = self.instrument
if instrument.hornbostel_sachs_class_id != self.id:
instrument.hornbostel_sachs_class = self
instrument.save(update_fields=["hornbostel_sachs_class"])

# If there is another HBS object set as main for this instrument, unset others
other_mains = (
type(self)
.objects.filter(instrument=self.instrument, is_main=True)
.exclude(pk=self.pk)
)
if other_mains.exists():
other_mains.update(is_main=False)
9 changes: 7 additions & 2 deletions web-app/django/VIM/apps/instruments/models/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ class Instrument(models.Model):
null=True,
related_name="thumbnail_of",
)
hornbostel_sachs_class = models.CharField(
max_length=50, blank=True, help_text="Hornbostel-Sachs classification"
hornbostel_sachs_class = models.ForeignKey(
"HornbostelSachs",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="main_for",
help_text="Currently selected Hornbostel–Sachs classification",
)
mimo_class = models.CharField(
max_length=50,
Expand Down
33 changes: 30 additions & 3 deletions web-app/django/VIM/apps/instruments/views/instrument_detail.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.views.generic import DetailView
from VIM.apps.instruments.models import Instrument, Language
from VIM.apps.instruments.models import Instrument, Language, HornbostelSachs


class InstrumentDetail(DetailView):
Expand All @@ -13,10 +13,11 @@ class InstrumentDetail(DetailView):

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
instrument = context["instrument"]

# Query the instrument names in all languages
instrument_names = (
context["instrument"].instrumentname_set.all().select_related("language")
instrument_names = instrument.instrumentname_set.all().select_related(
"language"
)
if self.request.user.is_authenticated:
# Show all names for authenticated users
Expand Down Expand Up @@ -61,4 +62,30 @@ def get_context_data(self, **kwargs):

context["active_tab"] = "instruments"

# Add user HBS to the context, if present
user_hbs = None
user = self.request.user
if user.is_authenticated:
user_hbs_qs = HornbostelSachs.objects.filter(
instrument=instrument, contributor=user
).order_by(
"-is_main", "-id"
) # prioritize main if more than one, fallback to latest
if user_hbs_qs.exists():
user_hbs = user_hbs_qs.first()
context["user_hbs"] = user_hbs

# Add HBS proposals for this instrument to the context, if instrument has no HBS
if not instrument.hornbostel_sachs_class:
hbs_proposals_qs = (
HornbostelSachs.objects.filter(instrument=instrument, is_main=False)
.order_by("-id")
.values_list("hbs_class", flat=True)
)
# Deduplicate and sort
hbs_proposals = sorted(set(hbs_proposals_qs))
context["hbs_proposals"] = hbs_proposals
else:
context["hbs_proposals"] = None

return context
Loading