From 813bd48c56fd0661670e497227c062276771a714 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Wed, 8 Jan 2025 10:12:29 +0100 Subject: [PATCH 01/28] collections app creation and first basic implementations --- freesound/settings.py | 1 + freesound/urls.py | 2 + fscollections/__init__.py | 0 fscollections/admin.py | 3 ++ fscollections/apps.py | 6 +++ fscollections/forms.py | 0 fscollections/migrations/0001_initial.py | 28 ++++++++++++ fscollections/migrations/__init__.py | 0 fscollections/models.py | 37 +++++++++++++++ fscollections/tests.py | 3 ++ fscollections/urls.py | 6 +++ fscollections/views.py | 57 ++++++++++++++++++++++++ templates/collections/collections.html | 11 +++++ 13 files changed, 154 insertions(+) create mode 100644 fscollections/__init__.py create mode 100644 fscollections/admin.py create mode 100644 fscollections/apps.py create mode 100644 fscollections/forms.py create mode 100644 fscollections/migrations/0001_initial.py create mode 100644 fscollections/migrations/__init__.py create mode 100644 fscollections/models.py create mode 100644 fscollections/tests.py create mode 100644 fscollections/urls.py create mode 100644 fscollections/views.py create mode 100644 templates/collections/collections.html diff --git a/freesound/settings.py b/freesound/settings.py index 231d4cbaa4..b3624f553b 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -78,6 +78,7 @@ 'admin_reorder', 'captcha', 'adminsortable', + 'fscollections' ] # Specify custom ordering of models in Django Admin index diff --git a/freesound/urls.py b/freesound/urls.py index 48d6ed94e5..539eb4ba69 100644 --- a/freesound/urls.py +++ b/freesound/urls.py @@ -34,6 +34,7 @@ import bookmarks.views import follow.views import donations.views +import fscollections.views import utils.tagrecommendation_utilities as tagrec from apiv2.apiv2_utils import apiv1_end_of_life_message @@ -116,6 +117,7 @@ path('tickets/', include('tickets.urls')), path('monitor/', include('monitor.urls')), path('follow/', include('follow.urls')), + path('fscollections/', include('fscollections.urls')), path('blog/', RedirectView.as_view(url='https://blog.freesound.org/'), name="blog"), diff --git a/fscollections/__init__.py b/fscollections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fscollections/admin.py b/fscollections/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/fscollections/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/fscollections/apps.py b/fscollections/apps.py new file mode 100644 index 0000000000..61912332ed --- /dev/null +++ b/fscollections/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FscollectionsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'fscollections' diff --git a/fscollections/forms.py b/fscollections/forms.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fscollections/migrations/0001_initial.py b/fscollections/migrations/0001_initial.py new file mode 100644 index 0000000000..3965880b77 --- /dev/null +++ b/fscollections/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.23 on 2025-01-07 12:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sounds', '0052_alter_sound_type'), + ] + + operations = [ + migrations.CreateModel( + name='Collection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=128)), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('sound', models.ManyToManyField(to='sounds.Sound')), + ], + ), + ] diff --git a/fscollections/migrations/__init__.py b/fscollections/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fscollections/models.py b/fscollections/models.py new file mode 100644 index 0000000000..9992aaa90c --- /dev/null +++ b/fscollections/models.py @@ -0,0 +1,37 @@ +# +# Freesound is (c) MUSIC TECHNOLOGY GROUP, UNIVERSITAT POMPEU FABRA +# +# Freesound is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Freesound is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Authors: +# See AUTHORS file. +# + +from django.contrib.auth.models import User +from django.db import models + +from sounds.models import Sound, License + +class Collection(models.Model): + + author = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=128, default="") #prob not default to "" + sounds = models.ManyToManyField(Sound, related_name="collections") #this will affect the following migration [related_name, sound(s)] + created = models.DateTimeField(db_index=True, auto_now_add=True) + #contributors = delicate stuff + #subcolletion_path = + + def __str__(self): + return f"{self.name}" + \ No newline at end of file diff --git a/fscollections/tests.py b/fscollections/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/fscollections/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/fscollections/urls.py b/fscollections/urls.py new file mode 100644 index 0000000000..c6d5ced882 --- /dev/null +++ b/fscollections/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('',views.collections, name='collections') +] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py new file mode 100644 index 0000000000..f36fad1359 --- /dev/null +++ b/fscollections/views.py @@ -0,0 +1,57 @@ +# +# Freesound is (c) MUSIC TECHNOLOGY GROUP, UNIVERSITAT POMPEU FABRA +# +# Freesound is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Freesound is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Authors: +# See AUTHORS file. +# + +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse +from django.shortcuts import get_object_or_404, render +from django.urls import reverse + +from fscollections.models import Collection +from sounds.models import Sound + +@login_required +def collections(request): + user = request.user + #collection = Collection.__init__(author=user, ) + #tvars = {collection = collection} + return render(request, 'collections/collections.html') + +def add_sound_to_collection(request, sound_id, collection_id): + sound = get_object_or_404(Sound, id=sound_id) + collection = get_object_or_404(Collection, id=collection_id, author=request.user) + + if sound.moderation_state=='OK': + collection.sound.add(sound) #TBC after next migration + collection.save() + return True + else: + return "sound not moderated or not collection owner" + +def delete_sound_from_collection(request, sound_id, collection_id): + sound = get_object_or_404(Sound, id=sound_id) + collection = get_object_or_404(Collection, id=collection_id, author=request.user) + + if sound in collection.sound.all(): + collection.sound.remove(sound) #TBC after next migration + collection.save() + return True + else: + return "this sound is not in the collection" \ No newline at end of file diff --git a/templates/collections/collections.html b/templates/collections/collections.html new file mode 100644 index 0000000000..490d829fb0 --- /dev/null +++ b/templates/collections/collections.html @@ -0,0 +1,11 @@ +{% extends "simple_page.html" %} +{% load static %} +{% load bw_templatetags %} +{% load display_sound %} +{% load util %} + +{% block title%}Collections{%endblock title%} +{% block page-title%}Collections{%endblock page-title%} +{%block page-content%} +Collection Name: {{collection.name}} +{%endblock page-content%} \ No newline at end of file From 52543c406b65b9db9c2c530685fefa6be107ad79 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 9 Jan 2025 12:52:50 +0100 Subject: [PATCH 02/28] migration: add collection attributes improve url handling and visualization --- freesound/urls.py | 2 +- .../migrations/0002_auto_20250109_1035.py | 28 ++++++++++++++++ fscollections/models.py | 13 +++++--- fscollections/urls.py | 3 +- fscollections/views.py | 22 +++++++++---- templates/collections/collections.html | 32 +++++++++++++++++-- 6 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 fscollections/migrations/0002_auto_20250109_1035.py diff --git a/freesound/urls.py b/freesound/urls.py index 539eb4ba69..15e48a4250 100644 --- a/freesound/urls.py +++ b/freesound/urls.py @@ -117,7 +117,7 @@ path('tickets/', include('tickets.urls')), path('monitor/', include('monitor.urls')), path('follow/', include('follow.urls')), - path('fscollections/', include('fscollections.urls')), + path('collections/', include('fscollections.urls')), path('blog/', RedirectView.as_view(url='https://blog.freesound.org/'), name="blog"), diff --git a/fscollections/migrations/0002_auto_20250109_1035.py b/fscollections/migrations/0002_auto_20250109_1035.py new file mode 100644 index 0000000000..d160c622f6 --- /dev/null +++ b/fscollections/migrations/0002_auto_20250109_1035.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.23 on 2025-01-09 10:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sounds', '0052_alter_sound_type'), + ('fscollections', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='collection', + name='sound', + ), + migrations.AddField( + model_name='collection', + name='description', + field=models.TextField(default='', max_length=500), + ), + migrations.AddField( + model_name='collection', + name='sounds', + field=models.ManyToManyField(related_name='collections', to='sounds.Sound'), + ), + ] diff --git a/fscollections/models.py b/fscollections/models.py index 9992aaa90c..14ca5a17d9 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -26,12 +26,17 @@ class Collection(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) - name = models.CharField(max_length=128, default="") #prob not default to "" - sounds = models.ManyToManyField(Sound, related_name="collections") #this will affect the following migration [related_name, sound(s)] + name = models.CharField(max_length=128, default="") #add restrictions + sounds = models.ManyToManyField(Sound, related_name="collections") #NOTE: before next migration pluralize sound(s) - check consequences in views created = models.DateTimeField(db_index=True, auto_now_add=True) + description = models.TextField(max_length=500, default="") + #NOTE: before next migration add a num_sounds attribute #contributors = delicate stuff - #subcolletion_path = + #subcolletion_path = sth with tagsn and routing folders for downloads def __str__(self): return f"{self.name}" - \ No newline at end of file + +''' +class CollectionSound(models.Model): + ''' \ No newline at end of file diff --git a/fscollections/urls.py b/fscollections/urls.py index c6d5ced882..63db75ba17 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -2,5 +2,6 @@ from . import views urlpatterns = [ - path('',views.collections, name='collections') + path('',views.collections, name='collections'), + path('/', views.collections, name='collections') ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index f36fad1359..b077e60bb4 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -28,18 +28,28 @@ from sounds.models import Sound @login_required -def collections(request): +def collections(request, collection_id=None): user = request.user - #collection = Collection.__init__(author=user, ) - #tvars = {collection = collection} - return render(request, 'collections/collections.html') + user_collections = Collection.objects.filter(author=user).order_by('-created') #first user collection should be on top - this would affect the if block + #if no collection id is provided for this URL, render the oldest collection in the webpage + if not collection_id: + collection = user_collections.last() + else: + collection = get_object_or_404(Collection, id=collection_id) + + tvars = {'collection': collection, + 'collections_for_user': user_collections} + + return render(request, 'collections/collections.html', tvars) + +#upon creating a new user, create alongside a personal PRIVATE collection to store sounds def add_sound_to_collection(request, sound_id, collection_id): sound = get_object_or_404(Sound, id=sound_id) collection = get_object_or_404(Collection, id=collection_id, author=request.user) if sound.moderation_state=='OK': - collection.sound.add(sound) #TBC after next migration + collection.sounds.add(sound) collection.save() return True else: @@ -50,7 +60,7 @@ def delete_sound_from_collection(request, sound_id, collection_id): collection = get_object_or_404(Collection, id=collection_id, author=request.user) if sound in collection.sound.all(): - collection.sound.remove(sound) #TBC after next migration + collection.sounds.remove(sound) collection.save() return True else: diff --git a/templates/collections/collections.html b/templates/collections/collections.html index 490d829fb0..0d47c6b2db 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -6,6 +6,32 @@ {% block title%}Collections{%endblock title%} {% block page-title%}Collections{%endblock page-title%} -{%block page-content%} -Collection Name: {{collection.name}} -{%endblock page-content%} \ No newline at end of file +{% block page-content %} +
+
+
+

Your collections

+
+
    + {% for col in collections_for_user %} +
  • {{col.name}} + · {{col.sounds.count}} sound{{col.sounds.count|pluralize}}
  • + {% endfor %} +
+
+
+
+

{{collection.name}}

