-
Notifications
You must be signed in to change notification settings - Fork 90
Collections #1810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Collections #1810
Conversation
improve url handling and visualization
fscollections/models.py
Outdated
class Collection(models.Model): | ||
|
||
author = models.ForeignKey(User, on_delete=models.CASCADE) | ||
name = models.CharField(max_length=128, default="BookmarkCollection") #add restrictions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to specify a default
fscollections/views.py
Outdated
def delete_collection(request, collection_id): | ||
collection = get_object_or_404(Collection, id=collection_id) | ||
|
||
if request.user==collection.user: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider else case, return 404 or reload edit page but with a message saying that user is not allowed
…l and bulk_sounds_for_collection
@@ -73,11 +88,14 @@ const prepareAddSoundsModalAndFields = (container) => { | |||
const newSoundIds = serializedIdListToIntList(selectedSoundIds); | |||
const combinedIds = combineIdsLists(currentSoundIds, newSoundIds); | |||
selectedSoundsHiddenInput.value = combinedIds.join(','); | |||
if(selectedSoundsHiddenInput.value.split(',').length >= 4 && selectedSoundsHiddenInput.id === "collection_sounds"){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's an error here, this 4 should take the value of settings.MAX_SOUNDS_PER_COLLECTION (something that happens again a few lines of code above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should take the value from some data property as I commented above. Is there a problem with that?
const modalUrlSplitted = element.dataset.modalContentUrl.split('/') | ||
const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 3], 10) | ||
if (!evt.altKey) { | ||
handleGenericModalWithForm(element.dataset.modalContentUrl, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in here, bookmarks used a custom saveCollection function, however I found more intuitive and standard to use a handleGenericModalWithForm to properly handle the submission of the SelectCollectionOrNewCollection form, which is the one used to add sounds to Collections from Sound URLS.
updateObjectSelectorDataProperties, the "call stack" states that originally, the function is called from here (line 175). | ||
Therefore, the definition of selectedMaintainersDestinationElement somehow is wrong or confused with the selectedSoundsDestinationElement. | ||
I tried to filter this using the above queryselector but it does not work. I guess this might have something to do with the file | ||
collectionEdit.js. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This piece of comment might be interesting but the issue prompted by this is already solved. Basically the query for users does not exclude any, and instead "validation" for users who are added as maintaienrs is done from server-side.
fields = CollectionEditForm.Meta.fields + ['collection_sounds'] + ['maintainers'] | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might need a good review, it was a topic of discussion in our last meeting and I think that my proposal differs a bit from your main idea. In here this forms inherits everything so that it can be displayed in the edit url but all fields get disabled as soon as the form is initalized.
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 | ||
|
||
@receiver(post_save, sender=CollectionSound) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
num_sounds are now updated using CollectionSounds triggers, not only in the save collection method
|
||
register = template.Library() | ||
|
||
@register.simple_tag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should've deleted this but I forgot, my mistake
# the collections_for_user can be reused to display ONE collection so give it a thought on full collections display | ||
return render(request, 'collections/your_collections.html', tvars) | ||
|
||
def collection_stats_section(request, collection_id): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In here I tried to imitate the pack stats behaviour but I was not much familiar with it and how the cache is handled so this is not properly updated (needs further review by me)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a look when testing the PR. This async loading of stats has to do with caching computation of stats, but maybe if stats computation is fast enough, then this might not be needed. We added some recent changes to compute some of the stats in DB rather than in python with good results, so we might apply this here and not do the stats section async.
if form.is_valid(): | ||
form.save(user_adding_sound=request.user) | ||
return HttpResponseRedirect(reverse('collection', args=[collection.id])) | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this part might need a good review since I did something unusual here, but I still believe it's solid enough
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a detailed look into this when I can test the PR. At first sight I don't think we need to anything unusual here.
{% load static %} | ||
{% load bw_templatetags %} | ||
|
||
{% block id %}collectSoundModal{% endblock %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be named "AddSoundToCollectionModal" or similar, I forgot to change this, my bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this allows to differentiate the sound selector in the edit collections page from the users selector (for maintainers)
…max_sounds for django templates
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @quimmrc for the PR! I had a quick look, but couldn't test it yet, I'll do it soon. This is long and complex code so some things still need to be looked at in detail, but it looks good generally! I'll did leave some comments of some small things to improve. You might want to look at those in the next days before I do a test and further review (if you have time).
def users_selector(context, users, selected_user_ids=[], show_select_all_buttons=False): | ||
if users: | ||
if not isinstance(users[0], User): | ||
# users are passed as a list of user ids, retrieve the Sound objects from DB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...retrieve the User objects...
'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) | ||
'enable_collections': settings.ENABLE_COLLECTIONS, | ||
'max_sounds_per_collection': settings.MAX_SOUNDS_PER_COLLECTION, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should add enable_collections
and max_sounds_per_collection
in the global context processor as this will only be used in collections-related pages. This should probably only be added in tvars
where needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree for max_sounds_per_collection
, however, for the enable_collections case it is used in a couple of html templates which are the navigation bar and the sound url ones, which have a more global behavior rather than collections-related-pages, do you think we could leave enable_collections
as a context processor?
|
||
</p> | ||
</div> | ||
<div id="config" data-max-sounds="{{max_sounds_per_collection}}"></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite ugly here :)
data-max-sounds
could be a property of the object selector, and place it somewhere there. I left other comments that should clarify this.
} | ||
|
||
if(soundsInput.id === "collection_sounds"){ | ||
const maxSounds = document.getElementById('config').dataset.maxSounds; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't hardcode this thing here if(soundsInput.id === "collection_sounds")
. I think you could set data-max-elements
maybe as a property of selectedSoundsDestinationElement
(because it already has the data-type
property), and here you check if there is any maximum set, and if there is, act accordingly and disable the add button if selected elements is above the maximum.
@@ -62,6 +78,9 @@ const prepareAddSoundsModalAndFields = (container) => { | |||
updateObjectSelectorDataProperties(selectedSoundsDestinationElement); | |||
const selectedSoundsHiddenInput = document.getElementById(addSoundsButton.dataset.selectedSoundsHiddenInputId); | |||
selectedSoundsHiddenInput.value = selectedSoundsDestinationElement.dataset.unselectedIds; | |||
if(selectedSoundsHiddenInput.value.split(',').length < maxSounds && selectedSoundsHiddenInput.id === "collection_sounds"){ | |||
addSoundsButton.disabled = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also be updated per the comments above.
# the collections_for_user can be reused to display ONE collection so give it a thought on full collections display | ||
return render(request, 'collections/your_collections.html', tvars) | ||
|
||
def collection_stats_section(request, collection_id): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a look when testing the PR. This async loading of stats has to do with caching computation of stats, but maybe if stats computation is fast enough, then this might not be needed. We added some recent changes to compute some of the stats in DB rather than in python with good results, so we might apply this here and not do the stats section async.
collection = get_object_or_404(Collection, id=collection_id) | ||
|
||
if request.method=="POST" and request.user==collection.user: | ||
collection.delete() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a message like "collection successfully deleted"? See how django messages app works, we do it in many other places
collection.delete() | ||
return HttpResponseRedirect(reverse('your-collections')) | ||
else: | ||
return HttpResponseRedirect(reverse('collection', args=[collection.id])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should return "not allowed" response. Did you check how similar things are handled elsewhere in Freesound?
if form.is_valid(): | ||
form.save(user_adding_sound=request.user) | ||
return HttpResponseRedirect(reverse('collection', args=[collection.id])) | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a detailed look into this when I can test the PR. At first sight I don't think we need to anything unusual here.
qs = qs.filter( | ||
moderation_state="OK", | ||
processing_state="OK", | ||
collections=collection_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work?? I guess it is some feature of the through
model, but I'm a bit surprised here... I'd like to look into this with @alastair
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it does, so far it is only used to render the sounds selector for the edit collection URL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this should be fine. it's not specifically related to the through
, just the ManyToManyField
} | ||
if (soundsLabel){ | ||
soundsLabel.innerHTML = "Sounds in collection (" + selectedSoundsDestinationElement.children.length + ")"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A part from removing the hard-coded part and retrieving the number of elements from the sound selector, I automated the display of the help text for this field (informing of maximum number of sounds permitted) and the label of this field (which shows the number of sounds in collection). Although this is also used for pack edits, it has no conflicts (queries for help text and label won't find anything).
fscollections/forms.py
Outdated
|
||
if self.instance.num_sounds >= settings.MAX_SOUNDS_PER_COLLECTION: | ||
self.fields['collection_sounds'].help_text=f"You have reached the maximum number of sounds available for a collection ({settings.MAX_SOUNDS_PER_COLLECTION}). " \ | ||
"In order to add new sounds, first remove some of the current ones." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could not be updated if done from server-side, which was solved with the comment above
@register.inclusion_tag('accounts/display_user_selectable.html', takes_context=True) | ||
def display_user_small_selectable(context, user, selected=False): | ||
context = context.get('original_context', context) # This is to allow passing context in nested inclusion tags | ||
tvars = display_user(context, user, size='basic') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double-check this - to me it seems a bit weird to call a function which is registered as a templatetag that returns HTML. Does this actually work?
It might be better to take the code for display_user
and move it into a separate util function that can be used by both of these template tags
|
||
@register.inclusion_tag('accounts/display_user_selectable.html', takes_context=True) | ||
def display_user_small_selectable(context, user, selected=False): | ||
context = context.get('original_context', context) # This is to allow passing context in nested inclusion tags |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary comment
@@ -0,0 +1,27 @@ | |||
# Authors: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a bit weird to have just the authors
block but no copyright?
|
||
|
||
@register.inclusion_tag('molecules/object_selector.html', takes_context=True) | ||
def users_selector(context, users, selected_user_ids=[], show_select_all_buttons=False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using =[]
in a function isn't a good idea, set this to None and only iterate through it if it's not NOne
'objects': users, | ||
'type': 'users', | ||
'show_select_all_buttons': show_select_all_buttons, | ||
'original_context': context # This will be used so a nested inclusion tag can get the original context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unneeded comment
</div> | ||
{% else %} | ||
<div class="v-spacing-top-3"> | ||
<span class=text-light-grey> You don't have any collection yet...</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<span class=text-light-grey> You don't have any collection yet...</span> | |
<span class=text-light-grey> You don't have any collections yet...</span> |
</div> | ||
{% else %} | ||
<div class="v-spacing-top-3"> | ||
<span class=text-light-grey> You're not a maintainer in any collection yet...</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<span class=text-light-grey> You're not a maintainer in any collection yet...</span> | |
<span class=text-light-grey> You're not a maintainer of any collection yet...</span> |
#sound won't be added to collection until maintainers approve the sound | ||
|
||
@receiver(post_save, sender=CollectionSound) | ||
def update_collection_num_sounds(**kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be better to have the explicit list of parameters to these signals: https://docs.djangoproject.com/en/5.2/ref/signals/#django.db.models.signals.pre_save
def update_collection_num_sounds(**kwargs): | |
def update_collection_num_sounds(sender, instance, raw, using, update_fields): |
Then you don't need to set isntance to False in the next line (this will already be None if you're in this situation)
Collection.objects.filter(collectionsound=collectionsound).update(num_sounds=Greatest(F('num_sounds') + 1, 0)) | ||
|
||
@receiver(post_delete, sender=CollectionSound) | ||
def update_collection_num_sounds_sound_removal(**kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that django doesn't send post_save/post_delete signals when you directly update a ManyToMany field: see https://docs.djangoproject.com/en/4.2/ref/signals/#m2m-changed
I see that you create these objects manually anyway (this is necessary if you have a through model with extra data, so this is fine), but we may be in a situation where we bulk remove sounds from a collection and this signal isn't triggered. Review if this might happen in any place
for snd in current_sounds: | ||
sound = Sound.objects.get(id=snd) | ||
CollectionSound.objects.get(collection=collection, sound=sound).delete() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes 3 queries per item to delete. better to do either:
sounds = Sound.objects.filter(id__in=current_sounds)
collection.sounds.remove(*sounds)
(but careful with this option, because it won't trigger a delete signal, because it modifies the m2m field)
or maybe
sounds = Sound.objects.filter(id__in=current_sounds)
CollectionSound.objects.filter(collection=collection, sound__in=sounds).delete()
because the filter/delete will do only a delete query and no SELECT.
Issue(s)
#1029
Description
App creation and first implementations of basic functionalitites for collections.