From fa750b48f854ace276baff594f668d8ae80555d2 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 19 Dec 2024 13:44:35 +0100 Subject: [PATCH 01/14] Add BST category to model and description form --- apiv2/serializers.py | 3 ++- sounds/admin.py | 2 +- sounds/forms.py | 3 +++ sounds/migrations/0053_sound_bst_category.py | 18 ++++++++++++++++++ sounds/models.py | 10 ++++++++++ sounds/views.py | 3 +++ templates/sounds/edit_and_describe.html | 5 +++++ utils/sound_upload.py | 1 + 8 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 sounds/migrations/0053_sound_bst_category.py diff --git a/apiv2/serializers.py b/apiv2/serializers.py index 5dc89baba..31e43e376 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -39,7 +39,7 @@ ################### DEFAULT_FIELDS_IN_SOUND_LIST = 'id,name,tags,username,license' # Separated by commas (None = all) -DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,geotag,created,license,type,channels,filesize,bitrate,' + \ +DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,bst_category,geotag,created,license,type,channels,filesize,bitrate,' + \ 'bitdepth,duration,samplerate,username,pack,pack_name,download,bookmark,previews,images,' + \ 'num_downloads,avg_rating,num_ratings,rate,comments,num_comments,comment,similar_sounds,' + \ 'analysis,analysis_frames,analysis_stats,is_explicit' # All except for analyzers @@ -100,6 +100,7 @@ class Meta: 'name', 'tags', 'description', + 'bst_category', 'geotag', 'created', 'license', diff --git a/sounds/admin.py b/sounds/admin.py index d0e4f7d84..73f8550bc 100644 --- a/sounds/admin.py +++ b/sounds/admin.py @@ -46,7 +46,7 @@ def has_delete_permission(self, request, obj=None): class SoundAdmin(DjangoObjectActions, admin.ModelAdmin): fieldsets = ((None, {'fields': ('user', 'num_downloads' )}), ('Filenames', {'fields': ('base_filename_slug',)}), - ('User defined fields', {'fields': ('description', 'license', 'original_filename', 'sources', 'pack')}), + ('User defined fields', {'fields': ('description', 'license', 'original_filename', 'bst_category', 'sources', 'pack')}), ('File properties', {'fields': ('md5', 'type', 'duration', 'bitrate', 'bitdepth', 'samplerate', 'filesize', 'channels', 'date_recorded')}), ('Moderation', {'fields': ('moderation_state', 'moderation_date', 'has_bad_description', 'is_explicit')}), diff --git a/sounds/forms.py b/sounds/forms.py index 59e1b846e..da0be3dd1 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -245,6 +245,9 @@ class SoundEditAndDescribeForm(forms.Form): file_full_path = None name = forms.CharField(max_length=512, min_length=5, widget=forms.TextInput(attrs={'size': 65, 'class': 'inputText'})) + bst_category = forms.ChoiceField( + choices=Sound.BST_CATEGORY_CHOICES, + ) tags = TagField( widget=forms.Textarea(attrs={'cols': 80, 'rows': 3}), help_text="At least 3 tags, separated by spaces or commas. " diff --git a/sounds/migrations/0053_sound_bst_category.py b/sounds/migrations/0053_sound_bst_category.py new file mode 100644 index 000000000..39cec8ab4 --- /dev/null +++ b/sounds/migrations/0053_sound_bst_category.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-12-19 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sounds', '0052_alter_sound_type'), + ] + + operations = [ + migrations.AddField( + model_name='sound', + name='bst_category', + field=models.CharField(blank=True, choices=[('m', 'Music'), ('is', 'Instrument samples'), ('sp', 'Speech'), ('fx', 'Sound Effects'), ('ss', 'Soundscapes')], default=None, max_length=8, null=True), + ), + ] diff --git a/sounds/models.py b/sounds/models.py index 8284f1460..90a24a334 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -618,6 +618,16 @@ class Sound(models.Model): description = models.TextField() date_recorded = models.DateField(null=True, blank=True, default=None) + # Broad Sound Taxonomy (BST) category + BST_CATEGORY_CHOICES = [ + ('m', 'Music'), + ('is', 'Instrument samples'), + ('sp', 'Speech'), + ('fx', 'Sound Effects'), + ('ss', 'Soundscapes'), + ] + bst_category = models.CharField(max_length=8, null=True, blank=True, default=None, choices=BST_CATEGORY_CHOICES) + # The history of licenses for a sound is stored on SoundLicenseHistory 'license' references the last one license = models.ForeignKey(License, on_delete=models.CASCADE) sources = models.ManyToManyField('self', symmetrical=False, related_name='remixes', blank=True) diff --git a/sounds/views.py b/sounds/views.py index cf5125486..f09214b6b 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -431,6 +431,7 @@ def create_sounds(request, forms): continue # Double check that we are not writing to the wrong file sound_fields = { 'name': form.cleaned_data['name'], + 'bst_category': form.cleaned_data['bst_category'], 'dest_path': file_full_path, 'license': form.cleaned_data['license'], 'description': form.cleaned_data.get('description', ''), @@ -492,6 +493,7 @@ def update_edited_sound(sound, data): sound.set_tags(data["tags"]) sound.description = remove_control_chars(data["description"]) sound.original_filename = data["name"] + sound.bst_category = data["bst_category"] new_license = data["license"] if new_license != sound.license: @@ -619,6 +621,7 @@ def update_edited_sound(sound, data): initial = dict(tags=element.get_sound_tags_string(), description=element.description, name=element.original_filename, + bst_category=element.bst_category, license=element.license, pack=element.pack.id if element.pack else None, lat=element.geotag.lat if element.geotag else None, diff --git a/templates/sounds/edit_and_describe.html b/templates/sounds/edit_and_describe.html index a3aa6b887..6650dd683 100644 --- a/templates/sounds/edit_and_describe.html +++ b/templates/sounds/edit_and_describe.html @@ -71,6 +71,11 @@
Basic information
{{ form.name }}
+ {{ form.bst_category.errors }} + {{ form.bst_category.label_tag }} + {{ form.bst_category }} +
+
{% include "molecules/tags_form_field.html" %}
diff --git a/utils/sound_upload.py b/utils/sound_upload.py index 63e4b080e..db9edabc6 100644 --- a/utils/sound_upload.py +++ b/utils/sound_upload.py @@ -153,6 +153,7 @@ def create_sound(user, sound.user = user sound.original_filename = sound_fields['name'] sound.original_path = sound_fields['dest_path'] + sound.bst_category = sound_fields['bst_category'] try: sound.filesize = os.path.getsize(sound.original_path) except OSError: From e348540a9ea3f1141b3a424cd3ac79308871662b Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 19 Dec 2024 14:52:13 +0100 Subject: [PATCH 02/14] Make tests pass after adding bst category --- accounts/tests/test_upload.py | 3 +++ sounds/forms.py | 1 + sounds/models.py | 2 ++ sounds/tests/test_sound.py | 3 +++ utils/sound_upload.py | 3 ++- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/accounts/tests/test_upload.py b/accounts/tests/test_upload.py index 81490d2d0..d3979eec7 100644 --- a/accounts/tests/test_upload.py +++ b/accounts/tests/test_upload.py @@ -141,6 +141,7 @@ def test_describe_selected_files(self): '0-license': '3', '0-description': 'a test description for the sound file', '0-new_pack': '', + '0-bst_category': 'ss', '0-name': filenames[0], '1-audio_filename': filenames[1], '1-license': '3', @@ -167,6 +168,8 @@ def test_describe_selected_files(self): self.assertEqual(Pack.objects.filter(name='Name of a new pack').exists(), True) self.assertEqual(Tag.objects.filter(name__contains="testtag").count(), 5) self.assertNotEqual(user.sounds.get(original_filename=filenames[0]).geotag, None) + self.assertEqual(user.sounds.get(original_filename=filenames[0]).bst_category, 'ss') + self.assertEqual(user.sounds.get(original_filename=filenames[1]).bst_category, '') sound_with_sources = user.sounds.get(original_filename=filenames[1]) self.assertEqual(sound_with_sources.sources.all().count(), len(sound_sources)) diff --git a/sounds/forms.py b/sounds/forms.py index da0be3dd1..cd23d441b 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -247,6 +247,7 @@ class SoundEditAndDescribeForm(forms.Form): widget=forms.TextInput(attrs={'size': 65, 'class': 'inputText'})) bst_category = forms.ChoiceField( choices=Sound.BST_CATEGORY_CHOICES, + required=False ) tags = TagField( widget=forms.Textarea(attrs={'cols': 80, 'rows': 3}), diff --git a/sounds/models.py b/sounds/models.py index 90a24a334..bb10a06bf 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -432,6 +432,7 @@ def bulk_query_solr(self, sound_ids): sound.id, sound.type, sound.original_filename, + sound.bst_category, sound.is_explicit, sound.filesize, sound.md5, @@ -490,6 +491,7 @@ def bulk_query(self, where, order_by, limit, args, include_analyzers_output=Fals sound.type, sound.user_id, sound.original_filename, + sound.bst_category, sound.is_explicit, sound.avg_rating, sound.channels, diff --git a/sounds/tests/test_sound.py b/sounds/tests/test_sound.py index 5b6b411fa..14c232846 100644 --- a/sounds/tests/test_sound.py +++ b/sounds/tests/test_sound.py @@ -1174,12 +1174,14 @@ def test_update_description(self): new_name = 'New name' new_tags = ['tag1', 'tag2', 'tag3'] new_pack_name = 'Name of a new pack' + new_bst_category = 'ss' new_sound_sources = Sound.objects.exclude(id=self.sound.id) geotag_lat = 46.31658418182218 resp = self.client.post(reverse('sound-edit', args=[self.sound.user.username, self.sound.id]), { '0-sound_id': self.sound.id, '0-description': new_description, '0-name': new_name, + '0-bst_category': new_bst_category, '0-tags': ' '.join(new_tags), '0-license': '3', '0-sources': ','.join([f'{s.id}' for s in new_sound_sources]), @@ -1194,6 +1196,7 @@ def test_update_description(self): self.sound.refresh_from_db() self.assertEqual(self.sound.description, new_description) self.assertEqual(self.sound.original_filename, new_name) + self.assertEqual(self.sound.bst_category, new_bst_category) self.assertListEqual(sorted(self.sound.get_sound_tags()), sorted(new_tags)) self.assertEqual(self.sound.sources.all().count(), len(new_sound_sources)) self.assertTrue(Pack.objects.filter(name='Name of a new pack').exists()) diff --git a/utils/sound_upload.py b/utils/sound_upload.py index db9edabc6..0a7b61cd7 100644 --- a/utils/sound_upload.py +++ b/utils/sound_upload.py @@ -153,7 +153,8 @@ def create_sound(user, sound.user = user sound.original_filename = sound_fields['name'] sound.original_path = sound_fields['dest_path'] - sound.bst_category = sound_fields['bst_category'] + if 'bst_category' in sound_fields: + sound.bst_category = sound_fields['bst_category'] try: sound.filesize = os.path.getsize(sound.original_path) except OSError: From d4326aaaaef28f1ce2f3b5d2844f1f91b8563cd3 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 19 Dec 2024 14:52:48 +0100 Subject: [PATCH 03/14] Add bst category support for API edit and describe --- apiv2/serializers.py | 25 +++++++++++++++++++++++++ apiv2/views.py | 3 +++ 2 files changed, 28 insertions(+) diff --git a/apiv2/serializers.py b/apiv2/serializers.py index 31e43e376..523e2acba 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -632,6 +632,12 @@ def validate_name(value): return value +def validate_bst_category(value): + if value not in [key for key, name in Sound.BST_CATEGORY_CHOICES]: + raise serializers.ValidationError('Invalid BST category, should be a valid Broad Sound Taxonomy code.') + return value + + def validate_tags(value): tags = clean_and_split_tags(value) if len(tags) < 3: @@ -694,6 +700,8 @@ class SoundDescriptionSerializer(serializers.Serializer): name = serializers.CharField(max_length=512, required=False, help_text='Not required. Name you want to give to the sound (by default it will be ' 'the original filename).') + bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES, + help_text='Not required. Must be a valid Broad Sound Taxonomy category code.') tags = serializers.CharField(max_length=512, help_text='Separate tags with spaces. Join multi-word tags with dashes.') description = serializers.CharField(help_text='Textual description of the sound.') @@ -721,6 +729,9 @@ def validate_tags(self, value): def validate_name(self, value): return validate_name(value) + + def validate_bst_category(self, value): + return validate_bst_category(value) def validate_description(self, value): return validate_description(value) @@ -732,6 +743,8 @@ def validate_pack(self, value): class EditSoundDescriptionSerializer(serializers.Serializer): name = serializers.CharField(max_length=512, required=False, help_text='Not required. New name you want to give to the sound.') + bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES, + help_text='Not required. Must be a valid Broad Sound Taxonomy category code.') tags = serializers.CharField(max_length=512, required=False, help_text='Not required. Tags that should be assigned to the sound (note that ' 'existing ones will be deleted). Separate tags with spaces. Join multi-word ' @@ -756,6 +769,9 @@ def validate_tags(self, value): def validate_name(self, value): return validate_name(value) + + def validate_bst_category(self, value): + return validate_bst_category(value) def validate_description(self, value): return validate_description(value) @@ -770,6 +786,8 @@ class UploadAndDescribeAudioFileSerializer(serializers.Serializer): name = serializers.CharField(max_length=512, required=False, help_text='Not required. Name you want to give to the sound (by default it will be ' 'the original filename).') + bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES, + help_text='Not required. Must be a valid Broad Sound Taxonomy category code.') tags = serializers.CharField(max_length=512, required=False, help_text='Only required if providing file description. Separate tags with spaces. ' 'Join multi-word tags with dashes.') @@ -809,15 +827,22 @@ def validate(self, data): data['description'] = validate_description(self.initial_data.get('description', '')) except serializers.ValidationError as e: errors['description'] = e.detail + try: data['name'] = validate_name(self.initial_data.get('name', '')) except serializers.ValidationError as e: errors['name'] = e.detail + try: + data['bst_category'] = validate_bst_category(self.initial_data.get('bst_category', '')) + except serializers.ValidationError as e: + errors['bst_category'] = e.detail + try: data['tags'] = validate_tags(self.initial_data.get('tags', '')) except serializers.ValidationError as e: errors['tags'] = e.detail + try: data['geotag'] = validate_geotag(self.initial_data.get('geotag', '')) except serializers.ValidationError as e: diff --git a/apiv2/views.py b/apiv2/views.py index a682061d0..336019b43 100755 --- a/apiv2/views.py +++ b/apiv2/views.py @@ -928,6 +928,9 @@ def post(self, request, *args, **kwargs): if 'name' in serializer.data: if serializer.data['name']: sound.original_filename = serializer.data['name'] + if 'bst_category' in serializer.data: + if serializer.data['bst_category']: + sound.bst_category = serializer.data['bst_category'] if 'description' in serializer.data: if serializer.data['description']: sound.description = serializer.data['description'] From 85e94c0aff56b82d2ee2748f1894a78083c7a729 Mon Sep 17 00:00:00 2001 From: allholy Date: Mon, 23 Dec 2024 17:12:57 +0100 Subject: [PATCH 04/14] Add all BST categories --- freesound/settings.py | 39 +++++++++++++++++++ .../0054_alter_sound_bst_category.py | 18 +++++++++ sounds/models.py | 8 +--- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 sounds/migrations/0054_alter_sound_bst_category.py diff --git a/freesound/settings.py b/freesound/settings.py index 8bcc8449c..5bc6a9006 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -457,6 +457,45 @@ ANNOUNCEMENT_CACHE_KEY = 'announcement_cache' +# ------------------------------------------------------------------------------- +# Broad Sound Taxonomy definition + +BROAD_SOUND_TAXONOMY = [ + {'category_code': 'm', 'level': 1, 'name': 'Music', 'description': 'Music excerpts, melodies, loops, fillers, drones and short musical snippets.'}, + {'category_code': 'm-sp', 'level': 2, 'name': 'Solo percussion', 'description': 'Music excerpts with solo percussive instruments.'}, + {'category_code': 'm-si', 'level': 2, 'name': 'Solo instrument', 'description': 'Music excerpts with only one instrument, excluding percussion.'}, + {'category_code': 'm-m', 'level': 2, 'name': 'Multiple instruments', 'description': 'Music excerpts with more than one instrument.'}, + {'category_code': 'm-other', 'level': 2, 'name': 'Other (music)', 'description': 'Music that doesn\'t belong to any of the above categories.'}, + {'category_code': 'is', 'level': 1, 'name': 'Instrument samples', 'description': 'Single notes from musical instruments, various versions of the same note, and scales.'}, + {'category_code': 'is-p', 'level': 2, 'name': 'Percussion', 'description': 'Instrument samples that are percussive (idiophones or membraphones).'}, + {'category_code': 'is-s', 'level': 2, 'name': 'String', 'description': 'Instrument samples that belong to the string instrument family.'}, + {'category_code': 'is-w', 'level': 2, 'name': 'Wind', 'description': 'Instrument samples that belong to the wind instrument family (aerophones).'}, + {'category_code': 'is-k', 'level': 2, 'name': 'Piano / Keyboard instruments', 'description': 'Instrument samples of piano or other keyboard instruments, not synthesized.'}, + {'category_code': 'is-e', 'level': 2, 'name': 'Synths / Electronic', 'description': 'Instrument samples synthesized or produced by electronic means.'}, + {'category_code': 'is-other', 'level': 2, 'name': 'Instrument samples (other)', 'description': 'Instrument samples that don\'t belong to any of the above categories.'}, + {'category_code': 'sp', 'level': 1, 'name': 'Speech', 'description': 'Sounds where human voice is prominent.'}, + {'category_code': 'sp-s', 'level': 2, 'name': 'Solo speech', 'description': 'Recording of a single voice speaking, excluding singing.'}, + {'category_code': 'sp-c', 'level': 2, 'name': 'Conversation / Crowd', 'description': 'Several people talking, having a conversation or dialogue.'}, + {'category_code': 'sp-p', 'level': 2, 'name': 'Processed / Synthetic', 'description': 'Voice(s) from an indirect source (e.g. radio), processed or synthesized.'}, + {'category_code': 'sp-other', 'level': 2, 'name': 'Other (speech)', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'}, + {'category_code': 'fx', 'level': 1, 'name': 'Sound effects', 'description': 'Isolated sound effects or sound events, each happening one at a time.'}, + {'category_code': 'fx-o', 'level': 2, 'name': 'Objects / House appliances', 'description': 'Everyday objects, inside the home or smaller in size.'}, + {'category_code': 'fx-v', 'level': 2, 'name': 'Vehicles', 'description': 'Sounds produced from a vehicle.'}, + {'category_code': 'fx-m', 'level': 2, 'name': 'Other mechanisms, engines, machines', 'description': 'Machine-like sounds, except vehicles and small house electric devices.'}, + {'category_code': 'fx-h', 'level': 2, 'name': 'Human sounds and actions', 'description': 'Sounds from the human body, excluding speech.'}, + {'category_code': 'fx-a', 'level': 2, 'name': 'Animals', 'description': 'Animal vocalizations or sounds.'}, + {'category_code': 'fx-n', 'level': 2, 'name': 'Natural elements and explosions', 'description': 'Sound events occuring by natural processes.'}, + {'category_code': 'fx-ex', 'level': 2, 'name': 'Experimental', 'description': 'Experimental sounds or heavily processed audio recordings.'}, + {'category_code': 'fx-el', 'level': 2, 'name': 'Electronic / Design', 'description': 'Sound effects that are computer-made or designed for user interfaces or animations.'}, + {'category_code': 'fx-other', 'level': 2, 'name': 'Other (sound effects)', 'description': 'Sound effects that don\'t belong to any of the above categories.'}, + {'category_code': 'ss', 'level': 1, 'name': 'Soundscapes', 'description': 'Ambiances, field-recordings with multiple events and sound environments.'}, + {'category_code': 'ss-n', 'level': 2, 'name': 'Nature', 'description': 'Soundscapes from natural habitats.'}, + {'category_code': 'ss-i', 'level': 2, 'name': 'Indoors', 'description': 'Soundscapes from closed or indoor spaces.'}, + {'category_code': 'ss-u', 'level': 2, 'name': 'Urban', 'description': 'Soundscapes from cityscapes or outdoor places with human intervention.'}, + {'category_code': 'ss-s', 'level': 2, 'name': 'Synthetic / Artificial', 'description': 'Soundscapes that are synthesized or computer-made ambiences.'}, + {'category_code': 'ss-other', 'level': 2, 'name': 'Other (soundscapes)', 'description': 'Soundscapes that don\'t belong to any of the above categories.'}, +] + # ------------------------------------------------------------------------------- # Freesound data paths and urls diff --git a/sounds/migrations/0054_alter_sound_bst_category.py b/sounds/migrations/0054_alter_sound_bst_category.py new file mode 100644 index 000000000..0988305d3 --- /dev/null +++ b/sounds/migrations/0054_alter_sound_bst_category.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-12-23 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sounds', '0053_sound_bst_category'), + ] + + operations = [ + migrations.AlterField( + model_name='sound', + name='bst_category', + field=models.CharField(blank=True, choices=[('m', 'Music'), ('m-sp', 'Solo percussion'), ('m-si', 'Solo instrument'), ('m-m', 'Multiple instruments'), ('m-other', 'Other (music)'), ('is', 'Instrument samples'), ('is-p', 'Percussion'), ('is-s', 'String'), ('is-w', 'Wind'), ('is-k', 'Piano / Keyboard instruments'), ('is-e', 'Synths / Electronic'), ('is-other', 'Instrument samples (other)'), ('sp', 'Speech'), ('sp-s', 'Solo speech'), ('sp-c', 'Conversation / Crowd'), ('sp-p', 'Processed / Synthetic'), ('sp-other', 'Other (speech)'), ('fx', 'Sound effects'), ('fx-o', 'Objects / House appliances'), ('fx-v', 'Vehicles'), ('fx-m', 'Other mechanisms, engines, machines'), ('fx-h', 'Human sounds and actions'), ('fx-a', 'Animals'), ('fx-n', 'Natural elements and explosions'), ('fx-ex', 'Experimental'), ('fx-el', 'Electronic / Design'), ('fx-other', 'Other (sound effects)'), ('ss', 'Soundscapes'), ('ss-n', 'Nature'), ('ss-i', 'Indoors'), ('ss-u', 'Urban'), ('ss-s', 'Synthetic / Artificial'), ('ss-other', 'Other (soundscapes)')], default=None, max_length=8, null=True), + ), + ] diff --git a/sounds/models.py b/sounds/models.py index bb10a06bf..d95006eb9 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -621,13 +621,7 @@ class Sound(models.Model): date_recorded = models.DateField(null=True, blank=True, default=None) # Broad Sound Taxonomy (BST) category - BST_CATEGORY_CHOICES = [ - ('m', 'Music'), - ('is', 'Instrument samples'), - ('sp', 'Speech'), - ('fx', 'Sound Effects'), - ('ss', 'Soundscapes'), - ] + BST_CATEGORY_CHOICES = [(item['category_code'], item['name']) for item in settings.BROAD_SOUND_TAXONOMY] bst_category = models.CharField(max_length=8, null=True, blank=True, default=None, choices=BST_CATEGORY_CHOICES) # The history of licenses for a sound is stored on SoundLicenseHistory 'license' references the last one From e54a5ff87b8fb5275ab6fa51f9bfbfa767752020 Mon Sep 17 00:00:00 2001 From: allholy Date: Wed, 15 Jan 2025 21:40:54 +0100 Subject: [PATCH 05/14] Implement frontend layout for BST category field --- .../src/pages/editDescribeSounds.js | 4 +- sounds/forms.py | 11 +- sounds/templatetags/bst_category.py | 11 ++ sounds/views.py | 3 +- .../molecules/bst_category_form_field.html | 124 ++++++++++++++++++ templates/sounds/edit_and_describe.html | 4 +- 6 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 sounds/templatetags/bst_category.py create mode 100644 templates/molecules/bst_category_form_field.html diff --git a/freesound/static/bw-frontend/src/pages/editDescribeSounds.js b/freesound/static/bw-frontend/src/pages/editDescribeSounds.js index 747313cf3..94922d16d 100644 --- a/freesound/static/bw-frontend/src/pages/editDescribeSounds.js +++ b/freesound/static/bw-frontend/src/pages/editDescribeSounds.js @@ -28,4 +28,6 @@ inputTypeSubmitElements.forEach(button => { } }); }); -}); \ No newline at end of file +}); + +// Move json for BST category field in description form here diff --git a/sounds/forms.py b/sounds/forms.py index cd23d441b..82a66486e 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -23,7 +23,7 @@ from captcha.fields import ReCaptchaField from django import forms -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Q from django.forms import ModelForm, Textarea, TextInput from django.core.signing import BadSignature, SignatureExpired @@ -247,7 +247,8 @@ class SoundEditAndDescribeForm(forms.Form): widget=forms.TextInput(attrs={'size': 65, 'class': 'inputText'})) bst_category = forms.ChoiceField( choices=Sound.BST_CATEGORY_CHOICES, - required=False + help_text="Choose the most appropriate catgeory and subcatgoery (you can only choose one). This category will be displayed as a filtering option in the Freesound side bar.", + required=True, ) tags = TagField( widget=forms.Textarea(attrs={'cols': 80, 'rows': 3}), @@ -348,6 +349,12 @@ def clean_sources(self): def clean_pack(self): return _pack_form_clean_pack_helper(self.cleaned_data) + + def clean_bst_category(self): + value = self.cleaned_data['bst_category'] + if not '-' in value: + raise ValidationError("Please choose a subcategory.") + return value class SoundCSVDescriptionForm(SoundEditAndDescribeForm): diff --git a/sounds/templatetags/bst_category.py b/sounds/templatetags/bst_category.py new file mode 100644 index 000000000..530ce30a7 --- /dev/null +++ b/sounds/templatetags/bst_category.py @@ -0,0 +1,11 @@ +from django import template + +register = template.Library() + +@register.filter +def get_top_level_bst_category(value): + """ + Extract the top level category from the given value. + The top level category is the part before the '-' in value. + """ + return value.split('-')[0] if '-' in value else value diff --git a/sounds/views.py b/sounds/views.py index f09214b6b..1d56869dc 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -661,7 +661,8 @@ def update_edited_sound(sound, data): 'sounds_per_round': forms_per_round, 'last_latlong': request.user.profile.get_last_latlong(), 'total_sounds_to_describe': len_original_describe_edit_sounds, - 'next': request.GET.get('next', '') + 'next': request.GET.get('next', ''), + 'bst_taxonomy': settings.BROAD_SOUND_TAXONOMY } if request.method == "POST" and all_forms_validated_ok: diff --git a/templates/molecules/bst_category_form_field.html b/templates/molecules/bst_category_form_field.html new file mode 100644 index 000000000..b9ca50975 --- /dev/null +++ b/templates/molecules/bst_category_form_field.html @@ -0,0 +1,124 @@ +{% load bst_category %} + +
+ + {{ form.bst_category.errors }} +
+ {{ form.bst_category.as_hidden }} + +
+ {% for value, label in form.fields.bst_category.choices %} + {% if "-" not in value %} + + {% endif %} + {% endfor %} +
+ + +
+ {{ form.bst_category.help_text|safe }} +
+
+ + diff --git a/templates/sounds/edit_and_describe.html b/templates/sounds/edit_and_describe.html index 6650dd683..f0b11e155 100644 --- a/templates/sounds/edit_and_describe.html +++ b/templates/sounds/edit_and_describe.html @@ -71,9 +71,7 @@
Basic information
{{ form.name }}
- {{ form.bst_category.errors }} - {{ form.bst_category.label_tag }} - {{ form.bst_category }} + {% include "molecules/bst_category_form_field.html" %}
{% include "molecules/tags_form_field.html" %} From 5b3f540720c4ed3deb1083b9980cff38d6d12df7 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 16 Jan 2025 11:50:51 +0100 Subject: [PATCH 06/14] Add ai field to model and description form --- apiv2/serializers.py | 7 ++++++- sounds/admin.py | 2 +- sounds/forms.py | 2 ++ sounds/models.py | 3 +++ sounds/views.py | 2 ++ templates/sounds/edit_and_describe.html | 5 +++++ utils/sound_upload.py | 3 +++ 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apiv2/serializers.py b/apiv2/serializers.py index 523e2acba..578a0fabd 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -42,7 +42,7 @@ DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,bst_category,geotag,created,license,type,channels,filesize,bitrate,' + \ 'bitdepth,duration,samplerate,username,pack,pack_name,download,bookmark,previews,images,' + \ 'num_downloads,avg_rating,num_ratings,rate,comments,num_comments,comment,similar_sounds,' + \ -'analysis,analysis_frames,analysis_stats,is_explicit' # All except for analyzers +'analysis,analysis_frames,analysis_stats,is_explicit,is_gen_ai' # All except for analyzers DEFAULT_FIELDS_IN_PACK_DETAIL = None # Separated by commas (None = all) @@ -132,6 +132,7 @@ class Meta: 'ac_analysis', # Kept for legacy reasons only as it is also contained in 'analyzers_output' 'analyzers_output', 'is_explicit', + 'is_gen_ai', 'score', ) @@ -312,6 +313,10 @@ def get_analyzers_output(self, obj): is_explicit = serializers.SerializerMethodField() def get_is_explicit(self, obj): return obj.is_explicit + + is_gen_ai = serializers.SerializerMethodField() + def get_is_gen_ai(self, obj): + return obj.is_gen_ai class SoundListSerializer(AbstractSoundSerializer): diff --git a/sounds/admin.py b/sounds/admin.py index 73f8550bc..434c05019 100644 --- a/sounds/admin.py +++ b/sounds/admin.py @@ -49,7 +49,7 @@ class SoundAdmin(DjangoObjectActions, admin.ModelAdmin): ('User defined fields', {'fields': ('description', 'license', 'original_filename', 'bst_category', 'sources', 'pack')}), ('File properties', {'fields': ('md5', 'type', 'duration', 'bitrate', 'bitdepth', 'samplerate', 'filesize', 'channels', 'date_recorded')}), - ('Moderation', {'fields': ('moderation_state', 'moderation_date', 'has_bad_description', 'is_explicit')}), + ('Moderation', {'fields': ('moderation_state', 'moderation_date', 'has_bad_description', 'is_explicit', 'is_gen_ai')}), ('Processing', {'fields': ('processing_state', 'processing_date', 'processing_ongoing_state', 'processing_log', 'similarity_state')}), ) raw_id_fields = ('user', 'pack', 'sources') diff --git a/sounds/forms.py b/sounds/forms.py index cd23d441b..4b3a11f8d 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -260,6 +260,7 @@ class SoundEditAndDescribeForm(forms.Form): help_text="You can add timestamps to the description using the syntax #minute:second (e.g. \"#1:07 nice bird chirp\"). " "This will be rendered with a little play button to play the sound at that timestamp. " + HtmlCleaningCharField.make_help_text()) is_explicit = forms.BooleanField(required=False, label="The sound contains explicit content") + is_gen_ai = forms.BooleanField(required=False, label="The sound involves generative AI") license_qs = License.objects.filter(Q(name__istartswith='Attribution') | Q(name__istartswith='Creative')) license = forms.ModelChoiceField(queryset=license_qs, required=True, widget=forms.RadioSelect()) pack = forms.ChoiceField(label="Select a pack for this sound:", choices=[], required=False) @@ -292,6 +293,7 @@ def __init__(self, *args, **kwargs): user_packs = kwargs.pop('user_packs', False) super().__init__(*args, **kwargs) self.fields['is_explicit'].widget.attrs['class'] = 'bw-checkbox' + self.fields['is_gen_ai'].widget.attrs['class'] = 'bw-checkbox' self.fields['remove_geotag'].widget.attrs['class'] = 'bw-checkbox' self.fields['license'].widget.attrs['class'] = 'bw-radio' diff --git a/sounds/models.py b/sounds/models.py index d95006eb9..c779fe662 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -434,6 +434,7 @@ def bulk_query_solr(self, sound_ids): sound.original_filename, sound.bst_category, sound.is_explicit, + sound.is_gen_ai, sound.filesize, sound.md5, sound.channels, @@ -493,6 +494,7 @@ def bulk_query(self, where, order_by, limit, args, include_analyzers_output=Fals sound.original_filename, sound.bst_category, sound.is_explicit, + sound.is_gen_ai, sound.avg_rating, sound.channels, sound.filesize, @@ -659,6 +661,7 @@ class Sound(models.Model): moderation_note = models.TextField(null=True, blank=True, default=None) has_bad_description = models.BooleanField(default=False) is_explicit = models.BooleanField(default=False) + is_gen_ai = models.BooleanField(default=False) # processing PROCESSING_STATE_CHOICES = ( diff --git a/sounds/views.py b/sounds/views.py index f09214b6b..d31396dcb 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -437,6 +437,7 @@ def create_sounds(request, forms): 'description': form.cleaned_data.get('description', ''), 'tags': form.cleaned_data.get('tags', ''), 'is_explicit': form.cleaned_data['is_explicit'], + 'is_gen_ai': form.cleaned_data['is_gen_ai'], } pack = form.cleaned_data.get('pack', False) @@ -490,6 +491,7 @@ def create_sounds(request, forms): def update_edited_sound(sound, data): sound.is_explicit = data["is_explicit"] + sound.is_gen_ai = data["is_gen_ai"] sound.set_tags(data["tags"]) sound.description = remove_control_chars(data["description"]) sound.original_filename = data["name"] diff --git a/templates/sounds/edit_and_describe.html b/templates/sounds/edit_and_describe.html index 6650dd683..07c95b044 100644 --- a/templates/sounds/edit_and_describe.html +++ b/templates/sounds/edit_and_describe.html @@ -89,6 +89,11 @@
Basic information
{{ form.is_explicit.label_tag }} {{ form.is_explicit }}
+
+ {{ form.is_gen_ai.errors }} + {{ form.is_gen_ai.label_tag }} + {{ form.is_gen_ai }} +
diff --git a/utils/sound_upload.py b/utils/sound_upload.py index 0a7b61cd7..9eec92974 100644 --- a/utils/sound_upload.py +++ b/utils/sound_upload.py @@ -254,6 +254,9 @@ def create_sound(user, if 'is_explicit' in sound_fields: sound.is_explicit = sound_fields['is_explicit'] + if 'is_gen_ai' in sound_fields: + sound.is_gen_ai = sound_fields['is_gen_ai'] + # 6.5 set uploaded apiv2 client or bulk progress object (if any) sound.uploaded_with_apiv2_client = apiv2_client sound.uploaded_with_bulk_upload_progress = bulk_upload_progress From 8103cb6942b2a1d0f8f5e6f5ffd3f4c4debda7f8 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 16 Jan 2025 15:19:35 +0100 Subject: [PATCH 07/14] Modify BST category list --- freesound/settings.py | 10 +++++----- sounds/migrations/0054_alter_sound_bst_category.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freesound/settings.py b/freesound/settings.py index 5bc6a9006..6179e949c 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -465,19 +465,19 @@ {'category_code': 'm-sp', 'level': 2, 'name': 'Solo percussion', 'description': 'Music excerpts with solo percussive instruments.'}, {'category_code': 'm-si', 'level': 2, 'name': 'Solo instrument', 'description': 'Music excerpts with only one instrument, excluding percussion.'}, {'category_code': 'm-m', 'level': 2, 'name': 'Multiple instruments', 'description': 'Music excerpts with more than one instrument.'}, - {'category_code': 'm-other', 'level': 2, 'name': 'Other (music)', 'description': 'Music that doesn\'t belong to any of the above categories.'}, + {'category_code': 'm-other', 'level': 2, 'name': 'Other', 'description': 'Music that doesn\'t belong to any of the above categories.'}, {'category_code': 'is', 'level': 1, 'name': 'Instrument samples', 'description': 'Single notes from musical instruments, various versions of the same note, and scales.'}, {'category_code': 'is-p', 'level': 2, 'name': 'Percussion', 'description': 'Instrument samples that are percussive (idiophones or membraphones).'}, {'category_code': 'is-s', 'level': 2, 'name': 'String', 'description': 'Instrument samples that belong to the string instrument family.'}, {'category_code': 'is-w', 'level': 2, 'name': 'Wind', 'description': 'Instrument samples that belong to the wind instrument family (aerophones).'}, {'category_code': 'is-k', 'level': 2, 'name': 'Piano / Keyboard instruments', 'description': 'Instrument samples of piano or other keyboard instruments, not synthesized.'}, {'category_code': 'is-e', 'level': 2, 'name': 'Synths / Electronic', 'description': 'Instrument samples synthesized or produced by electronic means.'}, - {'category_code': 'is-other', 'level': 2, 'name': 'Instrument samples (other)', 'description': 'Instrument samples that don\'t belong to any of the above categories.'}, + {'category_code': 'is-other', 'level': 2, 'name': 'Other', 'description': 'Instrument samples that don\'t belong to any of the above categories.'}, {'category_code': 'sp', 'level': 1, 'name': 'Speech', 'description': 'Sounds where human voice is prominent.'}, {'category_code': 'sp-s', 'level': 2, 'name': 'Solo speech', 'description': 'Recording of a single voice speaking, excluding singing.'}, {'category_code': 'sp-c', 'level': 2, 'name': 'Conversation / Crowd', 'description': 'Several people talking, having a conversation or dialogue.'}, {'category_code': 'sp-p', 'level': 2, 'name': 'Processed / Synthetic', 'description': 'Voice(s) from an indirect source (e.g. radio), processed or synthesized.'}, - {'category_code': 'sp-other', 'level': 2, 'name': 'Other (speech)', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'}, + {'category_code': 'sp-other', 'level': 2, 'name': 'Other', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'}, {'category_code': 'fx', 'level': 1, 'name': 'Sound effects', 'description': 'Isolated sound effects or sound events, each happening one at a time.'}, {'category_code': 'fx-o', 'level': 2, 'name': 'Objects / House appliances', 'description': 'Everyday objects, inside the home or smaller in size.'}, {'category_code': 'fx-v', 'level': 2, 'name': 'Vehicles', 'description': 'Sounds produced from a vehicle.'}, @@ -487,13 +487,13 @@ {'category_code': 'fx-n', 'level': 2, 'name': 'Natural elements and explosions', 'description': 'Sound events occuring by natural processes.'}, {'category_code': 'fx-ex', 'level': 2, 'name': 'Experimental', 'description': 'Experimental sounds or heavily processed audio recordings.'}, {'category_code': 'fx-el', 'level': 2, 'name': 'Electronic / Design', 'description': 'Sound effects that are computer-made or designed for user interfaces or animations.'}, - {'category_code': 'fx-other', 'level': 2, 'name': 'Other (sound effects)', 'description': 'Sound effects that don\'t belong to any of the above categories.'}, + {'category_code': 'fx-other', 'level': 2, 'name': 'Other', 'description': 'Sound effects that don\'t belong to any of the above categories.'}, {'category_code': 'ss', 'level': 1, 'name': 'Soundscapes', 'description': 'Ambiances, field-recordings with multiple events and sound environments.'}, {'category_code': 'ss-n', 'level': 2, 'name': 'Nature', 'description': 'Soundscapes from natural habitats.'}, {'category_code': 'ss-i', 'level': 2, 'name': 'Indoors', 'description': 'Soundscapes from closed or indoor spaces.'}, {'category_code': 'ss-u', 'level': 2, 'name': 'Urban', 'description': 'Soundscapes from cityscapes or outdoor places with human intervention.'}, {'category_code': 'ss-s', 'level': 2, 'name': 'Synthetic / Artificial', 'description': 'Soundscapes that are synthesized or computer-made ambiences.'}, - {'category_code': 'ss-other', 'level': 2, 'name': 'Other (soundscapes)', 'description': 'Soundscapes that don\'t belong to any of the above categories.'}, + {'category_code': 'ss-other', 'level': 2, 'name': 'Other', 'description': 'Soundscapes that don\'t belong to any of the above categories.'}, ] # ------------------------------------------------------------------------------- diff --git a/sounds/migrations/0054_alter_sound_bst_category.py b/sounds/migrations/0054_alter_sound_bst_category.py index 0988305d3..3c16b1011 100644 --- a/sounds/migrations/0054_alter_sound_bst_category.py +++ b/sounds/migrations/0054_alter_sound_bst_category.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-12-23 17:10 +# Generated by Django 3.2.23 on 2025-01-16 15:18 from django.db import migrations, models @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sound', name='bst_category', - field=models.CharField(blank=True, choices=[('m', 'Music'), ('m-sp', 'Solo percussion'), ('m-si', 'Solo instrument'), ('m-m', 'Multiple instruments'), ('m-other', 'Other (music)'), ('is', 'Instrument samples'), ('is-p', 'Percussion'), ('is-s', 'String'), ('is-w', 'Wind'), ('is-k', 'Piano / Keyboard instruments'), ('is-e', 'Synths / Electronic'), ('is-other', 'Instrument samples (other)'), ('sp', 'Speech'), ('sp-s', 'Solo speech'), ('sp-c', 'Conversation / Crowd'), ('sp-p', 'Processed / Synthetic'), ('sp-other', 'Other (speech)'), ('fx', 'Sound effects'), ('fx-o', 'Objects / House appliances'), ('fx-v', 'Vehicles'), ('fx-m', 'Other mechanisms, engines, machines'), ('fx-h', 'Human sounds and actions'), ('fx-a', 'Animals'), ('fx-n', 'Natural elements and explosions'), ('fx-ex', 'Experimental'), ('fx-el', 'Electronic / Design'), ('fx-other', 'Other (sound effects)'), ('ss', 'Soundscapes'), ('ss-n', 'Nature'), ('ss-i', 'Indoors'), ('ss-u', 'Urban'), ('ss-s', 'Synthetic / Artificial'), ('ss-other', 'Other (soundscapes)')], default=None, max_length=8, null=True), + field=models.CharField(blank=True, choices=[('m', 'Music'), ('m-sp', 'Solo percussion'), ('m-si', 'Solo instrument'), ('m-m', 'Multiple instruments'), ('m-other', 'Other'), ('is', 'Instrument samples'), ('is-p', 'Percussion'), ('is-s', 'String'), ('is-w', 'Wind'), ('is-k', 'Piano / Keyboard instruments'), ('is-e', 'Synths / Electronic'), ('is-other', 'Other'), ('sp', 'Speech'), ('sp-s', 'Solo speech'), ('sp-c', 'Conversation / Crowd'), ('sp-p', 'Processed / Synthetic'), ('sp-other', 'Other'), ('fx', 'Sound effects'), ('fx-o', 'Objects / House appliances'), ('fx-v', 'Vehicles'), ('fx-m', 'Other mechanisms, engines, machines'), ('fx-h', 'Human sounds and actions'), ('fx-a', 'Animals'), ('fx-n', 'Natural elements and explosions'), ('fx-ex', 'Experimental'), ('fx-el', 'Electronic / Design'), ('fx-other', 'Other'), ('ss', 'Soundscapes'), ('ss-n', 'Nature'), ('ss-i', 'Indoors'), ('ss-u', 'Urban'), ('ss-s', 'Synthetic / Artificial'), ('ss-other', 'Other')], default=None, max_length=8, null=True), ), ] From a3ad619998afc897ec9d044b42a64e8481afbb42 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 16 Jan 2025 15:36:34 +0100 Subject: [PATCH 08/14] Migration of ai field --- sounds/migrations/0055_sound_is_gen_ai.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 sounds/migrations/0055_sound_is_gen_ai.py diff --git a/sounds/migrations/0055_sound_is_gen_ai.py b/sounds/migrations/0055_sound_is_gen_ai.py new file mode 100644 index 000000000..30de4a8b9 --- /dev/null +++ b/sounds/migrations/0055_sound_is_gen_ai.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2025-01-16 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sounds', '0054_alter_sound_bst_category'), + ] + + operations = [ + migrations.AddField( + model_name='sound', + name='is_gen_ai', + field=models.BooleanField(default=False), + ), + ] From d2084d8563ce156ac4883949d6d6a6da2dd1e0c0 Mon Sep 17 00:00:00 2001 From: allholy Date: Thu, 16 Jan 2025 15:38:05 +0100 Subject: [PATCH 09/14] Add BST analyzer worker --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 4010c6a7a..f225e2202 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -147,6 +147,17 @@ services: depends_on: - rabbitmq + # BST analyzer - needs image to be built from freesound-audio-analyzers repository + worker_analyzer6: + profiles: ["analyzers", "all"] + image: bst-extractor_v1 + volumes: + - ./freesound-data/:/freesound-data + init: true + command: celery -A main worker --pool=threads --concurrency=1 -l info -Q bst-extractor_v1 + depends_on: + - rabbitmq + # Similarity http server similarity: profiles: ["all"] From 399610d412600b8f8bc0cec16fc4e14442f56921 Mon Sep 17 00:00:00 2001 From: ffont Date: Mon, 20 Jan 2025 18:54:06 +0100 Subject: [PATCH 10/14] Move js and css code to static, layout improvements --- .../src/components/bstCategoryFormField.js | 74 +++++++++++++++ .../src/pages/editDescribeSounds.js | 2 + .../bw-frontend/styles/atoms/index.scss | 3 +- .../bw-frontend/styles/atoms/tooltip.scss | 83 +++++++++++++++++ .../styles/pages/bulkDescribe.scss | 56 ------------ sounds/forms.py | 6 +- templates/accounts/bulk_describe.html | 18 ++-- .../molecules/bst_category_form_field.html | 90 ++----------------- 8 files changed, 179 insertions(+), 153 deletions(-) create mode 100644 freesound/static/bw-frontend/src/components/bstCategoryFormField.js create mode 100644 freesound/static/bw-frontend/styles/atoms/tooltip.scss diff --git a/freesound/static/bw-frontend/src/components/bstCategoryFormField.js b/freesound/static/bw-frontend/src/components/bstCategoryFormField.js new file mode 100644 index 000000000..9abfcc395 --- /dev/null +++ b/freesound/static/bw-frontend/src/components/bstCategoryFormField.js @@ -0,0 +1,74 @@ +const updateSubcategoriesList = (hiddenField, subcategoryContainer, subcategoryButtons) => { + if (hiddenField.value === "") { + subcategoryContainer.classList.add("display-none"); + return; + } else { + // Display subcategories that match the selected top-level category + // Triggered when a selection is made or updated. + const correspondingTopLevelValue = hiddenField.value.split("-")[0]; // Extract top-level value, if not already. + if(correspondingTopLevelValue) { + subcategoryContainer.classList.remove("display-none"); + subcategoryButtons.forEach(subBtn => { + const topLevelValue = subBtn.getAttribute("top_level"); + subBtn.style.display = topLevelValue === correspondingTopLevelValue ? "inline-block" : "none"; + }); + } + } +} + +const prepareCategoryFormFields = (mainContainer) => { + const categoryFieldContainers = mainContainer.getElementsByClassName("bst-category-field"); + categoryFieldContainers.forEach(container => { + const hiddenField = container.querySelectorAll("input[type=hidden]")[0]; + const topButtons = container.querySelectorAll(".top-buttons .btn"); + const subcategoryContainer = container.querySelector(".subcategory-buttons"); + const subcategoryButtons = subcategoryContainer.querySelectorAll(".btn-subcategory"); + + + // Event listener for top-level category buttons + topButtons.forEach(topBtn => { + topBtn.addEventListener("click", function () { + const selectedValue = this.getAttribute("data_value"); + + // Update hidden input value + hiddenField.value = selectedValue; + + // Highlight the selected top-level category button + topButtons.forEach(btn => { + btn.classList.remove("btn-primary") + btn.classList.add("btn-inverse") + btn.setAttribute("aria-selected", "false") + }) + this.classList.remove("btn-inverse") + this.classList.add("btn-primary") + this.setAttribute("aria-selected", "true") + + updateSubcategoriesList(hiddenField, subcategoryContainer, subcategoryButtons); + }); + }); + + // Event listener for subcategory buttons + subcategoryButtons.forEach(subBtn => { + subBtn.addEventListener("click", function () { + const subcategoryValue = this.getAttribute("data_value"); + + // Update hidden input value if subcategory is clicked + hiddenField.value = subcategoryValue; + + // Highlight the selected subcategory button + subcategoryButtons.forEach(btn => { + btn.classList.remove("btn-primary") + btn.classList.add("btn-inverse") + btn.setAttribute("aria-selected", "false") + }) + this.classList.remove("btn-inverse") + this.classList.add("btn-primary") + btn.setAttribute("aria-selected", "true"); + }); + }); + + updateSubcategoriesList(hiddenField, subcategoryContainer, subcategoryButtons); + }); +} + +export {prepareCategoryFormFields} diff --git a/freesound/static/bw-frontend/src/pages/editDescribeSounds.js b/freesound/static/bw-frontend/src/pages/editDescribeSounds.js index 94922d16d..b346b5d65 100644 --- a/freesound/static/bw-frontend/src/pages/editDescribeSounds.js +++ b/freesound/static/bw-frontend/src/pages/editDescribeSounds.js @@ -2,10 +2,12 @@ import {prepareTagsFormFields, updateTags} from "../components/tagsFormField" import {prepareGeotagFormFields} from "../components/geotagFormField" import {preparePackFormFields} from "../components/packFormField" import {prepareAddSoundsModalAndFields} from "../components/addSoundsModal" +import {prepareCategoryFormFields} from "../components/bstCategoryFormField"; prepareAddSoundsModalAndFields(document); prepareTagsFormFields(document); preparePackFormFields(document); +prepareCategoryFormFields(document); document.addEventListener("DOMContentLoaded", () => { // Running this inside DOMContentLoaded to make sure mapbox gl scripts are loaded prepareGeotagFormFields(document); diff --git a/freesound/static/bw-frontend/styles/atoms/index.scss b/freesound/static/bw-frontend/styles/atoms/index.scss index 0b7dddaa4..fc3b4d1e2 100644 --- a/freesound/static/bw-frontend/styles/atoms/index.scss +++ b/freesound/static/bw-frontend/styles/atoms/index.scss @@ -17,4 +17,5 @@ @import 'selectableObject'; @import 'remixArrows'; @import 'table'; -@import 'announcementBanner'; \ No newline at end of file +@import 'announcementBanner'; +@import 'tooltip'; \ No newline at end of file diff --git a/freesound/static/bw-frontend/styles/atoms/tooltip.scss b/freesound/static/bw-frontend/styles/atoms/tooltip.scss new file mode 100644 index 000000000..2e721ff84 --- /dev/null +++ b/freesound/static/bw-frontend/styles/atoms/tooltip.scss @@ -0,0 +1,83 @@ +/* Tooltip right */ +.tooltip_right { position: relative; } +.tooltip_right .tooltiptext { + visibility: hidden; + background-color: $black; + color: $white; + text-align: center; + padding: 5px; + border-radius: 6px; + position: absolute; + z-index: 1; + width: 200px; + top: 0px; + left: 105%; +} +.tooltip_right:hover .tooltiptext { visibility: visible; } +.tooltip_right:hover { cursor: pointer; } +.tooltip_right .tooltiptext::after { + content: " "; + position: absolute; + top: 20px; + right: 100%; /* To the left of the tooltip */ + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent $black transparent transparent; +} + +/* Tooltip left */ +.tooltip_left { position: relative; } +.tooltip_left .tooltiptext { + visibility: hidden; + background-color: $black; + color: $white; + text-align: center; + padding: 5px; + border-radius: 6px; + position: absolute; + z-index: 1; + width: 200px; + top: 0px; + right: 105%; +} +.tooltip_left:hover .tooltiptext { visibility: visible; } +.tooltip_left:hover { cursor: pointer; } +.tooltip_left .tooltiptext::after { + content: " "; + position: absolute; + top: 20px; + left: 100%; /* To the right of the tooltip */ + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent $black; +} + +/* Tooltip top */ +.tooltip_top { position: relative; } +.tooltip_top .tooltiptext { + visibility: hidden; + background-color: $black; + color: $white; + text-align: center; + padding: 5px; + border-radius: 6px; + position: absolute; + z-index: 1; + width: 200px; + bottom: 115%; + left: 0%; +} +.tooltip_top:hover .tooltiptext { visibility: visible; } +.tooltip_top:hover { cursor: pointer; } +.tooltip_top .tooltiptext::after { + content: " "; + position: absolute; + top: 100%; + left: 20px; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: $black transparent transparent transparent; +} diff --git a/freesound/static/bw-frontend/styles/pages/bulkDescribe.scss b/freesound/static/bw-frontend/styles/pages/bulkDescribe.scss index b2d31e69a..6f4d0faa5 100644 --- a/freesound/static/bw-frontend/styles/pages/bulkDescribe.scss +++ b/freesound/static/bw-frontend/styles/pages/bulkDescribe.scss @@ -31,59 +31,3 @@ background-color: $red; color: $white; } - -/* Tooltip right */ -.cell_tooltip_right { position: relative; } -.cell_tooltip_right .tooltiptext { - visibility: hidden; - background-color: $black; - color: $white; - text-align: center; - padding: 5px; - border-radius: 6px; - position: absolute; - z-index: 1; - width: 200px; - top: 0px; - left: 105%; -} -.cell_tooltip_right:hover .tooltiptext { visibility: visible; } -.cell_tooltip_right:hover { cursor: pointer; } -.cell_tooltip_right .tooltiptext::after { - content: " "; - position: absolute; - top: 20px; - right: 100%; /* To the left of the tooltip */ - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent $black transparent transparent; -} - -/* Tooltip left */ -.cell_tooltip_left { position: relative; } -.cell_tooltip_left .tooltiptext { - visibility: hidden; - background-color: $black; - color: $white; - text-align: center; - padding: 5px; - border-radius: 6px; - position: absolute; - z-index: 1; - width: 200px; - top: 0px; - right: 105%; -} -.cell_tooltip_left:hover .tooltiptext { visibility: visible; } -.cell_tooltip_left:hover { cursor: pointer; } -.cell_tooltip_left .tooltiptext::after { - content: " "; - position: absolute; - top: 20px; - left: 100%; /* To the right of the tooltip */ - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent transparent $black; -} \ No newline at end of file diff --git a/sounds/forms.py b/sounds/forms.py index 2ed6088c6..856ea1975 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -247,7 +247,9 @@ class SoundEditAndDescribeForm(forms.Form): widget=forms.TextInput(attrs={'size': 65, 'class': 'inputText'})) bst_category = forms.ChoiceField( choices=Sound.BST_CATEGORY_CHOICES, - help_text="Choose the most appropriate catgeory and subcatgoery (you can only choose one). This category will be displayed as a filtering option in the Freesound side bar.", + help_text="Choose the most appropriate Category and Subcategory for the sound. "\ + "These categories are drawn from the Broad Sound Taxonomy, "\ + "and are used to improve Freesound search capabilities.", required=True, ) tags = TagField( @@ -355,7 +357,7 @@ def clean_pack(self): def clean_bst_category(self): value = self.cleaned_data['bst_category'] if not '-' in value: - raise ValidationError("Please choose a subcategory.") + raise ValidationError("Please choose both a category and a subcategory.") return value diff --git a/templates/accounts/bulk_describe.html b/templates/accounts/bulk_describe.html index a37e78c16..c7df477d7 100644 --- a/templates/accounts/bulk_describe.html +++ b/templates/accounts/bulk_describe.html @@ -46,39 +46,39 @@

Validation results of the data file "{{ bulk.original_csv_ {% for line in lines_failed_validation %} - + {{ line.line_no }} {{ line.line_errors.columns }} - + {{ line.line_original.audio_filename }} {{ line.line_errors.audio_filename }} - + {{ line.line_original.name }} {{ line.line_errors.name }} - + {{ line.line_original.tags }} {{ line.line_errors.tags }} - + {{ line.line_original.geotag }} {{ line.line_errors.geotag }} - + {{ line.line_original.description|truncatewords:20 }} {{ line.line_errors.description }} - + {{ line.line_original.license }} {{ line.line_errors.license }} - + {{ line.line_original.pack_name }} {{ line.line_errors.pack_name }} - + {{ line.line_original.is_explicit }} {{ line.line_errors.is_explicit }} diff --git a/templates/molecules/bst_category_form_field.html b/templates/molecules/bst_category_form_field.html index b9ca50975..d9ab2ab69 100644 --- a/templates/molecules/bst_category_form_field.html +++ b/templates/molecules/bst_category_form_field.html @@ -6,10 +6,10 @@
{{ form.bst_category.as_hidden }} -
+
{% for value, label in form.fields.bst_category.choices %} {% if "-" not in value %} -
- - +
\ No newline at end of file From 3b469f2644b893cf7556366d918c6ac1f0247cc2 Mon Sep 17 00:00:00 2001 From: ffont Date: Tue, 21 Jan 2025 10:58:57 +0100 Subject: [PATCH 11/14] Add category info in sound page --- freesound/settings.py | 66 +++++++++---------- .../bw-frontend/styles/pages/sound.scss | 9 +++ sounds/models.py | 32 +++++++-- sounds/templatetags/bst_category.py | 35 +++++++++- .../molecules/bst_category_form_field.html | 6 +- templates/sounds/sound.html | 13 ++-- 6 files changed, 111 insertions(+), 50 deletions(-) diff --git a/freesound/settings.py b/freesound/settings.py index 6179e949c..5844e8e1b 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -461,39 +461,39 @@ # Broad Sound Taxonomy definition BROAD_SOUND_TAXONOMY = [ - {'category_code': 'm', 'level': 1, 'name': 'Music', 'description': 'Music excerpts, melodies, loops, fillers, drones and short musical snippets.'}, - {'category_code': 'm-sp', 'level': 2, 'name': 'Solo percussion', 'description': 'Music excerpts with solo percussive instruments.'}, - {'category_code': 'm-si', 'level': 2, 'name': 'Solo instrument', 'description': 'Music excerpts with only one instrument, excluding percussion.'}, - {'category_code': 'm-m', 'level': 2, 'name': 'Multiple instruments', 'description': 'Music excerpts with more than one instrument.'}, - {'category_code': 'm-other', 'level': 2, 'name': 'Other', 'description': 'Music that doesn\'t belong to any of the above categories.'}, - {'category_code': 'is', 'level': 1, 'name': 'Instrument samples', 'description': 'Single notes from musical instruments, various versions of the same note, and scales.'}, - {'category_code': 'is-p', 'level': 2, 'name': 'Percussion', 'description': 'Instrument samples that are percussive (idiophones or membraphones).'}, - {'category_code': 'is-s', 'level': 2, 'name': 'String', 'description': 'Instrument samples that belong to the string instrument family.'}, - {'category_code': 'is-w', 'level': 2, 'name': 'Wind', 'description': 'Instrument samples that belong to the wind instrument family (aerophones).'}, - {'category_code': 'is-k', 'level': 2, 'name': 'Piano / Keyboard instruments', 'description': 'Instrument samples of piano or other keyboard instruments, not synthesized.'}, - {'category_code': 'is-e', 'level': 2, 'name': 'Synths / Electronic', 'description': 'Instrument samples synthesized or produced by electronic means.'}, - {'category_code': 'is-other', 'level': 2, 'name': 'Other', 'description': 'Instrument samples that don\'t belong to any of the above categories.'}, - {'category_code': 'sp', 'level': 1, 'name': 'Speech', 'description': 'Sounds where human voice is prominent.'}, - {'category_code': 'sp-s', 'level': 2, 'name': 'Solo speech', 'description': 'Recording of a single voice speaking, excluding singing.'}, - {'category_code': 'sp-c', 'level': 2, 'name': 'Conversation / Crowd', 'description': 'Several people talking, having a conversation or dialogue.'}, - {'category_code': 'sp-p', 'level': 2, 'name': 'Processed / Synthetic', 'description': 'Voice(s) from an indirect source (e.g. radio), processed or synthesized.'}, - {'category_code': 'sp-other', 'level': 2, 'name': 'Other', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'}, - {'category_code': 'fx', 'level': 1, 'name': 'Sound effects', 'description': 'Isolated sound effects or sound events, each happening one at a time.'}, - {'category_code': 'fx-o', 'level': 2, 'name': 'Objects / House appliances', 'description': 'Everyday objects, inside the home or smaller in size.'}, - {'category_code': 'fx-v', 'level': 2, 'name': 'Vehicles', 'description': 'Sounds produced from a vehicle.'}, - {'category_code': 'fx-m', 'level': 2, 'name': 'Other mechanisms, engines, machines', 'description': 'Machine-like sounds, except vehicles and small house electric devices.'}, - {'category_code': 'fx-h', 'level': 2, 'name': 'Human sounds and actions', 'description': 'Sounds from the human body, excluding speech.'}, - {'category_code': 'fx-a', 'level': 2, 'name': 'Animals', 'description': 'Animal vocalizations or sounds.'}, - {'category_code': 'fx-n', 'level': 2, 'name': 'Natural elements and explosions', 'description': 'Sound events occuring by natural processes.'}, - {'category_code': 'fx-ex', 'level': 2, 'name': 'Experimental', 'description': 'Experimental sounds or heavily processed audio recordings.'}, - {'category_code': 'fx-el', 'level': 2, 'name': 'Electronic / Design', 'description': 'Sound effects that are computer-made or designed for user interfaces or animations.'}, - {'category_code': 'fx-other', 'level': 2, 'name': 'Other', 'description': 'Sound effects that don\'t belong to any of the above categories.'}, - {'category_code': 'ss', 'level': 1, 'name': 'Soundscapes', 'description': 'Ambiances, field-recordings with multiple events and sound environments.'}, - {'category_code': 'ss-n', 'level': 2, 'name': 'Nature', 'description': 'Soundscapes from natural habitats.'}, - {'category_code': 'ss-i', 'level': 2, 'name': 'Indoors', 'description': 'Soundscapes from closed or indoor spaces.'}, - {'category_code': 'ss-u', 'level': 2, 'name': 'Urban', 'description': 'Soundscapes from cityscapes or outdoor places with human intervention.'}, - {'category_code': 'ss-s', 'level': 2, 'name': 'Synthetic / Artificial', 'description': 'Soundscapes that are synthesized or computer-made ambiences.'}, - {'category_code': 'ss-other', 'level': 2, 'name': 'Other', 'description': 'Soundscapes that don\'t belong to any of the above categories.'}, + {'key': 'm', 'level': 1, 'name': 'Music', 'description': 'Music excerpts, melodies, loops, fillers, drones and short musical snippets.'}, + {'key': 'm-sp', 'level': 2, 'name': 'Solo percussion', 'description': 'Music excerpts with solo percussive instruments.'}, + {'key': 'm-si', 'level': 2, 'name': 'Solo instrument', 'description': 'Music excerpts with only one instrument, excluding percussion.'}, + {'key': 'm-m', 'level': 2, 'name': 'Multiple instruments', 'description': 'Music excerpts with more than one instrument.'}, + {'key': 'm-other', 'level': 2, 'name': 'Other', 'description': 'Music that doesn\'t belong to any of the above categories.'}, + {'key': 'is', 'level': 1, 'name': 'Instrument samples', 'description': 'Single notes from musical instruments, various versions of the same note, and scales.'}, + {'key': 'is-p', 'level': 2, 'name': 'Percussion', 'description': 'Instrument samples that are percussive (idiophones or membraphones).'}, + {'key': 'is-s', 'level': 2, 'name': 'String', 'description': 'Instrument samples that belong to the string instrument family.'}, + {'key': 'is-w', 'level': 2, 'name': 'Wind', 'description': 'Instrument samples that belong to the wind instrument family (aerophones).'}, + {'key': 'is-k', 'level': 2, 'name': 'Piano / Keyboard instruments', 'description': 'Instrument samples of piano or other keyboard instruments, not synthesized.'}, + {'key': 'is-e', 'level': 2, 'name': 'Synths / Electronic', 'description': 'Instrument samples synthesized or produced by electronic means.'}, + {'key': 'is-other', 'level': 2, 'name': 'Other', 'description': 'Instrument samples that don\'t belong to any of the above categories.'}, + {'key': 'sp', 'level': 1, 'name': 'Speech', 'description': 'Sounds where human voice is prominent.'}, + {'key': 'sp-s', 'level': 2, 'name': 'Solo speech', 'description': 'Recording of a single voice speaking, excluding singing.'}, + {'key': 'sp-c', 'level': 2, 'name': 'Conversation / Crowd', 'description': 'Several people talking, having a conversation or dialogue.'}, + {'key': 'sp-p', 'level': 2, 'name': 'Processed / Synthetic', 'description': 'Voice(s) from an indirect source (e.g. radio), processed or synthesized.'}, + {'key': 'sp-other', 'level': 2, 'name': 'Other', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'}, + {'key': 'fx', 'level': 1, 'name': 'Sound effects', 'description': 'Isolated sound effects or sound events, each happening one at a time.'}, + {'key': 'fx-o', 'level': 2, 'name': 'Objects / House appliances', 'description': 'Everyday objects, inside the home or smaller in size.'}, + {'key': 'fx-v', 'level': 2, 'name': 'Vehicles', 'description': 'Sounds produced from a vehicle.'}, + {'key': 'fx-m', 'level': 2, 'name': 'Other mechanisms, engines, machines', 'description': 'Machine-like sounds, except vehicles and small house electric devices.'}, + {'key': 'fx-h', 'level': 2, 'name': 'Human sounds and actions', 'description': 'Sounds from the human body, excluding speech.'}, + {'key': 'fx-a', 'level': 2, 'name': 'Animals', 'description': 'Animal vocalizations or sounds.'}, + {'key': 'fx-n', 'level': 2, 'name': 'Natural elements and explosions', 'description': 'Sound events occuring by natural processes.'}, + {'key': 'fx-ex', 'level': 2, 'name': 'Experimental', 'description': 'Experimental sounds or heavily processed audio recordings.'}, + {'key': 'fx-el', 'level': 2, 'name': 'Electronic / Design', 'description': 'Sound effects that are computer-made or designed for user interfaces or animations.'}, + {'key': 'fx-other', 'level': 2, 'name': 'Other', 'description': 'Sound effects that don\'t belong to any of the above categories.'}, + {'key': 'ss', 'level': 1, 'name': 'Soundscapes', 'description': 'Ambiances, field-recordings with multiple events and sound environments.'}, + {'key': 'ss-n', 'level': 2, 'name': 'Nature', 'description': 'Soundscapes from natural habitats.'}, + {'key': 'ss-i', 'level': 2, 'name': 'Indoors', 'description': 'Soundscapes from closed or indoor spaces.'}, + {'key': 'ss-u', 'level': 2, 'name': 'Urban', 'description': 'Soundscapes from cityscapes or outdoor places with human intervention.'}, + {'key': 'ss-s', 'level': 2, 'name': 'Synthetic / Artificial', 'description': 'Soundscapes that are synthesized or computer-made ambiences.'}, + {'key': 'ss-other', 'level': 2, 'name': 'Other', 'description': 'Soundscapes that don\'t belong to any of the above categories.'}, ] # ------------------------------------------------------------------------------- diff --git a/freesound/static/bw-frontend/styles/pages/sound.scss b/freesound/static/bw-frontend/styles/pages/sound.scss index 9cb696731..8968744a2 100644 --- a/freesound/static/bw-frontend/styles/pages/sound.scss +++ b/freesound/static/bw-frontend/styles/pages/sound.scss @@ -31,3 +31,12 @@ } } } + +.dot-separated-divs { + div:not(:last-child):after { + content: "·"; + color: $navy-grey; + margin-left: $tiny-spacing; + margin-right: $tiny-spacing; + } +} \ No newline at end of file diff --git a/sounds/models.py b/sounds/models.py index c779fe662..334d80975 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -44,7 +44,7 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.encoding import smart_str -from django.utils.http import urlquote +from django.utils.http import urlquote, urlencode from django.utils.functional import cached_property from django.utils.text import Truncator, slugify @@ -57,6 +57,7 @@ from geotags.models import GeoTag from ratings.models import SoundRating from general.templatetags.util import formatnumber +from sounds.templatetags.bst_category import bst_taxonomy_category_key_to_category_names from tags.models import TaggedItem, Tag from tickets import TICKET_STATUS_CLOSED, TICKET_STATUS_NEW from tickets.models import Ticket, Queue, TicketComment @@ -623,7 +624,7 @@ class Sound(models.Model): date_recorded = models.DateField(null=True, blank=True, default=None) # Broad Sound Taxonomy (BST) category - BST_CATEGORY_CHOICES = [(item['category_code'], item['name']) for item in settings.BROAD_SOUND_TAXONOMY] + BST_CATEGORY_CHOICES = [(item['key'], item['name']) for item in settings.BROAD_SOUND_TAXONOMY] bst_category = models.CharField(max_length=8, null=True, blank=True, default=None, choices=BST_CATEGORY_CHOICES) # The history of licenses for a sound is stored on SoundLicenseHistory 'license' references the last one @@ -701,11 +702,6 @@ class Sound(models.Model): def __str__(self): return self.base_filename_slug - @staticmethod - def is_sound(): - # N.B. This is used in the ticket template (ugly, but a quick fix) - return True - @property def moderated_and_processed_ok(self): return self.moderation_state == "OK" and self.processing_state == "OK" @@ -1410,6 +1406,28 @@ def ready_for_similarity(self): def get_similarity_search_target_vector(self, analyzer=settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER): # If the sound has been analyzed for similarity, returns the vector to be used for similarity search return get_similarity_search_target_vector(self.id, analyzer=analyzer) + + @property + def get_category_names(self): + return bst_taxonomy_category_key_to_category_names(self.bst_category) + + @property + def get_top_level_category_search_url(self): + top_level_name, _ = self.get_category_names + if top_level_name: + cat_filter = urlencode({'f': 'bst_top_level:' + top_level_name}) + return f'{reverse("sounds-search")}?{cat_filter}' + else: + return None + + @property + def get_second_level_category_search_url(self): + second_level_name, _ = self.get_category_names + if second_level_name: + cat_filter = urlencode({'f': 'bst_second_level:' + second_level_name}) + return f'{reverse("sounds-search")}?{cat_filter}' + else: + return None class Meta: ordering = ("-created", ) diff --git a/sounds/templatetags/bst_category.py b/sounds/templatetags/bst_category.py index 530ce30a7..cdefa2847 100644 --- a/sounds/templatetags/bst_category.py +++ b/sounds/templatetags/bst_category.py @@ -1,11 +1,42 @@ from django import template +from django.conf import settings register = template.Library() + +def bst_taxonomy_category_key_to_category_names(category_key): + """ + Get the category names for the given category key. + This includes both the top level and the sub level category names. + E.g.: "m-sp" -> ("Music", "Solo percussion"), "m" -> ("Music", None) + """ + if '-' in category_key: + # Sub level category key + top_level_key = category_key.split('-')[0] + second_level_key = category_key + else: + # Only top level category spcifcied, sub level is unknown + top_level_key = category_key + second_level_key = None + try: + top_level_category_name = [item['name'] for item in settings.BROAD_SOUND_TAXONOMY if item['key'] == top_level_key][0] + except IndexError: + # If for some reason we change category keys and some sounds are outdated, let's not crash + top_level_category_name = None + try: + second_level_category_name = [item['name'] for item in settings.BROAD_SOUND_TAXONOMY if item['key'] == second_level_key][0] \ + if second_level_key is not None else None + except IndexError: + # If for some reason we change category keys and some sounds are outdated, let's not crash + second_level_category_name = None + return (top_level_category_name, second_level_category_name) + + @register.filter -def get_top_level_bst_category(value): +def get_bst_taxonomy_top_level_category_key(value): """ - Extract the top level category from the given value. + Extract the top level category key from the given value. The top level category is the part before the '-' in value. + E.g.: "m-sp" -> "m" """ return value.split('-')[0] if '-' in value else value diff --git a/templates/molecules/bst_category_form_field.html b/templates/molecules/bst_category_form_field.html index d9ab2ab69..93353e96d 100644 --- a/templates/molecules/bst_category_form_field.html +++ b/templates/molecules/bst_category_form_field.html @@ -9,10 +9,10 @@
{% for value, label in form.fields.bst_category.choices %} {% if "-" not in value %} -