+
+ {% if collection.sounds.count > 0 %} + {% for sound in collection.sounds.all %} +
+ {% display_sound_small sound %} +
+ {% endfor %} + {% else %} There aren't any sounds in this collection yet {% endif %} +
+
+
+
+{% endblock page-content %} \ No newline at end of file From 5f137ebb4836042b1d217b9074e6f0bf0ea8df2e Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 16 Jan 2025 10:19:59 +0100 Subject: [PATCH 03/28] attempt to implement collection modals --- fscollections/forms.py | 94 +++++++++++++++++++ fscollections/models.py | 8 +- fscollections/urls.py | 5 +- fscollections/views.py | 84 ++++++++++++++--- sounds/forms.py | 2 +- templates/collections/collections.html | 16 +++- .../collections/modal_collect_sound.html | 47 ++++++++++ templates/molecules/navbar_user_section.html | 3 + templates/sounds/sound.html | 5 + 9 files changed, 245 insertions(+), 19 deletions(-) create mode 100644 templates/collections/modal_collect_sound.html diff --git a/fscollections/forms.py b/fscollections/forms.py index e69de29bb2..d3a11c2396 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -0,0 +1,94 @@ +# +# Freesound is (c) MUSIC TECHNOLOGY GROUP, UNIVERSITAT POMPEU FABRA +# +# Freesound is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Freesound is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Authors: +# See AUTHORS file. +# + +from django import forms +from fscollections.models import Collection + +#this class was aimed to perform similarly to BookmarkSound, however, at first the method to add a sound to a collection +#will be opening the search engine in a modal, looking for a sound in there and adding it to the actual collection page +#this can be found in edit pack -> add sounds +#class CollectSoundForm(forms.ModelForm): + +class CollectionSoundForm(forms.Form): + #list of existing collections + #add sound to collection from sound page + collection = forms.ChoiceField( + label=False, + choices=[], + required=True) + + new_collection_name = forms.ChoiceField( + label = False, + help_text=None, + required = False) + + use_last_collection = forms.BooleanField(widget=forms.HiddenInput(), required=False, initial=False) + user_collections = None + + NO_COLLECTION_CHOICE_VALUE = '-1' + NEW_COLLECTION_CHOICE_VALUE = '0' + + def __init__(self, *args, **kwargs): + self.user_collections = kwargs.pop('user_collections', False) + self.user_saving_sound = kwargs.pop('user_saving_sound', False) + self.sound_id = kwargs.pop('sound_id', False) + super().__init__(*args, **kwargs) + self.fields['collection'].choices = [(self.NO_COLLECTION_CHOICE_VALUE, '--- No collection ---'),#in this case this goes to bookmarks collection (might have to be created) + (self.NEW_COLLECTION_CHOICE_VALUE, 'Create a new collection...')] + \ + ([(collection.id, collection.name) for collection in self.user_collections] + if self.user_collections else[]) + + self.fields['new_collection_name'].widget.attrs['placeholder'] = "Fill in the name for the new collection" + self.fields['category'].widget.attrs = { + 'data-grey-items': f'{self.NO_CATEGORY_CHOICE_VALUE},{self.NEW_CATEGORY_CHOICE_VALUE}'} + #i don't fully understand what this last line of code does + + def save(self, *args, **kwargs): + collection_to_use = None + + if not self.cleaned_data['use_last_collection']: + if self.cleaned_data['collection'] == self.NO_COLLECTION_CHOICE_VALUE: + pass + elif self.cleaned_data['collection'] == self.NEW_COLLECTION_CHOICE_VALUE: + if self.cleaned_data['new_collection_name'] != "": + collection = \ + Collection(author=self.user_saving_bookmark, name=self.cleaned_data['new_collection_name']) + collection.save() + collection_to_use = collection + else: + collection_to_use = Collection.objects.get(id=self.cleaned_data['collection']) + else: #en aquest cas - SÍ estem fent servir l'última coleccio, NO estem creant una nova coleccio, NO estem agafant una coleccio existent i + # per tant ens trobem en un cas de NO COLLECTION CHOICE VALUE (no s'ha triat cap coleccio) + # si no es tria cap coleccio: l'usuari té alguna colecció? NO -> creem BookmarksCollection pels seus sons privats + # SI -> per defecte es posa a BookmarksCollection + try: + last_user_collection = \ + Collection.objects.filter(user=self.user_saving_bookmark).order_by('-created')[0] + # If user has a previous bookmark, use the same category (or use none if no category used in last + # bookmark) + collection_to_use = last_user_collection + except IndexError: + # This is first bookmark of the user + pass + + # If collection already exists, don't save it and return the existing one + collection, _ = Collection.objects.get_or_create( + name = collection_to_use.name, author=self.user_saving_bookmark) + return collection \ No newline at end of file diff --git a/fscollections/models.py b/fscollections/models.py index 14ca5a17d9..8ef207ed69 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -26,13 +26,17 @@ class Collection(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) - name = models.CharField(max_length=128, default="") #add restrictions - sounds = models.ManyToManyField(Sound, related_name="collections") #NOTE: before next migration pluralize sound(s) - check consequences in views + name = models.CharField(max_length=128, default="BookmarkCollection") #add restrictions + sounds = models.ManyToManyField(Sound, related_name="collections") created = models.DateTimeField(db_index=True, auto_now_add=True) description = models.TextField(max_length=500, default="") #NOTE: before next migration add a num_sounds attribute + #bookmarks = True or False depending on it being the BookmarkCollection for a user (the one created for the first sound without collection assigned) #contributors = delicate stuff #subcolletion_path = sth with tagsn and routing folders for downloads + #follow relation for users and collections (intersted but not owner nor contributor) + #sooner or later you'll need to start using forms for adding sounds to collections xd + def __str__(self): return f"{self.name}" diff --git a/fscollections/urls.py b/fscollections/urls.py index 63db75ba17..a58ab80e1c 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -3,5 +3,8 @@ urlpatterns = [ path('',views.collections, name='collections'), - path('/', views.collections, name='collections') + path('/', views.collections, name='collections'), + path('//delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), + path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), + path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index b077e60bb4..f1b0ec83e2 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -25,43 +25,101 @@ from django.urls import reverse from fscollections.models import Collection +from fscollections.forms import CollectionSoundForm from sounds.models import Sound +from sounds.views import add_sounds_modal_helper @login_required def collections(request, collection_id=None): user = request.user user_collections = Collection.objects.filter(author=user).order_by('-created') #first user collection should be on top - this would affect the if block + is_owner = False #if no collection id is provided for this URL, render the oldest collection in the webpage + #be careful when loading this url without having any collection for a user + #rn the relation user-collection only exists when you own the collection + if not collection_id: collection = user_collections.last() else: collection = get_object_or_404(Collection, id=collection_id) - + + if user == collection.author: + is_owner = True + tvars = {'collection': collection, - 'collections_for_user': user_collections} + 'collections_for_user': user_collections, + 'is_owner': is_owner} return render(request, 'collections/collections.html', tvars) -#upon creating a new user, create alongside a personal PRIVATE collection to store sounds +#NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection -def add_sound_to_collection(request, sound_id, collection_id): +def add_sound_to_collection(request, sound_id, collection_id=None): sound = get_object_or_404(Sound, id=sound_id) - collection = get_object_or_404(Collection, id=collection_id, author=request.user) + if collection_id is None: + collection = Collection.objects.filter(author=request.user).order_by("created")[0] + else: + collection = get_object_or_404(Collection, id=collection_id, author=request.user) if sound.moderation_state=='OK': collection.sounds.add(sound) collection.save() - return True + return HttpResponseRedirect(reverse("collections", args=[collection.id])) else: return "sound not moderated or not collection owner" + -def delete_sound_from_collection(request, sound_id, collection_id): +def delete_sound_from_collection(request, collection_id, sound_id): + #this should work as in Packs - select several sounds and remove them all at once from the collection + #by now it works as in Bookmarks in terms of UI sound = get_object_or_404(Sound, id=sound_id) collection = get_object_or_404(Collection, id=collection_id, author=request.user) + collection.sounds.remove(sound) + collection.save() + return HttpResponseRedirect(reverse("collections", args=[collection.id])) - if sound in collection.sound.all(): - collection.sounds.remove(sound) - collection.save() - return True - else: - return "this sound is not in the collection" \ No newline at end of file +@login_required +def get_form_for_collecting_sound(request, sound_id): + user = request.user + sound = Sound.objects.get(id=sound_id) + + try: + last_collection = \ + Collection.objects.filter(author=request.user).order_by('-created')[0] + # If user has a previous bookmark, use the same category by default (or use none if no category used in last + # bookmark) + except IndexError: + last_collection = None + + user_collections = Collection.objects.filter(author=user).order_by('-created') + form = CollectionSoundForm(initial={'collection': last_collection.id if last_collection else CollectionSoundForm.NO_COLLECTION_CHOICE_VALUE}, + prefix=sound.id, + user_collections=user_collections) + + collections_already_containing_sound = Collection.objects.filter(author=user, collection__sounds=sound).distinct() + tvars = {'user': user, + 'sound': sound, + 'last_collection': last_collection, + 'collections': user_collections, + 'form': form, + 'collections_with_sound': collections_already_containing_sound} + print("NICE CHECKPOINT") + print(tvars) + + return render(request, 'modal_collect_sound.html', tvars) + + +#NOTE: there should be two methods to add a sound into a collection +#1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal +#2: from the collection.html page through a search-engine modal as done in Packs +""" +@login_required +def add_sounds_modal_for_collection(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) + tvars = add_sounds_modal_helper(request, username=collection.author.username) + tvars.update({ + 'modal_title': 'Add sounds to Collection', + 'help_text': 'Collections are great!', + }) + return render(request, 'sounds/modal_add_sounds.html', tvars) +""" \ No newline at end of file diff --git a/sounds/forms.py b/sounds/forms.py index 59e1b846e8..ff2bc1f097 100644 --- a/sounds/forms.py +++ b/sounds/forms.py @@ -121,7 +121,7 @@ class PackEditForm(ModelForm): required=False) description = HtmlCleaningCharField(widget=forms.Textarea(attrs={'cols': 80, 'rows': 10}), help_text=HtmlCleaningCharField.make_help_text(), required=False) - + def clean_pack_sounds(self): pack_sounds = re.sub("[^0-9,]", "", self.cleaned_data['pack_sounds']) pack_sounds = re.sub(",+", ",", pack_sounds) diff --git a/templates/collections/collections.html b/templates/collections/collections.html index 0d47c6b2db..a3f8a2058f 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -13,23 +13,35 @@

Your collections

    + {% if collections_for_user %} {% for col in collections_for_user %}
  • {{col.name}} · {{col.sounds.count}} sound{{col.sounds.count|pluralize}}
  • {% endfor %} + {% else %} + You don't have any collection yet... + {% endif %}
-

{{collection.name}}

+

{{collection.name}}

+ by {{collection.author.username}} +
Description: {% if collection.description %}{{collection.description}} {% endif %}
+
{% if collection.sounds.count > 0 %} {% for sound in collection.sounds.all %}
{% display_sound_small sound %} + {% if is_owner %} + + {% endif %}
{% endfor %} - {% else %} There aren't any sounds in this collection yet {% endif %} + {% else %} + There aren't any sounds in this collection yet + {% endif %}
diff --git a/templates/collections/modal_collect_sound.html b/templates/collections/modal_collect_sound.html new file mode 100644 index 0000000000..b98dba4224 --- /dev/null +++ b/templates/collections/modal_collect_sound.html @@ -0,0 +1,47 @@ +{% extends "molecules/modal_base.html" %} +{% load static %} +{% load bw_templatetags %} + +{% block id %}collectSoundModal{% endblock %} +{% block extra-class %}{% if request.user.is_authenticated %}modal-width-60{% endif %}{% endblock %} +{% block aria-label %}Collect sound modal{% endblock %} + +
+ {% if not request.user.is_authenticated %} +
+

Can't collect sound

+
+
+
To collect sounds, you need to be logged in with your Freesound account.
+ +
+ {% elif not sound_is_moderated_and_processed_ok %} +
+

Can't collect sound

+
+
+
This sound can't be collected because it has not yet been processed or moderated.
+ +
+ {% else %} +
+

Collect Sound

+
+
+ {% if collections%} +
+ {% bw_icon 'bookmark-filled' %}This sound is already in your collections under + {% for col in collections_with_sound %} + {{col.name}}{% if not forloop.last%},{% endif %} + {% endfor %} +
+ {% endif %} +
+ {{ form }} + +
+
+ {% endif %} +
+{% endblock%} + diff --git a/templates/molecules/navbar_user_section.html b/templates/molecules/navbar_user_section.html index c5b271da34..f878f0a464 100644 --- a/templates/molecules/navbar_user_section.html +++ b/templates/molecules/navbar_user_section.html @@ -18,6 +18,9 @@ + diff --git a/templates/sounds/sound.html b/templates/sounds/sound.html index c0af6107de..24c6accc29 100644 --- a/templates/sounds/sound.html +++ b/templates/sounds/sound.html @@ -75,6 +75,11 @@

{% bw_icon 'bookmark' %} + + + From 51adc6142ce0f336330861542fc379efc08f63c1 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Tue, 21 Jan 2025 09:41:06 +0100 Subject: [PATCH 04/28] models reformulation --- bookmarks/views.py | 1 + .../src/components/collectSound.js | 105 ++++++++++++++++++ fscollections/forms.py | 4 +- .../migrations/0003_auto_20250116_1606.py | 62 +++++++++++ fscollections/models.py | 36 ++++-- fscollections/urls.py | 4 +- fscollections/views.py | 63 +++++++---- templates/collections/collections.html | 2 +- templates/sounds/sound.html | 2 +- 9 files changed, 242 insertions(+), 37 deletions(-) create mode 100644 freesound/static/bw-frontend/src/components/collectSound.js create mode 100644 fscollections/migrations/0003_auto_20250116_1606.py diff --git a/bookmarks/views.py b/bookmarks/views.py index e1a8a7a94d..67e9d78d2c 100755 --- a/bookmarks/views.py +++ b/bookmarks/views.py @@ -201,6 +201,7 @@ def get_form_for_sound(request, sound_id): sound_has_bookmark_without_category = Bookmark.objects.filter(user=request.user, sound=sound, category=None).exists() add_bookmark_url = '/'.join( request.build_absolute_uri(reverse('add-bookmark', args=[sound_id])).split('/')[:-2]) + '/' + print(add_bookmark_url) tvars = { 'bookmarks': Bookmark.objects.filter(user=request.user, sound=sound).exists(), 'sound_id': sound.id, diff --git a/freesound/static/bw-frontend/src/components/collectSound.js b/freesound/static/bw-frontend/src/components/collectSound.js new file mode 100644 index 0000000000..39b4d72d94 --- /dev/null +++ b/freesound/static/bw-frontend/src/components/collectSound.js @@ -0,0 +1,105 @@ +import {dismissModal, handleGenericModal} from "./modal"; +import {showToast} from "./toast"; +import {makePostRequest} from "../utils/postRequest"; + +const saveCollectionSound = (collectSoundUrl, data) => { + + let formData = {}; + if (data === undefined){ + formData.name = ""; + formData.collection = ""; + formData.new_collection_name = ""; + formData.use_last_collection = true; + } else { + formData = data; + } + makePostRequest(collectSoundUrl, formData, (responseText) => { + // CollectionSound saved successfully. Close model and show feedback + dismissModal(`collectSoundModal`); // TBC + try { + showToast(JSON.parse(responseText).message); + } catch (error) { + // If not logged in, the url will respond with a redirect and JSON parsing will fail + showToast("You need to be logged in before collecting sounds.") + } + }, () => { + // Unexpected errors happened while processing request: close modal and show error in toast + dismissModal(`collectSoundModal`); + showToast('Some errors occurred while collecting the sound.'); + }); +} + + +const toggleNewCollectionNameDiv = (select, newCollectionNameDiv) => { + if (select.value == '0'){ + // No category is selected, show the new category name input + newCollectionNameDiv.classList.remove('display-none'); + } else { + newCollectionNameDiv.classList.add('display-none'); + } +} + + +const initCollectSoundFormModal = (soundId, collectSoundUrl) => { + + // Modify the form structure to add a "Category" label inline with the select dropdown + const modalContainer = document.getElementById(`collectSoundModal`); + const selectElement = modalContainer.getElementsByTagName('select')[0]; + const wrapper = document.createElement('div'); + wrapper.style = 'display:inline-block;'; + if (selectElement === undefined){ + // If no select element, the modal has probably loaded for an unauthenticated user + return; + } + selectElement.parentNode.insertBefore(wrapper, selectElement.parentNode.firstChild); + const label = document.createElement('div'); + label.innerHTML = "Select a collection:" + label.classList.add('text-grey'); + wrapper.appendChild(label) + wrapper.appendChild(selectElement) + + const formElement = modalContainer.getElementsByTagName('form')[0]; + const buttonsInModalForm = formElement.getElementsByTagName('button'); + const saveButtonElement = buttonsInModalForm[buttonsInModalForm.length - 1]; + const categorySelectElement = document.getElementById(`id_${ soundId.toString() }-collection`); + const newCategoryNameElement = document.getElementById(`id_${ soundId.toString() }-new_collection_name`); + toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); + categorySelectElement.addEventListener('change', (event) => { + toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); + }); + + // Bind action to save collectionSound in "add sound to collection button" (and prevent default form submit) + saveButtonElement.addEventListener('click', (e) => { + e.preventDefault(); + const data = {}; + data.collection = document.getElementById(`id_${ soundId.toString() }-collection`).value; + data.new_collection_name = document.getElementById(`id_${ soundId.toString() }-new_collection_name`).value; + saveCollectionSound(collectSoundUrl, data); + }); +}; + +const bindCollectSoundModals = (container) => { + const collectSoundButtons = [...container.querySelectorAll('[data-toggle="collect-modal"]')]; + console.log("Collect Modal Button properly selected"); + collectSoundButtons.forEach(element => { + if (element.dataset.alreadyBinded !== undefined){ + return; + } + element.dataset.alreadyBinded = true; + element.addEventListener('click', (evt) => { + console.log("click detected") + evt.preventDefault(); + const modalUrlSplitted = element.dataset.modalUrl.split('/'); + const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 2], 10); + if (!evt.altKey) { + handleGenericModal(element.dataset.modalUrl, () => { + initCollectSoundFormModal(soundId, element.dataset.collectSoundUrl); + }, undefined, true, true); + } else { + saveCollectionSound(element.dataset.collectSoundUrl); + } + }); + }); +} + +export { bindCollectSoundModals }; diff --git a/fscollections/forms.py b/fscollections/forms.py index d3a11c2396..15191badf5 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -69,7 +69,7 @@ def save(self, *args, **kwargs): elif self.cleaned_data['collection'] == self.NEW_COLLECTION_CHOICE_VALUE: if self.cleaned_data['new_collection_name'] != "": collection = \ - Collection(author=self.user_saving_bookmark, name=self.cleaned_data['new_collection_name']) + Collection(user=self.user_saving_bookmark, name=self.cleaned_data['new_collection_name']) collection.save() collection_to_use = collection else: @@ -90,5 +90,5 @@ def save(self, *args, **kwargs): # If collection already exists, don't save it and return the existing one collection, _ = Collection.objects.get_or_create( - name = collection_to_use.name, author=self.user_saving_bookmark) + name = collection_to_use.name, user=self.user_saving_bookmark) return collection \ No newline at end of file diff --git a/fscollections/migrations/0003_auto_20250116_1606.py b/fscollections/migrations/0003_auto_20250116_1606.py new file mode 100644 index 0000000000..e6c3f16860 --- /dev/null +++ b/fscollections/migrations/0003_auto_20250116_1606.py @@ -0,0 +1,62 @@ +# Generated by Django 3.2.23 on 2025-01-16 16:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sounds', '0052_alter_sound_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('fscollections', '0002_auto_20250109_1035'), + ] + + operations = [ + migrations.RenameField( + model_name='collection', + old_name='author', + new_name='user', + ), + migrations.RemoveField( + model_name='collection', + name='sounds', + ), + migrations.AddField( + model_name='collection', + name='maintainers', + field=models.ManyToManyField(related_name='collection_maintainer', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='collection', + name='num_sounds', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='collection', + name='public', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='collection', + name='description', + field=models.TextField(), + ), + migrations.AlterField( + model_name='collection', + name='name', + field=models.CharField(max_length=255), + ), + migrations.CreateModel( + name='CollectionSound', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('status', models.CharField(choices=[('PE', 'Pending'), ('OK', 'OK'), ('DE', 'Deferred')], db_index=True, default='PE', max_length=2)), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collectionsound', to='fscollections.collection')), + ('sound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sounds.sound')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/fscollections/models.py b/fscollections/models.py index 8ef207ed69..212a040a40 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -25,22 +25,34 @@ class Collection(models.Model): - author = models.ForeignKey(User, on_delete=models.CASCADE) - name = models.CharField(max_length=128, default="BookmarkCollection") #add restrictions - sounds = models.ManyToManyField(Sound, related_name="collections") + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=255) #max_length as in Packs (128 for Bookmarks) created = models.DateTimeField(db_index=True, auto_now_add=True) - description = models.TextField(max_length=500, default="") - #NOTE: before next migration add a num_sounds attribute - #bookmarks = True or False depending on it being the BookmarkCollection for a user (the one created for the first sound without collection assigned) - #contributors = delicate stuff - #subcolletion_path = sth with tagsn and routing folders for downloads + description = models.TextField() + maintainers = models.ManyToManyField(User, related_name="collection_maintainer") + num_sounds = models.PositiveIntegerField(default=0) + public = models.BooleanField(default=False) + #NOTE: Don't fear migrations, you're just testing + #sounds are related to collections through CollectionSound model (bookmark-wise) + #contributors = delicate stuff + #subcolletion_path = sth with tagn and routing folders for downloads #follow relation for users and collections (intersted but not owner nor contributor) - #sooner or later you'll need to start using forms for adding sounds to collections xd - def __str__(self): return f"{self.name}" -''' + class CollectionSound(models.Model): - ''' \ No newline at end of file + #this model relates collections and sounds + user = models.ForeignKey(User, on_delete=models.CASCADE) #not sure bout this + sound = models.ForeignKey(Sound, on_delete=models.CASCADE) + collection = models.ForeignKey(Collection, related_name='collectionsound', on_delete=models.CASCADE) + created = models.DateTimeField(db_index=True, auto_now_add=True) + + STATUS_CHOICES = ( + ("PE", 'Pending'), + ("OK", 'OK'), + ("DE", 'Deferred'), + ) + status = models.CharField(db_index=True, max_length=2, choices=STATUS_CHOICES, default="PE") + #sound won't be added to collection until maintainers approve the sound \ No newline at end of file diff --git a/fscollections/urls.py b/fscollections/urls.py index a58ab80e1c..66289c285c 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -2,8 +2,8 @@ from . import views urlpatterns = [ - path('',views.collections, name='collections'), - path('/', views.collections, name='collections'), + path('',views.collections_for_user, name='collections'), + path('/', views.collections_for_user, name='collections'), path('//delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound") diff --git a/fscollections/views.py b/fscollections/views.py index f1b0ec83e2..5f7311fc8e 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -19,85 +19,110 @@ # from django.contrib.auth.decorators import login_required +from django.contrib import messages from django.db import transaction from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse -from fscollections.models import Collection +from fscollections.models import Collection, CollectionSound from fscollections.forms import CollectionSoundForm from sounds.models import Sound from sounds.views import add_sounds_modal_helper @login_required -def collections(request, collection_id=None): +def collections_for_user(request, collection_id=None): user = request.user - user_collections = Collection.objects.filter(author=user).order_by('-created') #first user collection should be on top - this would affect the if block + user_collections = Collection.objects.filter(user=user).order_by('-created') is_owner = False - #if no collection id is provided for this URL, render the oldest collection in the webpage + #if no collection id is provided for this URL, render the oldest collection #be careful when loading this url without having any collection for a user - #rn the relation user-collection only exists when you own the collection - + #only show the collections for which you're the user(owner) + if not collection_id: collection = user_collections.last() else: collection = get_object_or_404(Collection, id=collection_id) - if user == collection.author: + if user == collection.user: is_owner = True tvars = {'collection': collection, 'collections_for_user': user_collections, 'is_owner': is_owner} - + return render(request, 'collections/collections.html', tvars) #NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection def add_sound_to_collection(request, sound_id, collection_id=None): + #this view from now on should create a new CollectionSound object instead of adding + #a sound to collection.sounds sound = get_object_or_404(Sound, id=sound_id) + msg_to_return = '' + if request.method == 'POST': + print('good job') + #by now work with direct additions (only user-wise, not maintainer-wise) + user_collections = Collection.objects.filter(user=request.user) + # NOTE: aquí ens hem quedat de moment xd + # form = CollectionSoundForm() + if request.is_ajax(): + return msg_to_return + else: + messages.add_message(request, messages.WARNING, msg_to_return) + next = request.GET.get("next", "") + print("NEXT VALUE",next) + if next: + return HttpResponseRedirect(next) + else: + return HttpResponseRedirect(reverse("sound", args=[sound.user.username, sound.id])) + """ if collection_id is None: - collection = Collection.objects.filter(author=request.user).order_by("created")[0] + collection = Collection.objects.filter(user=request.user).order_by("created")[0] else: - collection = get_object_or_404(Collection, id=collection_id, author=request.user) - + collection = get_object_or_404(Collection, id=collection_id, user=request.user) + """ + #the creation of the SoundCollection object should be done through a form + if sound.moderation_state=='OK': collection.sounds.add(sound) collection.save() return HttpResponseRedirect(reverse("collections", args=[collection.id])) else: - return "sound not moderated or not collection owner" + return HttpResponseRedirect(reverse("sound", args=[sound_id])) def delete_sound_from_collection(request, collection_id, sound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection #by now it works as in Bookmarks in terms of UI sound = get_object_or_404(Sound, id=sound_id) - collection = get_object_or_404(Collection, id=collection_id, author=request.user) + collection = get_object_or_404(Collection, id=collection_id, user=request.user) collection.sounds.remove(sound) collection.save() return HttpResponseRedirect(reverse("collections", args=[collection.id])) @login_required def get_form_for_collecting_sound(request, sound_id): - user = request.user + sound = Sound.objects.get(id=sound_id) try: last_collection = \ - Collection.objects.filter(author=request.user).order_by('-created')[0] + Collection.objects.filter(user=request.user).order_by('-created')[0] # If user has a previous bookmark, use the same category by default (or use none if no category used in last # bookmark) except IndexError: last_collection = None - user_collections = Collection.objects.filter(author=user).order_by('-created') + user_collections = Collection.objects.filter(user=request.user).order_by('-created') form = CollectionSoundForm(initial={'collection': last_collection.id if last_collection else CollectionSoundForm.NO_COLLECTION_CHOICE_VALUE}, prefix=sound.id, user_collections=user_collections) - collections_already_containing_sound = Collection.objects.filter(author=user, collection__sounds=sound).distinct() - tvars = {'user': user, + collections_already_containing_sound = Collection.objects.filter(user=request. user, collection__sounds=sound).distinct() + # add_bookmark_url = '/'.join( + # request.build_absolute_uri(reverse('add-bookmark', args=[sound_id])).split('/')[:-2]) + '/' + tvars = {'user': request.user, 'sound': sound, 'last_collection': last_collection, 'collections': user_collections, @@ -116,7 +141,7 @@ def get_form_for_collecting_sound(request, sound_id): @login_required def add_sounds_modal_for_collection(request, collection_id): collection = get_object_or_404(Collection, id=collection_id) - tvars = add_sounds_modal_helper(request, username=collection.author.username) + tvars = add_sounds_modal_helper(request, username=collection.user.username) tvars.update({ 'modal_title': 'Add sounds to Collection', 'help_text': 'Collections are great!', diff --git a/templates/collections/collections.html b/templates/collections/collections.html index a3f8a2058f..1a7d813720 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -26,7 +26,7 @@

Your collections

{{collection.name}}

- by
{{collection.author.username}} + by {{collection.user.username}}
Description: {% if collection.description %}{{collection.description}} {% endif %}
diff --git a/templates/sounds/sound.html b/templates/sounds/sound.html index 24c6accc29..fe2adf3480 100644 --- a/templates/sounds/sound.html +++ b/templates/sounds/sound.html @@ -76,7 +76,7 @@

- From 2780959c9771e50e63768d8957518238654605c8 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 23 Jan 2025 09:30:32 +0100 Subject: [PATCH 05/28] add sound modal behavior tbd --- bookmarks/views.py | 1 - .../src/components/collectSound.js | 11 ++++++-- .../bw-frontend/src/components/modal.js | 1 + .../bw-frontend/src/utils/initHelper.js | 2 ++ fscollections/forms.py | 5 ++-- fscollections/views.py | 28 ++++++------------- .../collections/modal_collect_sound.html | 7 +++-- 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/bookmarks/views.py b/bookmarks/views.py index 67e9d78d2c..e1a8a7a94d 100755 --- a/bookmarks/views.py +++ b/bookmarks/views.py @@ -201,7 +201,6 @@ def get_form_for_sound(request, sound_id): sound_has_bookmark_without_category = Bookmark.objects.filter(user=request.user, sound=sound, category=None).exists() add_bookmark_url = '/'.join( request.build_absolute_uri(reverse('add-bookmark', args=[sound_id])).split('/')[:-2]) + '/' - print(add_bookmark_url) tvars = { 'bookmarks': Bookmark.objects.filter(user=request.user, sound=sound).exists(), 'sound_id': sound.id, diff --git a/freesound/static/bw-frontend/src/components/collectSound.js b/freesound/static/bw-frontend/src/components/collectSound.js index 39b4d72d94..1ab6bdff48 100644 --- a/freesound/static/bw-frontend/src/components/collectSound.js +++ b/freesound/static/bw-frontend/src/components/collectSound.js @@ -43,14 +43,16 @@ const toggleNewCollectionNameDiv = (select, newCollectionNameDiv) => { const initCollectSoundFormModal = (soundId, collectSoundUrl) => { // Modify the form structure to add a "Category" label inline with the select dropdown - const modalContainer = document.getElementById(`collectSoundModal`); + const modalContainer = document.getElementById('collectSoundModal'); const selectElement = modalContainer.getElementsByTagName('select')[0]; const wrapper = document.createElement('div'); wrapper.style = 'display:inline-block;'; if (selectElement === undefined){ // If no select element, the modal has probably loaded for an unauthenticated user + console.log("select element is undefined") return; } + console.log("SELECT ELEMENT", selectElement); selectElement.parentNode.insertBefore(wrapper, selectElement.parentNode.firstChild); const label = document.createElement('div'); label.innerHTML = "Select a collection:" @@ -63,6 +65,8 @@ const initCollectSoundFormModal = (soundId, collectSoundUrl) => { const saveButtonElement = buttonsInModalForm[buttonsInModalForm.length - 1]; const categorySelectElement = document.getElementById(`id_${ soundId.toString() }-collection`); const newCategoryNameElement = document.getElementById(`id_${ soundId.toString() }-new_collection_name`); + console.log("CATEGORY SELECT ELEMENT: ", categorySelectElement); + console.log("NEW CATEGORY NAME ELEMENT: ", newCategoryNameElement); toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); categorySelectElement.addEventListener('change', (event) => { toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); @@ -80,18 +84,19 @@ const initCollectSoundFormModal = (soundId, collectSoundUrl) => { const bindCollectSoundModals = (container) => { const collectSoundButtons = [...container.querySelectorAll('[data-toggle="collect-modal"]')]; - console.log("Collect Modal Button properly selected"); collectSoundButtons.forEach(element => { if (element.dataset.alreadyBinded !== undefined){ return; } element.dataset.alreadyBinded = true; element.addEventListener('click', (evt) => { - console.log("click detected") evt.preventDefault(); const modalUrlSplitted = element.dataset.modalUrl.split('/'); const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 2], 10); if (!evt.altKey) { + console.log("MODAL URL", element.dataset.modalUrl); + console.log("COLLECT SOUND URL", element.dataset.collectSoundUrl); + console.log("SOUND ID ", soundId); handleGenericModal(element.dataset.modalUrl, () => { initCollectSoundFormModal(soundId, element.dataset.collectSoundUrl); }, undefined, true, true); diff --git a/freesound/static/bw-frontend/src/components/modal.js b/freesound/static/bw-frontend/src/components/modal.js index ddaabb37b1..06d43def80 100644 --- a/freesound/static/bw-frontend/src/components/modal.js +++ b/freesound/static/bw-frontend/src/components/modal.js @@ -162,6 +162,7 @@ const handleGenericModal = (fetchContentUrl, onLoadedCallback, onClosedCallback, if (showLoadingToast !== false) { dismissToast(); } // Call default initialization function, and also call callback if defined + console.log("MODAL CONTAINER", modalContainer) initializeStuffInContainer(modalContainer, false, false); if (onLoadedCallback !== undefined){ onLoadedCallback(modalContainer); diff --git a/freesound/static/bw-frontend/src/utils/initHelper.js b/freesound/static/bw-frontend/src/utils/initHelper.js index fbafdd40c4..e14deb33dd 100644 --- a/freesound/static/bw-frontend/src/utils/initHelper.js +++ b/freesound/static/bw-frontend/src/utils/initHelper.js @@ -20,6 +20,7 @@ import { makeSelect } from '../components/select.js'; import { makeTextareaCharacterCounter } from '../components/textareaCharactersCounter.js'; import { bindUnsecureImageCheckListeners } from '../components/unsecureImageCheck.js'; import { initMap } from '../pages/map.js'; +import { bindCollectSoundModals } from '../components/collectSound.js'; const initializeStuffInContainer = (container, bindModals, activateModals) => { @@ -53,6 +54,7 @@ const initializeStuffInContainer = (container, bindModals, activateModals) => { bindRemixGroupModals(container); bindBookmarkSoundModals(container); bindUserAnnotationsModal(container); + bindCollectSoundModals(container); } // Activate modals if needed (this should only be used the first time initializeStuffInContainer is called) diff --git a/fscollections/forms.py b/fscollections/forms.py index 15191badf5..1cb62c5683 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -56,9 +56,8 @@ def __init__(self, *args, **kwargs): if self.user_collections else[]) self.fields['new_collection_name'].widget.attrs['placeholder'] = "Fill in the name for the new collection" - self.fields['category'].widget.attrs = { - 'data-grey-items': f'{self.NO_CATEGORY_CHOICE_VALUE},{self.NEW_CATEGORY_CHOICE_VALUE}'} - #i don't fully understand what this last line of code does + self.fields['collection'].widget.attrs = { + 'data-grey-items': f'{self.NO_COLLECTION_CHOICE_VALUE},{self.NEW_COLLECTION_CHOICE_VALUE}'} def save(self, *args, **kwargs): collection_to_use = None diff --git a/fscollections/views.py b/fscollections/views.py index 5f7311fc8e..2ee2dd91d1 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -60,6 +60,7 @@ def add_sound_to_collection(request, sound_id, collection_id=None): #a sound to collection.sounds sound = get_object_or_404(Sound, id=sound_id) msg_to_return = '' + print("enter add sound") if request.method == 'POST': print('good job') #by now work with direct additions (only user-wise, not maintainer-wise) @@ -67,6 +68,7 @@ def add_sound_to_collection(request, sound_id, collection_id=None): # NOTE: aquí ens hem quedat de moment xd # form = CollectionSoundForm() if request.is_ajax(): + print("SECOND CASE") return msg_to_return else: messages.add_message(request, messages.WARNING, msg_to_return) @@ -76,20 +78,6 @@ def add_sound_to_collection(request, sound_id, collection_id=None): return HttpResponseRedirect(next) else: return HttpResponseRedirect(reverse("sound", args=[sound.user.username, sound.id])) - """ - if collection_id is None: - collection = Collection.objects.filter(user=request.user).order_by("created")[0] - else: - collection = get_object_or_404(Collection, id=collection_id, user=request.user) - """ - #the creation of the SoundCollection object should be done through a form - - if sound.moderation_state=='OK': - collection.sounds.add(sound) - collection.save() - return HttpResponseRedirect(reverse("collections", args=[collection.id])) - else: - return HttpResponseRedirect(reverse("sound", args=[sound_id])) def delete_sound_from_collection(request, collection_id, sound_id): @@ -119,19 +107,21 @@ def get_form_for_collecting_sound(request, sound_id): prefix=sound.id, user_collections=user_collections) - collections_already_containing_sound = Collection.objects.filter(user=request. user, collection__sounds=sound).distinct() - # add_bookmark_url = '/'.join( - # request.build_absolute_uri(reverse('add-bookmark', args=[sound_id])).split('/')[:-2]) + '/' + collections_already_containing_sound = Collection.objects.filter(user=request.user, collectionsound__sound__id=sound.id).distinct() + # collect_sound_url = '/'.join( + # request.build_absolute_uri(reverse('add-sound-to-collection', args=[sound_id])).split('/')[:-2]) + '/' tvars = {'user': request.user, 'sound': sound, + 'sound_is_moderated_and_processed_ok': sound.moderated_and_processed_ok, 'last_collection': last_collection, 'collections': user_collections, 'form': form, - 'collections_with_sound': collections_already_containing_sound} + 'collections_with_sound': collections_already_containing_sound + } print("NICE CHECKPOINT") print(tvars) - return render(request, 'modal_collect_sound.html', tvars) + return render(request, 'collections/modal_collect_sound.html', tvars) #NOTE: there should be two methods to add a sound into a collection diff --git a/templates/collections/modal_collect_sound.html b/templates/collections/modal_collect_sound.html index b98dba4224..a1ab4c168f 100644 --- a/templates/collections/modal_collect_sound.html +++ b/templates/collections/modal_collect_sound.html @@ -6,6 +6,7 @@ {% block extra-class %}{% if request.user.is_authenticated %}modal-width-60{% endif %}{% endblock %} {% block aria-label %}Collect sound modal{% endblock %} +{% block body %}
{% if not request.user.is_authenticated %}
@@ -25,10 +26,10 @@

Can't collect sound

{% else %}
-

Collect Sound

+

Add sound to collection

- {% if collections%} + {% if collections_with_sound%}
{% bw_icon 'bookmark-filled' %}This sound is already in your collections under {% for col in collections_with_sound %} @@ -43,5 +44,5 @@

Collect Sound

{% endif %}
-{% endblock%} +{% endblock %} From 4c7c1907c8f3be09fe93a3d084173df1a44688b9 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 23 Jan 2025 11:34:34 +0100 Subject: [PATCH 06/28] add sound modal collection selector --- freesound/static/bw-frontend/src/components/collectSound.js | 3 --- freesound/static/bw-frontend/src/components/modal.js | 1 - fscollections/forms.py | 3 ++- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freesound/static/bw-frontend/src/components/collectSound.js b/freesound/static/bw-frontend/src/components/collectSound.js index 1ab6bdff48..e909954270 100644 --- a/freesound/static/bw-frontend/src/components/collectSound.js +++ b/freesound/static/bw-frontend/src/components/collectSound.js @@ -94,9 +94,6 @@ const bindCollectSoundModals = (container) => { const modalUrlSplitted = element.dataset.modalUrl.split('/'); const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 2], 10); if (!evt.altKey) { - console.log("MODAL URL", element.dataset.modalUrl); - console.log("COLLECT SOUND URL", element.dataset.collectSoundUrl); - console.log("SOUND ID ", soundId); handleGenericModal(element.dataset.modalUrl, () => { initCollectSoundFormModal(soundId, element.dataset.collectSoundUrl); }, undefined, true, true); diff --git a/freesound/static/bw-frontend/src/components/modal.js b/freesound/static/bw-frontend/src/components/modal.js index 06d43def80..ddaabb37b1 100644 --- a/freesound/static/bw-frontend/src/components/modal.js +++ b/freesound/static/bw-frontend/src/components/modal.js @@ -162,7 +162,6 @@ const handleGenericModal = (fetchContentUrl, onLoadedCallback, onClosedCallback, if (showLoadingToast !== false) { dismissToast(); } // Call default initialization function, and also call callback if defined - console.log("MODAL CONTAINER", modalContainer) initializeStuffInContainer(modalContainer, false, false); if (onLoadedCallback !== undefined){ onLoadedCallback(modalContainer); diff --git a/fscollections/forms.py b/fscollections/forms.py index 1cb62c5683..991587e69d 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -34,9 +34,10 @@ class CollectionSoundForm(forms.Form): choices=[], required=True) - new_collection_name = forms.ChoiceField( + new_collection_name = forms.CharField( label = False, help_text=None, + max_length = 128, required = False) use_last_collection = forms.BooleanField(widget=forms.HiddenInput(), required=False, initial=False) From 0f470907fdb3d575bdb927b6184db8c32e6eca06 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 23 Jan 2025 13:18:41 +0100 Subject: [PATCH 07/28] add sound to col from sound url --- fscollections/forms.py | 8 ++++---- fscollections/models.py | 2 ++ fscollections/views.py | 28 +++++++++++++++++--------- templates/collections/collections.html | 18 +++++++++-------- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/fscollections/forms.py b/fscollections/forms.py index 991587e69d..9779fd1dc5 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -19,7 +19,7 @@ # from django import forms -from fscollections.models import Collection +from fscollections.models import Collection, CollectionSound #this class was aimed to perform similarly to BookmarkSound, however, at first the method to add a sound to a collection #will be opening the search engine in a modal, looking for a sound in there and adding it to the actual collection page @@ -69,7 +69,7 @@ def save(self, *args, **kwargs): elif self.cleaned_data['collection'] == self.NEW_COLLECTION_CHOICE_VALUE: if self.cleaned_data['new_collection_name'] != "": collection = \ - Collection(user=self.user_saving_bookmark, name=self.cleaned_data['new_collection_name']) + Collection(user=self.user_saving_sound, name=self.cleaned_data['new_collection_name']) collection.save() collection_to_use = collection else: @@ -80,7 +80,7 @@ def save(self, *args, **kwargs): # SI -> per defecte es posa a BookmarksCollection try: last_user_collection = \ - Collection.objects.filter(user=self.user_saving_bookmark).order_by('-created')[0] + Collection.objects.filter(user=self.user_saving_sound).order_by('-created')[0] # If user has a previous bookmark, use the same category (or use none if no category used in last # bookmark) collection_to_use = last_user_collection @@ -90,5 +90,5 @@ def save(self, *args, **kwargs): # If collection already exists, don't save it and return the existing one collection, _ = Collection.objects.get_or_create( - name = collection_to_use.name, user=self.user_saving_bookmark) + name = collection_to_use.name, user=self.user_saving_sound) return collection \ No newline at end of file diff --git a/fscollections/models.py b/fscollections/models.py index 212a040a40..2b70b2ad1c 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -44,6 +44,8 @@ def __str__(self): class CollectionSound(models.Model): #this model relates collections and sounds + #it might be worth adding a name field composed of the sound ID and the collection name for + # for the sake of queries understanding user = models.ForeignKey(User, on_delete=models.CASCADE) #not sure bout this sound = models.ForeignKey(Sound, on_delete=models.CASCADE) collection = models.ForeignKey(Collection, related_name='collectionsound', on_delete=models.CASCADE) diff --git a/fscollections/views.py b/fscollections/views.py index 2ee2dd91d1..39d645514e 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -18,6 +18,7 @@ # See AUTHORS file. # +from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db import transaction @@ -29,6 +30,8 @@ from fscollections.forms import CollectionSoundForm from sounds.models import Sound from sounds.views import add_sounds_modal_helper +from utils.pagination import paginate + @login_required def collections_for_user(request, collection_id=None): @@ -47,9 +50,16 @@ def collections_for_user(request, collection_id=None): if user == collection.user: is_owner = True + tvars = {'collection': collection, 'collections_for_user': user_collections, 'is_owner': is_owner} + + collection_sounds = CollectionSound.objects.filter(collection=collection) + paginator = paginate(request, collection_sounds, settings.BOOKMARKS_PER_PAGE) + page_sounds = Sound.objects.ordered_ids([col_sound.sound_id for col_sound in paginator['page'].object_list]) + tvars.update(paginator) + tvars['page_collection_and_sound_objects'] = zip(paginator['page'].object_list, page_sounds) return render(request, 'collections/collections.html', tvars) @@ -60,20 +70,22 @@ def add_sound_to_collection(request, sound_id, collection_id=None): #a sound to collection.sounds sound = get_object_or_404(Sound, id=sound_id) msg_to_return = '' - print("enter add sound") if request.method == 'POST': - print('good job') #by now work with direct additions (only user-wise, not maintainer-wise) user_collections = Collection.objects.filter(user=request.user) - # NOTE: aquí ens hem quedat de moment xd - # form = CollectionSoundForm() + form = CollectionSoundForm(request.POST, sound_id=sound_id, user_collections=user_collections, user_saving_sound=request.user) + if form.is_valid(): + saved_collection = form.save() + CollectionSound(user=request.user, collection=saved_collection, sound=sound, status="OK").save() + saved_collection.num_sounds =+ 1 #this should be done with a signal/method in Collection models + saved_collection.save() + msg_to_return = f'Sound {sound.original_filename} saved under collection {saved_collection.name}' + if request.is_ajax(): - print("SECOND CASE") - return msg_to_return + return JsonResponse({'message': msg_to_return}) else: messages.add_message(request, messages.WARNING, msg_to_return) next = request.GET.get("next", "") - print("NEXT VALUE",next) if next: return HttpResponseRedirect(next) else: @@ -118,8 +130,6 @@ def get_form_for_collecting_sound(request, sound_id): 'form': form, 'collections_with_sound': collections_already_containing_sound } - print("NICE CHECKPOINT") - print(tvars) return render(request, 'collections/modal_collect_sound.html', tvars) diff --git a/templates/collections/collections.html b/templates/collections/collections.html index 1a7d813720..8b197d96f8 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -16,7 +16,7 @@

Your collections

{% if collections_for_user %} {% for col in collections_for_user %}
  • {{col.name}} - · {{col.sounds.count}} sound{{col.sounds.count|pluralize}}
  • + · {{col.num_sounds}} sound{{col.num_sounds|pluralize}} {% endfor %} {% else %} You don't have any collection yet... @@ -28,10 +28,10 @@

    Your collections

    {{collection.name}}

    by {{collection.user.username}}
    Description: {% if collection.description %}{{collection.description}} {% endif %}
    - -
    - {% if collection.sounds.count > 0 %} - {% for sound in collection.sounds.all %} +
    + {% if page.object_list %} +
    + {% for collection, sound in page_collection_and_sound_objects %}
    {% display_sound_small sound %} {% if is_owner %} @@ -39,9 +39,11 @@

    {{collection.name}}

    {% endif %}
    {% endfor %} - {% else %} - There aren't any sounds in this collection yet - {% endif %} +
    + {% else %} + There aren't any sounds in this collection yet + {% endif %} + {% bw_paginator paginator page current_page request "bookmark" %}
    From fc55074d8661d6b9b76cac55e93fc71f7f5a4243 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 23 Jan 2025 15:18:07 +0100 Subject: [PATCH 08/28] delete sound from collection func. --- fscollections/views.py | 7 ++++--- templates/collections/collections.html | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fscollections/views.py b/fscollections/views.py index 39d645514e..5a07660690 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -60,7 +60,6 @@ def collections_for_user(request, collection_id=None): page_sounds = Sound.objects.ordered_ids([col_sound.sound_id for col_sound in paginator['page'].object_list]) tvars.update(paginator) tvars['page_collection_and_sound_objects'] = zip(paginator['page'].object_list, page_sounds) - return render(request, 'collections/collections.html', tvars) #NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection @@ -96,9 +95,11 @@ def delete_sound_from_collection(request, collection_id, sound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection #by now it works as in Bookmarks in terms of UI sound = get_object_or_404(Sound, id=sound_id) + print(collection_id) + print(request.user.username) collection = get_object_or_404(Collection, id=collection_id, user=request.user) - collection.sounds.remove(sound) - collection.save() + collection_sound = CollectionSound.objects.get(sound=sound, collection=collection) + collection_sound.delete() return HttpResponseRedirect(reverse("collections", args=[collection.id])) @login_required diff --git a/templates/collections/collections.html b/templates/collections/collections.html index 8b197d96f8..9d604d6d38 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -31,11 +31,11 @@

    {{collection.name}}

    {% if page.object_list %}
    - {% for collection, sound in page_collection_and_sound_objects %} + {% for collectionsound, sound in page_collection_and_sound_objects %}
    {% display_sound_small sound %} {% if is_owner %} - + {% endif %}
    {% endfor %} @@ -43,7 +43,7 @@

    {{collection.name}}

    {% else %} There aren't any sounds in this collection yet {% endif %} - {% bw_paginator paginator page current_page request "bookmark" %} + {% bw_paginator paginator page current_page request "collection" %}

    From b2e02776337b27e7794b3c6020dfe9aa45d1ac21 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Fri, 24 Jan 2025 14:26:30 +0100 Subject: [PATCH 09/28] deletion for CollectionSound + restrict add duplicates --- fscollections/forms.py | 22 ++++++++++++--- fscollections/urls.py | 2 +- fscollections/views.py | 37 +++++++++++++------------- templates/collections/collections.html | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/fscollections/forms.py b/fscollections/forms.py index 9779fd1dc5..bc0005fcf4 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -42,6 +42,7 @@ class CollectionSoundForm(forms.Form): use_last_collection = forms.BooleanField(widget=forms.HiddenInput(), required=False, initial=False) user_collections = None + user_available_collections = None NO_COLLECTION_CHOICE_VALUE = '-1' NEW_COLLECTION_CHOICE_VALUE = '0' @@ -50,10 +51,15 @@ def __init__(self, *args, **kwargs): self.user_collections = kwargs.pop('user_collections', False) self.user_saving_sound = kwargs.pop('user_saving_sound', False) self.sound_id = kwargs.pop('sound_id', False) + if self.user_collections: + self.user_available_collections = Collection.objects.filter(id__in=self.user_collections).exclude(collectionsound__sound__id=self.sound_id) + print(self.user_available_collections) + print(self.sound_id) + print(self.user_collections) super().__init__(*args, **kwargs) self.fields['collection'].choices = [(self.NO_COLLECTION_CHOICE_VALUE, '--- No collection ---'),#in this case this goes to bookmarks collection (might have to be created) (self.NEW_COLLECTION_CHOICE_VALUE, 'Create a new collection...')] + \ - ([(collection.id, collection.name) for collection in self.user_collections] + ([(collection.id, collection.name) for collection in self.user_available_collections ] if self.user_collections else[]) self.fields['new_collection_name'].widget.attrs['placeholder'] = "Fill in the name for the new collection" @@ -86,9 +92,17 @@ def save(self, *args, **kwargs): collection_to_use = last_user_collection except IndexError: # This is first bookmark of the user - pass - + pass # If collection already exists, don't save it and return the existing one collection, _ = Collection.objects.get_or_create( name = collection_to_use.name, user=self.user_saving_sound) - return collection \ No newline at end of file + return collection + + def clean(self): + collection = self.cleaned_data['collection'] + sound = self.sound_id + if CollectionSound.objects.filter(collection=collection,sound=sound).exists(): + raise forms.ValidationError("This sound already exists in the collection") + + return super().clean() + \ No newline at end of file diff --git a/fscollections/urls.py b/fscollections/urls.py index 66289c285c..5dda996a1f 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -4,7 +4,7 @@ urlpatterns = [ path('',views.collections_for_user, name='collections'), path('/', views.collections_for_user, name='collections'), - path('//delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), + path('/delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index 5a07660690..af78252951 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -64,41 +64,41 @@ def collections_for_user(request, collection_id=None): #NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection -def add_sound_to_collection(request, sound_id, collection_id=None): +def add_sound_to_collection(request, sound_id): #this view from now on should create a new CollectionSound object instead of adding #a sound to collection.sounds + # TODO: add restrictions for sound repetition and for user being owner/maintainer sound = get_object_or_404(Sound, id=sound_id) msg_to_return = '' + + if not request.GET.get('ajax'): + HttpResponseRedirect(reverse("sound", args=[sound.user.username,sound.id])) + if request.method == 'POST': #by now work with direct additions (only user-wise, not maintainer-wise) user_collections = Collection.objects.filter(user=request.user) form = CollectionSoundForm(request.POST, sound_id=sound_id, user_collections=user_collections, user_saving_sound=request.user) + if form.is_valid(): saved_collection = form.save() + # TODO: moderation of CollectionSounds to be accounted for users who are neither maintainers nor owners CollectionSound(user=request.user, collection=saved_collection, sound=sound, status="OK").save() saved_collection.num_sounds =+ 1 #this should be done with a signal/method in Collection models saved_collection.save() - msg_to_return = f'Sound {sound.original_filename} saved under collection {saved_collection.name}' - - if request.is_ajax(): - return JsonResponse({'message': msg_to_return}) - else: - messages.add_message(request, messages.WARNING, msg_to_return) - next = request.GET.get("next", "") - if next: - return HttpResponseRedirect(next) + msg_to_return = f'Sound "{sound.original_filename}" saved under collection {saved_collection.name}' + return JsonResponse('message', msg_to_return) else: - return HttpResponseRedirect(reverse("sound", args=[sound.user.username, sound.id])) - + msg_to_return = 'This sound already exists in this category' + return JsonResponse('message', msg_to_return) + -def delete_sound_from_collection(request, collection_id, sound_id): +def delete_sound_from_collection(request, collectionsound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection #by now it works as in Bookmarks in terms of UI - sound = get_object_or_404(Sound, id=sound_id) - print(collection_id) - print(request.user.username) - collection = get_object_or_404(Collection, id=collection_id, user=request.user) - collection_sound = CollectionSound.objects.get(sound=sound, collection=collection) + #TODO: this should be done through a POST request method, would be easier to send CollectionSound ID and delete it directly + # this would save up a query + collection_sound = get_object_or_404(CollectionSound, id=collectionsound_id) + collection = collection_sound.collection collection_sound.delete() return HttpResponseRedirect(reverse("collections", args=[collection.id])) @@ -117,6 +117,7 @@ def get_form_for_collecting_sound(request, sound_id): user_collections = Collection.objects.filter(user=request.user).order_by('-created') form = CollectionSoundForm(initial={'collection': last_collection.id if last_collection else CollectionSoundForm.NO_COLLECTION_CHOICE_VALUE}, + sound_id=sound.id, prefix=sound.id, user_collections=user_collections) diff --git a/templates/collections/collections.html b/templates/collections/collections.html index 9d604d6d38..f051b18401 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -35,7 +35,7 @@

    {{collection.name}}

    {% display_sound_small sound %} {% if is_owner %} - + {% endif %}
    {% endfor %} From 5ba088d8b53d1a68a97e6170b77bdd53c608a447 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Fri, 24 Jan 2025 15:04:33 +0100 Subject: [PATCH 10/28] minor details correction --- fscollections/forms.py | 15 ++++----------- fscollections/views.py | 15 ++++++--------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/fscollections/forms.py b/fscollections/forms.py index bc0005fcf4..0b4d4066ed 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -51,16 +51,15 @@ def __init__(self, *args, **kwargs): self.user_collections = kwargs.pop('user_collections', False) self.user_saving_sound = kwargs.pop('user_saving_sound', False) self.sound_id = kwargs.pop('sound_id', False) + if self.user_collections: self.user_available_collections = Collection.objects.filter(id__in=self.user_collections).exclude(collectionsound__sound__id=self.sound_id) - print(self.user_available_collections) - print(self.sound_id) - print(self.user_collections) + super().__init__(*args, **kwargs) self.fields['collection'].choices = [(self.NO_COLLECTION_CHOICE_VALUE, '--- No collection ---'),#in this case this goes to bookmarks collection (might have to be created) (self.NEW_COLLECTION_CHOICE_VALUE, 'Create a new collection...')] + \ ([(collection.id, collection.name) for collection in self.user_available_collections ] - if self.user_collections else[]) + if self.user_available_collections else[]) self.fields['new_collection_name'].widget.attrs['placeholder'] = "Fill in the name for the new collection" self.fields['collection'].widget.attrs = { @@ -80,18 +79,12 @@ def save(self, *args, **kwargs): collection_to_use = collection else: collection_to_use = Collection.objects.get(id=self.cleaned_data['collection']) - else: #en aquest cas - SÍ estem fent servir l'última coleccio, NO estem creant una nova coleccio, NO estem agafant una coleccio existent i - # per tant ens trobem en un cas de NO COLLECTION CHOICE VALUE (no s'ha triat cap coleccio) - # si no es tria cap coleccio: l'usuari té alguna colecció? NO -> creem BookmarksCollection pels seus sons privats - # SI -> per defecte es posa a BookmarksCollection + else: try: last_user_collection = \ Collection.objects.filter(user=self.user_saving_sound).order_by('-created')[0] - # If user has a previous bookmark, use the same category (or use none if no category used in last - # bookmark) collection_to_use = last_user_collection except IndexError: - # This is first bookmark of the user pass # If collection already exists, don't save it and return the existing one collection, _ = Collection.objects.get_or_create( diff --git a/fscollections/views.py b/fscollections/views.py index af78252951..28b0600d2f 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -38,9 +38,9 @@ def collections_for_user(request, collection_id=None): user = request.user user_collections = Collection.objects.filter(user=user).order_by('-created') is_owner = False - #if no collection id is provided for this URL, render the oldest collection - #be careful when loading this url without having any collection for a user - #only show the collections for which you're the user(owner) + # if no collection id is provided for this URL, render the oldest collection + # be careful when loading this url without having any collection for a user + # only show the collections for which you're the user(owner) if not collection_id: collection = user_collections.last() @@ -65,8 +65,8 @@ def collections_for_user(request, collection_id=None): #NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection def add_sound_to_collection(request, sound_id): - #this view from now on should create a new CollectionSound object instead of adding - #a sound to collection.sounds + # this view from now on should create a new CollectionSound object instead of adding + # a sound to collection.sounds # TODO: add restrictions for sound repetition and for user being owner/maintainer sound = get_object_or_404(Sound, id=sound_id) msg_to_return = '' @@ -95,8 +95,7 @@ def add_sound_to_collection(request, sound_id): def delete_sound_from_collection(request, collectionsound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection #by now it works as in Bookmarks in terms of UI - #TODO: this should be done through a POST request method, would be easier to send CollectionSound ID and delete it directly - # this would save up a query + #TODO: this should be done through a POST request method collection_sound = get_object_or_404(CollectionSound, id=collectionsound_id) collection = collection_sound.collection collection_sound.delete() @@ -122,8 +121,6 @@ def get_form_for_collecting_sound(request, sound_id): user_collections=user_collections) collections_already_containing_sound = Collection.objects.filter(user=request.user, collectionsound__sound__id=sound.id).distinct() - # collect_sound_url = '/'.join( - # request.build_absolute_uri(reverse('add-sound-to-collection', args=[sound_id])).split('/')[:-2]) + '/' tvars = {'user': request.user, 'sound': sound, 'sound_is_moderated_and_processed_ok': sound.moderated_and_processed_ok, From ae9950bcdda7deebd7dd774c2f7ae2597d99b35f Mon Sep 17 00:00:00 2001 From: quimmrc Date: Mon, 27 Jan 2025 20:04:44 +0100 Subject: [PATCH 11/28] delete and create collection functionalities --- fscollections/forms.py | 17 ++++++- fscollections/urls.py | 4 +- fscollections/views.py | 24 ++++++++-- templates/collections/collections.html | 7 ++- templates/collections/edit_collection.html | 48 +++++++++++++++++++ .../collections/modal_collect_sound.html | 2 +- templates/sounds/sound.html | 1 - 7 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 templates/collections/edit_collection.html diff --git a/fscollections/forms.py b/fscollections/forms.py index 0b4d4066ed..0812b37f42 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -19,7 +19,9 @@ # from django import forms +from django.forms import ModelForm, Textarea, TextInput from fscollections.models import Collection, CollectionSound +from utils.forms import HtmlCleaningCharField #this class was aimed to perform similarly to BookmarkSound, however, at first the method to add a sound to a collection #will be opening the search engine in a modal, looking for a sound in there and adding it to the actual collection page @@ -55,6 +57,7 @@ def __init__(self, *args, **kwargs): if self.user_collections: self.user_available_collections = Collection.objects.filter(id__in=self.user_collections).exclude(collectionsound__sound__id=self.sound_id) + # NOTE: as a provisional solution to avoid duplicate sounds in a collection, Collections already containing the sound are not selectable super().__init__(*args, **kwargs) self.fields['collection'].choices = [(self.NO_COLLECTION_CHOICE_VALUE, '--- No collection ---'),#in this case this goes to bookmarks collection (might have to be created) (self.NEW_COLLECTION_CHOICE_VALUE, 'Create a new collection...')] + \ @@ -98,4 +101,16 @@ def clean(self): raise forms.ValidationError("This sound already exists in the collection") return super().clean() - \ No newline at end of file + +class CollectionEditForm(forms.ModelForm): + + # description = HtmlCleaningCharField(widget=forms.Textarea(attrs={'cols': 80, 'rows': 10}), + # help_text=HtmlCleaningCharField.make_help_text(), required=False) + + class Meta(): + model = Collection + fields = ('name', 'description',) + widgets = { + 'name': TextInput(), + 'description': Textarea(attrs={'rows': 5, 'cols': 50}) + } diff --git a/fscollections/urls.py b/fscollections/urls.py index 5dda996a1f..01010e9ac8 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -6,5 +6,7 @@ path('/', views.collections_for_user, name='collections'), path('/delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), - path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound") + path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound"), + path('/edit', views.edit_collection, name="collection-edit"), + path('/delete', views.delete_collection, name="delete-collection") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index 28b0600d2f..293e283ef6 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -27,7 +27,7 @@ from django.urls import reverse from fscollections.models import Collection, CollectionSound -from fscollections.forms import CollectionSoundForm +from fscollections.forms import CollectionSoundForm, CollectionEditForm from sounds.models import Sound from sounds.views import add_sounds_modal_helper from utils.pagination import paginate @@ -132,10 +132,26 @@ def get_form_for_collecting_sound(request, sound_id): return render(request, 'collections/modal_collect_sound.html', tvars) +def delete_collection(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) + + if request.user==collection.user: + collection.delete() + return HttpResponseRedirect(reverse('collections')) -#NOTE: there should be two methods to add a sound into a collection -#1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal -#2: from the collection.html page through a search-engine modal as done in Packs +def edit_collection(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) + form = CollectionEditForm(instance=collection) + tvars = { + "form": form, + "collection": collection + } + if request.user.username == collection.user.username: + return render(request, 'collections/edit_collection.html', tvars) + +# NOTE: there should be two methods to add a sound into a collection +# 1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal +# 2: from the collection.html page through a search-engine modal as done in Packs """ @login_required def add_sounds_modal_for_collection(request, collection_id): diff --git a/templates/collections/collections.html b/templates/collections/collections.html index f051b18401..82644d2574 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -23,6 +23,9 @@

    Your collections

    {% endif %}
    +

    {{collection.name}}

    @@ -35,7 +38,9 @@

    {{collection.name}}

    {% display_sound_small sound %} {% if is_owner %} - + {% endif %}
    {% endfor %} diff --git a/templates/collections/edit_collection.html b/templates/collections/edit_collection.html new file mode 100644 index 0000000000..eb3da2d8f5 --- /dev/null +++ b/templates/collections/edit_collection.html @@ -0,0 +1,48 @@ +{% extends "simple_page.html" %} +{% load static %} +{% load util %} +{% load bw_templatetags %} +{% load sounds_selector %} + +{% block title %}Edit collection - {{ collection.name }}{% endblock %} +{% block page-title %}Edit collection{% endblock %} + +{% block page-content %} +
    +

    You can specify a name for the collection, a description, and the sounds that are part of the it. You can add any moderated sound in Freesound. + +

    +
    +
    +
    +

    {{ collection.name }}

    +
    {% csrf_token %} + {{ form.non_field_errors }} + {{ form.name.errors }} + {{ form.name.label_tag }} + {{ form.name }} + {{ form.description.errors }} + {{ form.description.label_tag }} + {{ form.description }} + + + +
    + +
    +
    +{% endblock %} + +{% block extrabody %} + +{% endblock %} diff --git a/templates/collections/modal_collect_sound.html b/templates/collections/modal_collect_sound.html index a1ab4c168f..19b9a3a2c7 100644 --- a/templates/collections/modal_collect_sound.html +++ b/templates/collections/modal_collect_sound.html @@ -33,7 +33,7 @@

    Add sound to collection

    {% bw_icon 'bookmark-filled' %}This sound is already in your collections under {% for col in collections_with_sound %} - {{col.name}}{% if not forloop.last%},{% endif %} + {{col.name}}{% if not forloop.last%},{% endif %} {% endfor %}
    {% endif %} diff --git a/templates/sounds/sound.html b/templates/sounds/sound.html index fe2adf3480..00db49afbc 100644 --- a/templates/sounds/sound.html +++ b/templates/sounds/sound.html @@ -79,7 +79,6 @@

    {% bw_icon 'bookmark' %} -

    From d6798498653139aa29664e2d66ff0a413afc990e Mon Sep 17 00:00:00 2001 From: quimmrc Date: Tue, 28 Jan 2025 10:47:20 +0100 Subject: [PATCH 12/28] Edit collection permissions --- fscollections/forms.py | 8 ++++++++ fscollections/views.py | 21 +++++++++++++++++---- templates/collections/edit_collection.html | 3 +++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/fscollections/forms.py b/fscollections/forms.py index 0812b37f42..9507d3a08f 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -114,3 +114,11 @@ class Meta(): 'name': TextInput(), 'description': Textarea(attrs={'rows': 5, 'cols': 50}) } + + def __init__(self, *args, **kwargs): + is_owner = kwargs.pop('is_owner', True) + super().__init__(*args, **kwargs) + if not is_owner: + for field in self.fields: + self.fields[field].widget.attrs['readonly'] = 'readonly' + \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index 293e283ef6..d6b40f925d 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -140,14 +140,27 @@ def delete_collection(request, collection_id): return HttpResponseRedirect(reverse('collections')) def edit_collection(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) - form = CollectionEditForm(instance=collection) + + if request.method=="POST": + form = CollectionEditForm(request.POST, instance=collection) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('collections', args=[collection.id])) + else: + is_owner = False + if request.user == collection.user: + is_owner = True + form = CollectionEditForm(instance=collection, is_owner=is_owner) + tvars = { "form": form, - "collection": collection + "collection": collection, + "is_owner": is_owner } - if request.user.username == collection.user.username: - return render(request, 'collections/edit_collection.html', tvars) + + return render(request, 'collections/edit_collection.html', tvars) # NOTE: there should be two methods to add a sound into a collection # 1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal diff --git a/templates/collections/edit_collection.html b/templates/collections/edit_collection.html index eb3da2d8f5..8c1e8a1aee 100644 --- a/templates/collections/edit_collection.html +++ b/templates/collections/edit_collection.html @@ -16,6 +16,9 @@

    {{ collection.name }}

    + {% if not is_owner %} + You don't have permissions to edit collection's name nor description + {% endif %}
    {% csrf_token %} {{ form.non_field_errors }} {{ form.name.errors }} From 58d845f4622f29a6811396fe30b0542a1142e081 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Tue, 28 Jan 2025 11:43:15 +0100 Subject: [PATCH 13/28] add collection parameter to settings.py --- freesound/context_processor.py | 3 ++- freesound/settings.py | 2 ++ sounds/views.py | 3 ++- templates/molecules/navbar_user_section.html | 3 +++ templates/sounds/sound.html | 7 +++++-- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freesound/context_processor.py b/freesound/context_processor.py index c349889449..cac9e7e931 100644 --- a/freesound/context_processor.py +++ b/freesound/context_processor.py @@ -76,7 +76,8 @@ def context_extra(request): 'next_path': request.GET.get('next', request.get_full_path()), 'login_form': FsAuthenticationForm(), 'problems_logging_in_form': ProblemsLoggingInForm(), - 'system_prefers_dark_theme': request.COOKIES.get('systemPrefersDarkTheme', 'no') == 'yes' # Determine the user's system preference for dark/light theme (for non authenticated users, always use light theme) + 'system_prefers_dark_theme': request.COOKIES.get('systemPrefersDarkTheme', 'no') == 'yes', # Determine the user's system preference for dark/light theme (for non authenticated users, always use light theme) + 'COLLECTIONS': settings.COLLECTIONS }) return tvars diff --git a/freesound/settings.py b/freesound/settings.py index c969b7880f..e1110f1753 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -928,6 +928,8 @@ # ------------------------------------------------------------------------------- # Extra Freesound settings +COLLECTIONS = True + # Paths (depend on DATA_PATH potentially re-defined in local_settings.py) # If new paths are added here, remember to add a line for them at general.apps.GeneralConfig. This will ensure # directories are created if not existing diff --git a/sounds/views.py b/sounds/views.py index cf51254862..5802db6fb8 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -266,7 +266,8 @@ def sound(request, username, sound_id): 'is_following': is_following, 'is_explicit': is_explicit, # if the sound should be shown blurred, already checks for adult profile 'sizes': settings.IFRAME_PLAYER_SIZE, - 'min_num_ratings': settings.MIN_NUMBER_RATINGS + 'min_num_ratings': settings.MIN_NUMBER_RATINGS, + 'collections': settings.COLLECTIONS } tvars.update(paginate(request, qs, settings.SOUND_COMMENTS_PER_PAGE)) return render(request, 'sounds/sound.html', tvars) diff --git a/templates/molecules/navbar_user_section.html b/templates/molecules/navbar_user_section.html index f878f0a464..5262302e99 100644 --- a/templates/molecules/navbar_user_section.html +++ b/templates/molecules/navbar_user_section.html @@ -18,12 +18,15 @@ + {% if COLLECTIONS%} + {% else %} + {% endif %} diff --git a/templates/sounds/sound.html b/templates/sounds/sound.html index 00db49afbc..3db390bedb 100644 --- a/templates/sounds/sound.html +++ b/templates/sounds/sound.html @@ -71,14 +71,17 @@

    - + {% else %} - + {% endif %}

    From 3d7a6684f83d759f24f025828553281ab138f037 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Wed, 29 Jan 2025 11:05:10 +0100 Subject: [PATCH 14/28] add maintainer modal (fails) --- fscollections/forms.py | 30 ++++++++++- fscollections/urls.py | 4 +- fscollections/views.py | 54 +++++++++++++++++-- templates/accounts/account.html | 1 + templates/collections/collections.html | 5 +- .../collections/modal_add_maintainer.html | 39 ++++++++++++++ 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 templates/collections/modal_add_maintainer.html diff --git a/fscollections/forms.py b/fscollections/forms.py index 9507d3a08f..34dcbed513 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -121,4 +121,32 @@ def __init__(self, *args, **kwargs): if not is_owner: for field in self.fields: self.fields[field].widget.attrs['readonly'] = 'readonly' - \ No newline at end of file + +class CollectionMaintainerForm(forms.Form): + collection = forms.ChoiceField( + label=False, + choices=[], + required=True) + + use_last_collection = forms.BooleanField(widget=forms.HiddenInput(), required=False, initial=False) + user_collections = None + user_available_collections = None + + def __init__(self, *args, **kwargs): + self.user_collections = kwargs.pop('user_collections', False) + self.user_adding_maintainer = kwargs.pop('user_adding_maintainer', False) + self.maintainer_id = kwargs.pop('maintainer_id', False) + + if self.user_collections: + # the available collections are: from the user's collections, the ones in which the maintainer is not a maintaner still + self.user_available_collections = Collection.objects.filter(id__in=self.user_collections).exclude(maintainers__id=self.maintainer_id) + + super().__init__(*args, **kwargs) + self.fields['collection'].choices = ([(collection.id, collection.name) for collection in self.user_available_collections] + if self.user_available_collections else []) + + + def save(self, *args, **kwargs): + # this function returns de selected collection + collection_to_use = Collection.objects.get(id=self.cleaned_data['collection']) + return collection_to_use diff --git a/fscollections/urls.py b/fscollections/urls.py index 01010e9ac8..a612ecde40 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -8,5 +8,7 @@ path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound"), path('/edit', views.edit_collection, name="collection-edit"), - path('/delete', views.delete_collection, name="delete-collection") + path('/delete', views.delete_collection, name="delete-collection"), + path('get_form_for_maintainer//', views.get_form_for_maintainer, name="add-maintainer-form"), + path('/add/', views.add_maintainer_to_collection, name="add-maintainer-to-collection") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index d6b40f925d..d5371fc8de 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -20,6 +20,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User from django.contrib import messages from django.db import transaction from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse @@ -27,7 +28,7 @@ from django.urls import reverse from fscollections.models import Collection, CollectionSound -from fscollections.forms import CollectionSoundForm, CollectionEditForm +from fscollections.forms import CollectionSoundForm, CollectionEditForm, CollectionMaintainerForm from sounds.models import Sound from sounds.views import add_sounds_modal_helper from utils.pagination import paginate @@ -50,10 +51,11 @@ def collections_for_user(request, collection_id=None): if user == collection.user: is_owner = True - + maintainers = User.objects.filter(collection_maintainer=collection.id) tvars = {'collection': collection, 'collections_for_user': user_collections, - 'is_owner': is_owner} + 'is_owner': is_owner, + 'maintainers': maintainers} collection_sounds = CollectionSound.objects.filter(collection=collection) paginator = paginate(request, collection_sounds, settings.BOOKMARKS_PER_PAGE) @@ -65,8 +67,6 @@ def collections_for_user(request, collection_id=None): #NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection def add_sound_to_collection(request, sound_id): - # this view from now on should create a new CollectionSound object instead of adding - # a sound to collection.sounds # TODO: add restrictions for sound repetition and for user being owner/maintainer sound = get_object_or_404(Sound, id=sound_id) msg_to_return = '' @@ -162,6 +162,50 @@ def edit_collection(request, collection_id): return render(request, 'collections/edit_collection.html', tvars) +def get_form_for_maintainer(request, user_id): + maintainer = get_object_or_404(User, id=user_id) + + user_collections = Collection.objects.filter(user=request.user).order_by('-created') + last_collection = user_collections[0] + form = CollectionMaintainerForm(initial={'collection': last_collection.id}, + maintainer_id=maintainer.id, + user_collections=user_collections) + + collections_already_containing_maintainer = Collection.objects.filter(user=request.user, maintainers__id=maintainer.id).distinct() + tvars = {'user': request.user, + 'maintainer_id': maintainer.id, + 'last_collection': last_collection, + 'collections': user_collections, + 'form': form, + 'collections_with_maintainer': collections_already_containing_maintainer + } + return render(request, 'collections/modal_add_maintainer.html', tvars) +# def add_maintainer(request, maintainer_id): + +def add_maintainer_to_collection(request, maintainer_id): + maintainer = get_object_or_404(User, id=maintainer_id) + msg_to_return = '' + + if not request.GET.get('ajax'): + HttpResponseRedirect(reverse("accounts", args=[maintainer.username])) + + if request.method == 'POST': + #by now work with direct additions (only user-wise, not maintainer-wise) + user_collections = Collection.objects.filter(user=request.user) + form = CollectionMaintainerForm(request.POST, maintainer_id=maintainer_id, user_collections=user_collections, user_adding_maintainer=request.user) + if form.is_valid(): + saved_collection = form.save() + # TODO: moderation of CollectionSounds to be accounted for users who are neither maintainers nor owners + saved_collection.maintainers.add(maintainer) + saved_collection.save() + msg_to_return = f'User "{maintainer.username}" added as a maintainer to collection {saved_collection.name}' + return JsonResponse('message', msg_to_return) + else: + msg_to_return = 'Something is wrong view-wise' + return JsonResponse('message', msg_to_return) + + + # NOTE: there should be two methods to add a sound into a collection # 1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal # 2: from the collection.html page through a search-engine modal as done in Packs diff --git a/templates/accounts/account.html b/templates/accounts/account.html index 4202858e72..d3bde88d1a 100644 --- a/templates/accounts/account.html +++ b/templates/accounts/account.html @@ -37,6 +37,7 @@

    {{ user.username }}

    Follow {% endif %} Message + Add maintainer {% endif %} {% if perms.tickets.can_moderate or request.user.is_staff %}

    {{collection.name}}

    - by {{collection.user.username}} + by {{collection.user.username}}
    Description: {% if collection.description %}{{collection.description}} {% endif %}
    + Maintainers: {% if maintainers %}{% for u in maintainers %}{{u.username}}{% endfor %} + {% else %}There are no maintainers in this collection yet{% endif %} +
    {% if page.object_list %}
    diff --git a/templates/collections/modal_add_maintainer.html b/templates/collections/modal_add_maintainer.html new file mode 100644 index 0000000000..1bbc7625df --- /dev/null +++ b/templates/collections/modal_add_maintainer.html @@ -0,0 +1,39 @@ +{% extends "molecules/modal_base.html" %} +{% load static %} +{% load bw_templatetags %} + +{% block id %}addMaintainerModal{% endblock %} +{% block extra-class %}{% if request.user.is_authenticated %}modal-width-60{% endif %}{% endblock %} +{% block aria-label %}Add maintainer modal{% endblock %} + +{% block body %} +
    + {% if not request.user.is_authenticated %} +
    +

    Can't add maintainer

    +
    +
    +
    For this action, you need to be logged in with your Freesound account.
    + +
    + {% else %} +
    +

    Add maintainer to collection

    +
    +
    + {% if collections_with_maintainer%} +
    + {% bw_icon 'bookmark-filled' %}This user is already a maintainer for + {% for col in collections_with_maintainer %} + {{col.name}}{% if not forloop.last%},{% endif %} + {% endfor %} +
    + {% endif %} + + {{ form }} + + +
    + {% endif %} +
    +{% endblock %} \ No newline at end of file From 5ffea0b2fed918dbad30ae3e64f42c5f495f227f Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 30 Jan 2025 10:43:34 +0100 Subject: [PATCH 15/28] add maintainers interface --- .../src/components/collectSound.js | 35 ++++++++++--------- fscollections/urls.py | 2 +- fscollections/views.py | 20 ++++++----- templates/accounts/account.html | 2 +- templates/sounds/sound.html | 2 +- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/freesound/static/bw-frontend/src/components/collectSound.js b/freesound/static/bw-frontend/src/components/collectSound.js index e909954270..c0f7329b9b 100644 --- a/freesound/static/bw-frontend/src/components/collectSound.js +++ b/freesound/static/bw-frontend/src/components/collectSound.js @@ -2,7 +2,7 @@ import {dismissModal, handleGenericModal} from "./modal"; import {showToast} from "./toast"; import {makePostRequest} from "../utils/postRequest"; -const saveCollectionSound = (collectSoundUrl, data) => { +const saveCollectionSound = (collectSoundUrl, data, modalType) => { let formData = {}; if (data === undefined){ @@ -15,7 +15,7 @@ const saveCollectionSound = (collectSoundUrl, data) => { } makePostRequest(collectSoundUrl, formData, (responseText) => { // CollectionSound saved successfully. Close model and show feedback - dismissModal(`collectSoundModal`); // TBC + dismissModal(modalType); // TBC try { showToast(JSON.parse(responseText).message); } catch (error) { @@ -24,7 +24,7 @@ const saveCollectionSound = (collectSoundUrl, data) => { } }, () => { // Unexpected errors happened while processing request: close modal and show error in toast - dismissModal(`collectSoundModal`); + dismissModal(modalType); showToast('Some errors occurred while collecting the sound.'); }); } @@ -40,19 +40,17 @@ const toggleNewCollectionNameDiv = (select, newCollectionNameDiv) => { } -const initCollectSoundFormModal = (soundId, collectSoundUrl) => { +const initCollectSoundFormModal = (soundId, collectSoundUrl, modalType) => { // Modify the form structure to add a "Category" label inline with the select dropdown - const modalContainer = document.getElementById('collectSoundModal'); + const modalContainer = document.getElementById(modalType); const selectElement = modalContainer.getElementsByTagName('select')[0]; const wrapper = document.createElement('div'); wrapper.style = 'display:inline-block;'; if (selectElement === undefined){ // If no select element, the modal has probably loaded for an unauthenticated user - console.log("select element is undefined") return; } - console.log("SELECT ELEMENT", selectElement); selectElement.parentNode.insertBefore(wrapper, selectElement.parentNode.firstChild); const label = document.createElement('div'); label.innerHTML = "Select a collection:" @@ -64,21 +62,25 @@ const initCollectSoundFormModal = (soundId, collectSoundUrl) => { const buttonsInModalForm = formElement.getElementsByTagName('button'); const saveButtonElement = buttonsInModalForm[buttonsInModalForm.length - 1]; const categorySelectElement = document.getElementById(`id_${ soundId.toString() }-collection`); - const newCategoryNameElement = document.getElementById(`id_${ soundId.toString() }-new_collection_name`); - console.log("CATEGORY SELECT ELEMENT: ", categorySelectElement); - console.log("NEW CATEGORY NAME ELEMENT: ", newCategoryNameElement); - toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); - categorySelectElement.addEventListener('change', (event) => { + // New collection is not allowed for addMaintainerModal + if (modalType=='collectSoundModal'){ + const newCategoryNameElement = document.getElementById(`id_${ soundId.toString() }-new_collection_name`); toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); - }); + categorySelectElement.addEventListener('change', (event) => { + toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement); + }); + } + // Bind action to save collectionSound in "add sound to collection button" (and prevent default form submit) saveButtonElement.addEventListener('click', (e) => { e.preventDefault(); const data = {}; data.collection = document.getElementById(`id_${ soundId.toString() }-collection`).value; - data.new_collection_name = document.getElementById(`id_${ soundId.toString() }-new_collection_name`).value; - saveCollectionSound(collectSoundUrl, data); + if(modalType=='collectSoundModal'){ + data.new_collection_name = document.getElementById(`id_${ soundId.toString() }-new_collection_name`).value; + } + saveCollectionSound(collectSoundUrl, data, modalType); }); }; @@ -93,9 +95,10 @@ const bindCollectSoundModals = (container) => { evt.preventDefault(); const modalUrlSplitted = element.dataset.modalUrl.split('/'); const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 2], 10); + const modalType = element.dataset.modalType; if (!evt.altKey) { handleGenericModal(element.dataset.modalUrl, () => { - initCollectSoundFormModal(soundId, element.dataset.collectSoundUrl); + initCollectSoundFormModal(soundId, element.dataset.collectSoundUrl, modalType); }, undefined, true, true); } else { saveCollectionSound(element.dataset.collectSoundUrl); diff --git a/fscollections/urls.py b/fscollections/urls.py index a612ecde40..bdfb7c3587 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -10,5 +10,5 @@ path('/edit', views.edit_collection, name="collection-edit"), path('/delete', views.delete_collection, name="delete-collection"), path('get_form_for_maintainer//', views.get_form_for_maintainer, name="add-maintainer-form"), - path('/add/', views.add_maintainer_to_collection, name="add-maintainer-to-collection") + path('addmaintainer//', views.add_maintainer_to_collection, name="add-maintainer-to-collection") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index d5371fc8de..48d73b6aee 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -169,6 +169,7 @@ def get_form_for_maintainer(request, user_id): last_collection = user_collections[0] form = CollectionMaintainerForm(initial={'collection': last_collection.id}, maintainer_id=maintainer.id, + prefix=maintainer.id, user_collections=user_collections) collections_already_containing_maintainer = Collection.objects.filter(user=request.user, maintainers__id=maintainer.id).distinct() @@ -180,17 +181,12 @@ def get_form_for_maintainer(request, user_id): 'collections_with_maintainer': collections_already_containing_maintainer } return render(request, 'collections/modal_add_maintainer.html', tvars) -# def add_maintainer(request, maintainer_id): def add_maintainer_to_collection(request, maintainer_id): maintainer = get_object_or_404(User, id=maintainer_id) msg_to_return = '' - if not request.GET.get('ajax'): - HttpResponseRedirect(reverse("accounts", args=[maintainer.username])) - if request.method == 'POST': - #by now work with direct additions (only user-wise, not maintainer-wise) user_collections = Collection.objects.filter(user=request.user) form = CollectionMaintainerForm(request.POST, maintainer_id=maintainer_id, user_collections=user_collections, user_adding_maintainer=request.user) if form.is_valid(): @@ -199,10 +195,18 @@ def add_maintainer_to_collection(request, maintainer_id): saved_collection.maintainers.add(maintainer) saved_collection.save() msg_to_return = f'User "{maintainer.username}" added as a maintainer to collection {saved_collection.name}' - return JsonResponse('message', msg_to_return) else: - msg_to_return = 'Something is wrong view-wise' - return JsonResponse('message', msg_to_return) + msg_to_return = form.errors + + if request.is_ajax(): + return JsonResponse({'message': msg_to_return}) + else: + messages.add_message(request, messages.WARNING, msg_to_return) + next = request.GET.get("next", "") + if next: + return HttpResponseRedirect(next) + else: + return HttpResponseRedirect(reverse("account", args=[maintainer.username])) diff --git a/templates/accounts/account.html b/templates/accounts/account.html index d3bde88d1a..e87cbca917 100644 --- a/templates/accounts/account.html +++ b/templates/accounts/account.html @@ -37,7 +37,7 @@

    {{ user.username }}

    Follow {% endif %} Message - Add maintainer + Add maintainer {% endif %} {% if perms.tickets.can_moderate or request.user.is_staff %}
    From 8fd039ae5bfd8395a9d27edce05ec24095953cb1 Mon Sep 17 00:00:00 2001 From: quimmrc Date: Wed, 12 Feb 2025 11:27:27 +0100 Subject: [PATCH 25/28] add sounds from small player + display Json success msg --- freesound/static/bw-frontend/src/components/player/player-ui.js | 2 +- fscollections/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freesound/static/bw-frontend/src/components/player/player-ui.js b/freesound/static/bw-frontend/src/components/player/player-ui.js index acb4cd3560..f3df92a9d6 100644 --- a/freesound/static/bw-frontend/src/components/player/player-ui.js +++ b/freesound/static/bw-frontend/src/components/player/player-ui.js @@ -578,7 +578,7 @@ const createCollectionButton = (parentNode, playerImgNode) => { } collectionButton.setAttribute('data-toggle', 'collection-modal'); collectionButton.setAttribute('data-modal-url', parentNode.dataset.collectionModalUrl); - collectionButton.setAttribute('data-add-collection-url', parentNode.dataset.collectionSoundUrl); + collectionButton.setAttribute('data-collection-sound-url', parentNode.dataset.collectionSoundUrl); collectionButtonContainer.appendChild(collectionButton); return collectionButtonContainer; } diff --git a/fscollections/views.py b/fscollections/views.py index f9f30d4883..11cf0418ff 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -86,7 +86,7 @@ def add_sound_to_collection(request, sound_id): saved_collection.num_sounds += 1 #this should be done with a signal/method in Collection models saved_collection.save() msg_to_return = f'Sound "{sound.original_filename}" saved under collection {saved_collection.name}' - return JsonResponse('message', msg_to_return) + return JsonResponse({'success': True, 'message': msg_to_return}) def delete_sound_from_collection(request, collectionsound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection From cd609e00aefecad65a71d933461174d00aada5be Mon Sep 17 00:00:00 2001 From: quimmrc Date: Thu, 13 Feb 2025 10:24:16 +0100 Subject: [PATCH 26/28] download collections --- fscollections/models.py | 25 +++++++++++++++++++++++++ fscollections/urls.py | 6 ++++-- fscollections/views.py | 23 ++++++++++++++++++++--- templates/collections/collections.html | 3 +++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/fscollections/models.py b/fscollections/models.py index eda21c2537..98e077c668 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -20,6 +20,8 @@ from django.contrib.auth.models import User from django.db import models +from django.template.loader import render_to_string +from django.utils.text import slugify from sounds.models import Sound, License @@ -43,6 +45,29 @@ class Collection(models.Model): def __str__(self): return f"{self.name}" + + def get_attribution(self, sound_qs=None): + #If no queryset of sounds is provided, take it from the bookmark category + if sound_qs is None: + collection_sounds = CollectionSound.objects.filter(collection=self).values("sound_id") + sound_qs = Sound.objects.filter(id__in=collection_sounds, processing_state="OK", moderation_state="OK").select_related('user','license') + + users = User.objects.filter(sounds__in=sound_qs).distinct() + # Generate text file with license info + licenses = License.objects.filter(sound__id__in=sound_qs).distinct() + attribution = render_to_string(("sounds/multiple_sounds_attribution.txt"), + dict(type="Collection", + users=users, + object=self, + licenses=licenses, + sound_list=sound_qs)) + return attribution + + @property + def download_filename(self): + name_slug = slugify(self.name) + username_slug = slugify(self.user.username) + return "%d__%s__%s.zip" % (self.id, username_slug, name_slug) class CollectionSound(models.Model): diff --git a/fscollections/urls.py b/fscollections/urls.py index fa2e075607..2d326c716a 100644 --- a/fscollections/urls.py +++ b/fscollections/urls.py @@ -6,9 +6,11 @@ path('/', views.collections_for_user, name='collections'), path('/delete/', views.delete_sound_from_collection, name='delete-sound-from-collection'), path('/add/', views.add_sound_to_collection, name='add-sound-to-collection'), - path('get_form_for_collection_sound//', views.get_form_for_collecting_sound, name="collection-add-form-for-sound"), + path('/get_form_for_collection_sound/', views.get_form_for_collecting_sound, name="collection-add-form-for-sound"), path('create/', views.create_collection, name='create-collection'), path('/edit', views.edit_collection, name="edit-collection"), path('/delete', views.delete_collection, name="delete-collection"), - path('addmaintainer//', views.add_maintainer_to_collection, name="add-maintainers-to-collection") + path('/addmaintainer', views.add_maintainer_to_collection, name="add-maintainers-to-collection"), + path('/download/', views.download_collection, name="download-collection"), + path('/licenses/', views.collection_licenses, name="collection-licenses") ] \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index 11cf0418ff..6e36a08771 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -32,6 +32,7 @@ from sounds.models import Sound from sounds.views import add_sounds_modal_helper from utils.pagination import paginate +from utils.downloads import download_sounds @login_required @@ -39,6 +40,7 @@ def collections_for_user(request, collection_id=None): user = request.user user_collections = Collection.objects.filter(user=user).order_by('-modified') is_owner = False + is_maintainer = False # if no collection id is provided for this URL, render the oldest collection # only show the collections for which you're the user(owner) @@ -47,13 +49,17 @@ def collections_for_user(request, collection_id=None): else: collection = get_object_or_404(Collection, id=collection_id) + maintainers = User.objects.filter(collection_maintainer=collection.id) + if user == collection.user: is_owner = True + elif user in maintainers: + is_maintainer = True - maintainers = User.objects.filter(collection_maintainer=collection.id) tvars = {'collection': collection, 'collections_for_user': user_collections, 'is_owner': is_owner, + 'is_maintainer': is_maintainer, 'maintainers': maintainers} collection_sounds = CollectionSound.objects.filter(collection=collection) @@ -63,8 +69,6 @@ def collections_for_user(request, collection_id=None): tvars['page_collection_and_sound_objects'] = zip(paginator['page'].object_list, page_sounds) return render(request, 'collections/collections.html', tvars) -#NOTE: tbd - when a user wants to save a sound without having any collection, create a personal bookmarks collection - def add_sound_to_collection(request, sound_id): # TODO: add restrictions for sound repetition and for user being owner/maintainer # this does not work when adding sounds from search results @@ -199,6 +203,19 @@ def add_maintainer_to_collection(request, collection_id): "form": form} return render(request, 'collections/modal_add_maintainer.html', tvars) +def download_collection(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) + collection_sounds = CollectionSound.objects.filter(collection=collection).values('sound_id') + sounds_list = Sound.objects.filter(id__in=collection_sounds, processing_state="OK", moderation_state="OK").select_related('user','license') + licenses_url = (reverse('collection-licenses', args=[collection_id])) + licenses_content = collection.get_attribution(sound_qs=sounds_list) + return download_sounds(licenses_url, licenses_content, sounds_list, collection.download_filename) + +def collection_licenses(request, collection_id): + collection = get_object_or_404(Collection, id=collection_id) + attribution = collection.get_attribution() + return HttpResponse(attribution, content_type="text/plain") + # NOTE: there should be two methods to add a sound into a collection # 1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal # 2: from the collection.html page through a search-engine modal as done in Packs diff --git a/templates/collections/collections.html b/templates/collections/collections.html index c0e4d6c5c6..efb6521f65 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -25,7 +25,10 @@

    Your collections

    + {% if is_owner or is_maintainer%} Edit collection + {% endif %} + Download Collection
    From f64d1b2182e0a26628c34f28b8a55261636d1bef Mon Sep 17 00:00:00 2001 From: quimmrc Date: Mon, 17 Feb 2025 19:34:40 +0100 Subject: [PATCH 27/28] tests and miscellanious --- fscollections/forms.py | 4 +- .../migrations/0006_auto_20250213_1108.py | 30 ++++++++ fscollections/models.py | 2 +- fscollections/tests.py | 70 ++++++++++++++----- fscollections/views.py | 36 +++++----- templates/collections/collections.html | 4 +- 6 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 fscollections/migrations/0006_auto_20250213_1108.py diff --git a/fscollections/forms.py b/fscollections/forms.py index 9079595faa..535d156f2b 100644 --- a/fscollections/forms.py +++ b/fscollections/forms.py @@ -179,6 +179,7 @@ def clean(self): class MaintainerForm(forms.Form): maintainer = forms.CharField( + widget=TextInput(attrs={'placeholder': "Fill in the username of the maintainer"}), label=False, help_text=None, max_length=128, @@ -189,7 +190,6 @@ class MaintainerForm(forms.Form): def __init__(self, *args, **kwargs): self.collection = kwargs.pop('collection', False) super().__init__(*args, **kwargs) - self.fields['maintainer'].widget.attrs['placeholder'] = "Fill in the username of the maintainer" def clean(self): try: @@ -199,8 +199,6 @@ def clean(self): return super().clean() except User.DoesNotExist: raise forms.ValidationError("The user does not exist") - - return super().clean() # NOTE: adding maintainers will be done frome edit collection page using a modal to introduce # username diff --git a/fscollections/migrations/0006_auto_20250213_1108.py b/fscollections/migrations/0006_auto_20250213_1108.py new file mode 100644 index 0000000000..5bf1303dec --- /dev/null +++ b/fscollections/migrations/0006_auto_20250213_1108.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.23 on 2025-02-13 11:08 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('fscollections', '0005_collection_is_default_collection'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='num_downloads', + field=models.PositiveIntegerField(default=0), + ), + migrations.AlterField( + model_name='collection', + name='description', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='collection', + name='maintainers', + field=models.ManyToManyField(blank=True, related_name='collection_maintainer', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/fscollections/models.py b/fscollections/models.py index 98e077c668..fc8f726bd9 100644 --- a/fscollections/models.py +++ b/fscollections/models.py @@ -35,6 +35,7 @@ class Collection(models.Model): description = models.TextField(blank=True) maintainers = models.ManyToManyField(User, related_name="collection_maintainer", blank=True) num_sounds = models.PositiveIntegerField(default=0) + num_downloads = models.PositiveIntegerField(default=0) public = models.BooleanField(default=False) is_default_collection = models.BooleanField(default=False) #NOTE: Don't fear migrations, you're just testing @@ -69,7 +70,6 @@ def download_filename(self): username_slug = slugify(self.user.username) return "%d__%s__%s.zip" % (self.id, username_slug, name_slug) - class CollectionSound(models.Model): #this model relates collections and sounds #it might be worth adding a name field composed of the sound ID and the collection name for diff --git a/fscollections/tests.py b/fscollections/tests.py index 8d5fd7d7a5..d55cf60c90 100644 --- a/fscollections/tests.py +++ b/fscollections/tests.py @@ -2,45 +2,79 @@ from django.contrib.auth.models import User from django.urls import reverse -from fscollections import models +from utils.test_helpers import create_user_and_sounds +from fscollections.models import * # Create your tests here. class CollectionTest(TestCase): - def test_collections_create_and_delete(self): - user = User.objects.create_user(username='testuser') - collection = models.Collection.objects.create(user=user, name='testcollection') - self.assertEqual(collection.user, user) - self.assertEqual(collection.name, 'testcollection') - - # User not logged in + fixtures = ['licenses', 'sounds'] + + def setUp(self): + self.user = User.objects.create(username='testuser', email='testuser@freesound.org') + ___, ___, sounds = create_user_and_sounds(num_sounds=3, user=self.user, processing_state="OK", moderation_state="OK") + self.sound = sounds[0] + self.collection = Collection(user=self.user, name='testcollection') + self.collection.save() + + + def test_collections_create_and_delete(self): + + # User not logged in - redirects to login page # For this to be truly useful, collection urls should be moved into accounts # This might have consequences for the way collections work resp = self.client.get(reverse('collections')) - self.assertEqual(302, resp.status_code) + self.assertEqual(resp.status_code, 302) # Force login and test collections page - self.client.force_login(user) + self.client.force_login(self.user) resp = self.client.get(reverse('collections')) self.assertEqual(resp.status_code, 200) - # User logged in, redirect to collections/collection page - resp = self.client.get(reverse('collections', kwargs={'collection_id': collection.id})) + # User logged in, check given collection id works + resp = self.client.get(reverse('collections', args=[self.collection.id])) self.assertEqual(resp.status_code, 200) + # Create collection view + resp = self.client.post(reverse('create-collection'), {'name': 'tbdcollection'}) + self.assertEqual(resp.status_code, 200) + delete_collection = Collection.objects.get(name='tbdcollection') + # Delete collection view - resp = self.client.get(reverse('delete-collection', kwargs={'collection_id': collection.id})) + resp = self.client.get(reverse('delete-collection',args=[delete_collection.id])) self.assertEqual(resp.status_code, 302) # Test collection URL for collection.id does not exist - resp = self.client.get(reverse('collections', kwargs={'collection_id': 100})) + resp = self.client.get(reverse('collections', args=[delete_collection.id])) self.assertEqual(resp.status_code, 404) - # Test collection URL with no collection id - resp = self.client.get(reverse('collections')) + def test_add_remove_sounds(self): + self.client.force_login(self.user) + + # Test adding sound to collection + resp = self.client.post(reverse('add-sound-to-collection', args=[self.sound.id]), {'collection': self.collection.id}) + self.collection.refresh_from_db() self.assertEqual(resp.status_code, 200) + self.assertEqual(1, self.collection.num_sounds) + + # Test removing sound from collection + collectionsound = CollectionSound.objects.get(collection=self.collection, sound=self.sound) + resp = self.client.post(reverse('delete-sound-from-collection', args=[collectionsound.id])) + self.collection.refresh_from_db() + self.assertEqual(resp.status_code, 302) + self.assertEqual(0, self.collection.num_sounds) - # def test_add_sound_to_collection(self): + # Test edit collection URL + resp = self.client.get(reverse('edit-collection', args=[self.collection.id])) + self.assertEqual(resp.status_code, 200) - # def test_remove_sound_from_collection(self): + # Test adding maintainer to collection + maintainer = User.objects.create(username='maintainer_user', email='maintainer@freesound.org') + resp = self.client.post(reverse('add-maintainers-to-collection', args=[self.collection.id]), {'maintainer': maintainer.username}) + self.collection.refresh_from_db() + self.assertEqual(resp.status_code, 200) + self.assertEqual(maintainer, self.collection.maintainers.all()[0]) + # Test download collection + resp = self.client.get(reverse('download-collection', args=[self.collection.id])) + self.assertEqual(resp.status_code, 200) \ No newline at end of file diff --git a/fscollections/views.py b/fscollections/views.py index 6e36a08771..21e441d96f 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -41,20 +41,20 @@ def collections_for_user(request, collection_id=None): user_collections = Collection.objects.filter(user=user).order_by('-modified') is_owner = False is_maintainer = False + maintainers = [] # if no collection id is provided for this URL, render the oldest collection # only show the collections for which you're the user(owner) - if not collection_id: collection = user_collections.last() else: collection = get_object_or_404(Collection, id=collection_id) - - maintainers = User.objects.filter(collection_maintainer=collection.id) - - if user == collection.user: - is_owner = True - elif user in maintainers: - is_maintainer = True + + if collection: + maintainers = User.objects.filter(collection_maintainer=collection.id) + if user == collection.user: + is_owner = True + elif user in maintainers: + is_maintainer = True tvars = {'collection': collection, 'collections_for_user': user_collections, @@ -82,7 +82,7 @@ def add_sound_to_collection(request, sound_id): #by now work with direct additions (only user-wise, not maintainer-wise) user_collections = Collection.objects.filter(user=request.user) form = CollectionSoundForm(request.POST, sound_id=sound_id, user_collections=user_collections, user_saving_sound=request.user) - + if form.is_valid(): saved_collection = form.save() # TODO: moderation of CollectionSounds to be accounted for users who are neither maintainers nor owners @@ -94,11 +94,12 @@ def add_sound_to_collection(request, sound_id): def delete_sound_from_collection(request, collectionsound_id): #this should work as in Packs - select several sounds and remove them all at once from the collection - #by now it works as in Bookmarks in terms of UI #TODO: this should be done through a POST request method collection_sound = get_object_or_404(CollectionSound, id=collectionsound_id) collection = collection_sound.collection collection_sound.delete() + collection.num_sounds -= 1 #this shouldn't be done like this but it is for the sake of tests + collection.save() return HttpResponseRedirect(reverse("collections", args=[collection.id])) @login_required @@ -133,14 +134,14 @@ def get_form_for_collecting_sound(request, sound_id): return render(request, 'collections/modal_collect_sound.html', tvars) def create_collection(request): - if not request.GET.get('ajax'): - return HttpResponseRedirect(reverse("collections-for-user")) + # if not request.GET.get('ajax'): + # return HttpResponseRedirect(reverse("collections")) if request.method == "POST": form = CreateCollectionForm(request.POST, user=request.user) if form.is_valid(): Collection(user = request.user, name = form.cleaned_data['name'], - description=form.cleaned_data['description']).save() + description = form.cleaned_data['description']).save() return JsonResponse({'success': True}) else: form = CreateCollectionForm(user=request.user) @@ -185,11 +186,11 @@ def edit_collection(request, collection_id): def add_maintainer_to_collection(request, collection_id): #TODO: store maintainers inside modal to further add them at once - if not request.GET.get('ajax'): - return HttpResponseRedirect(reverse("collections-for-user", args=[collection_id])) + # if not request.GET.get('ajax'): + # return HttpResponseRedirect(reverse("collections", args=[collection_id])) collection = get_object_or_404(Collection, id=collection_id, user=request.user) - + if request.method == "POST": form = MaintainerForm(request.POST, collection=collection) if form.is_valid(): @@ -209,6 +210,7 @@ def download_collection(request, collection_id): sounds_list = Sound.objects.filter(id__in=collection_sounds, processing_state="OK", moderation_state="OK").select_related('user','license') licenses_url = (reverse('collection-licenses', args=[collection_id])) licenses_content = collection.get_attribution(sound_qs=sounds_list) + collection.num_downloads += 1 return download_sounds(licenses_url, licenses_content, sounds_list, collection.download_filename) def collection_licenses(request, collection_id): @@ -218,4 +220,4 @@ def collection_licenses(request, collection_id): # NOTE: there should be two methods to add a sound into a collection # 1: adding from the sound.html page through a "bookmark-like" button and opening a Collections modal -# 2: from the collection.html page through a search-engine modal as done in Packs +# 2: from the collection.html page through a search-engine modal as done in Packs \ No newline at end of file diff --git a/templates/collections/collections.html b/templates/collections/collections.html index efb6521f65..14a873ff8d 100644 --- a/templates/collections/collections.html +++ b/templates/collections/collections.html @@ -28,7 +28,9 @@

    Your collections

    {% if is_owner or is_maintainer%} Edit collection {% endif %} - Download Collection + {% if collection.num_sounds > 0%} + Download Collection + {% endif %}
    From ff18e6bcaa4a663f2a7c4c72696fc9765081202d Mon Sep 17 00:00:00 2001 From: quimmrc Date: Mon, 17 Feb 2025 19:44:32 +0100 Subject: [PATCH 28/28] order paginator query --- fscollections/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fscollections/views.py b/fscollections/views.py index 21e441d96f..80712a8a26 100644 --- a/fscollections/views.py +++ b/fscollections/views.py @@ -62,7 +62,7 @@ def collections_for_user(request, collection_id=None): 'is_maintainer': is_maintainer, 'maintainers': maintainers} - collection_sounds = CollectionSound.objects.filter(collection=collection) + collection_sounds = CollectionSound.objects.filter(collection=collection).order_by('created') paginator = paginate(request, collection_sounds, settings.BOOKMARKS_PER_PAGE) page_sounds = Sound.objects.ordered_ids([col_sound.sound_id for col_sound in paginator['page'].object_list]) tvars.update(paginator)