Skip to content

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

Draft
wants to merge 48 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
813bd48
collections app creation and first basic implementations
quimmrc Jan 8, 2025
52543c4
migration: add collection attributes
quimmrc Jan 9, 2025
5f137eb
attempt to implement collection modals
quimmrc Jan 16, 2025
51adc61
models reformulation
quimmrc Jan 21, 2025
13a90da
Merge branch 'master' into collections
quimmrc Jan 22, 2025
2780959
add sound modal behavior tbd
quimmrc Jan 23, 2025
4c7c190
add sound modal collection selector
quimmrc Jan 23, 2025
0f47090
add sound to col from sound url
quimmrc Jan 23, 2025
fc55074
delete sound from collection func.
quimmrc Jan 23, 2025
b2e0277
deletion for CollectionSound + restrict add duplicates
quimmrc Jan 24, 2025
5ba088d
minor details correction
quimmrc Jan 24, 2025
ae9950b
delete and create collection functionalities
quimmrc Jan 27, 2025
d679849
Edit collection permissions
quimmrc Jan 28, 2025
58d845f
add collection parameter to settings.py
quimmrc Jan 28, 2025
3d7a668
add maintainer modal (fails)
quimmrc Jan 29, 2025
5ffea0b
add maintainers interface
quimmrc Jan 30, 2025
d5fed09
adequate variable namings for collection modals
quimmrc Jan 30, 2025
04ba23d
maintainers display in edit collection url
quimmrc Feb 3, 2025
e2bc70a
Merge branch 'master' into collections
quimmrc Feb 3, 2025
18c9548
changes from github review
quimmrc Feb 4, 2025
034528c
Merge branch 'master' into collections
quimmrc Feb 4, 2025
f62d388
db update + review + collect sound small player
quimmrc Feb 5, 2025
81affae
add sound to collection for all sound displays
quimmrc Feb 5, 2025
79787d1
remove maintainers from edit page
quimmrc Feb 6, 2025
82b3e2a
add maintainers
quimmrc Feb 6, 2025
d8fe151
Initial tests + create collections from scratch
quimmrc Feb 7, 2025
058d326
enable bookmark collection + public/private edition
quimmrc Feb 12, 2025
8fd039a
add sounds from small player + display Json success msg
quimmrc Feb 12, 2025
cd609e0
download collections
quimmrc Feb 13, 2025
f64d1b2
tests and miscellanious
quimmrc Feb 17, 2025
ff18e6b
order paginator query
quimmrc Feb 17, 2025
6a4d773
edit collection: sound display + bulk and adding maintainers temporary
quimmrc Feb 25, 2025
21cce70
users selectable for maintainers display
quimmrc Feb 25, 2025
33bcc39
add and remove maintainers through modal
quimmrc Mar 4, 2025
fb104f3
collections display approach
quimmrc Mar 5, 2025
02c50dc
update user permissions for maintainers and regular users (client and…
quimmrc Mar 6, 2025
21fedc7
maintainer form and modal improvements
quimmrc Mar 6, 2025
b56c9b0
num_sounds, downloads and max-sounds-collection
quimmrc Mar 11, 2025
53d4e1d
add sound to collection rework + edit collection errors display
quimmrc Mar 26, 2025
f4682e2
tests + adjustments in selectCollection form cleaning method
quimmrc Mar 27, 2025
f5d8bab
Merge branch 'master' into collections + collectionsound through mode…
quimmrc Mar 31, 2025
d6953ec
collectionsound through model + bulk_sounds_for_collection
quimmrc Mar 31, 2025
68b615a
update num sounds by triggers and remove unnecessary view
quimmrc Mar 31, 2025
65cfda7
collection info display approach + tests, comments, and remove unnece…
quimmrc Apr 1, 2025
fbadeb7
remove unnecessary code, rename files, avatar adjustments and export …
quimmrc Apr 2, 2025
6e9a5db
user creation test solution
quimmrc Apr 2, 2025
822953c
Merge branch 'master' into collections
quimmrc Apr 24, 2025
2c39aef
review improvements + parameters update in edit collection url
quimmrc Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions accounts/templatetags/display_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,12 @@ def display_user_top_donor(context, user, donated_amount):
@register.inclusion_tag('accounts/display_user.html', takes_context=True)
def display_user_comment(context, user, comment_created):
return display_user(context, user, size='comment', comment_created=comment_created)

@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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary comment

tvars = display_user(context, user, size='basic')
Copy link
Member

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

tvars.update({
'selected': selected,
})
return tvars
27 changes: 27 additions & 0 deletions accounts/templatetags/users_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Authors:
Copy link
Member

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?

# See AUTHORS file.
#


from django import template
from django.conf import settings

from accounts.models import User

register = template.Library()


@register.inclusion_tag('molecules/object_selector.html', takes_context=True)
def users_selector(context, users, selected_user_ids=[], show_select_all_buttons=False):
Copy link
Member

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

if users:
if not isinstance(users[0], User):
# users are passed as a list of user ids, retrieve the User objects from DB
users = User.objects.ordered_ids(users)
for user in users:
user.selected = user.id in selected_user_ids
return {
'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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unneeded comment

}
Empty file added current_db.txt
Empty file.
4 changes: 3 additions & 1 deletion freesound/context_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ 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)
'enable_collections': settings.ENABLE_COLLECTIONS,
'max_sounds_per_collection': settings.MAX_SOUNDS_PER_COLLECTION,
Copy link
Member

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.

Copy link
Contributor Author

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?

})

return tvars
4 changes: 4 additions & 0 deletions freesound/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
'silk',
'django_recaptcha',
'adminsortable',
'fscollections'
]

# Silk is the Request/SQL logging platform. We install it but leave it disabled
Expand Down Expand Up @@ -937,6 +938,9 @@
# -------------------------------------------------------------------------------
# Extra Freesound settings

ENABLE_COLLECTIONS = True
MAX_SOUNDS_PER_COLLECTION = 250

# 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
Expand Down
35 changes: 33 additions & 2 deletions freesound/static/bw-frontend/src/components/addSoundsModal.js
Copy link
Contributor Author

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)

Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,30 @@ const prepareAddSoundsModalAndFields = (container) => {
const removeSoundsButton = addSoundsButton.nextElementSibling;
removeSoundsButton.disabled = true;

const selectedSoundsDestinationElement = addSoundsButton.parentNode.parentNode.getElementsByClassName('bw-object-selector-container')[0];
const selectedSoundsDestinationElement = addSoundsButton.parentNode.parentNode.querySelector('.bw-object-selector-container[data-type="sounds"]');
initializeObjectSelector(selectedSoundsDestinationElement, (element) => {
removeSoundsButton.disabled = element.dataset.selectedIds == ""
});

const soundsInput = selectedSoundsDestinationElement.parentNode.parentNode.getElementsByTagName('input')[0];
if(soundsInput.disabled){
addSoundsButton.disabled = true
const checkboxes = selectedSoundsDestinationElement.querySelectorAll('span.bw-checkbox-container');
checkboxes.forEach(checkbox => {
checkbox.remove()
})
}

const soundsLabel = selectedSoundsDestinationElement.parentNode.parentNode.getElementsByTagName('label')[0];
const maxSounds = selectedSoundsDestinationElement.dataset.maxElements;
const maxSoundsHelpText = selectedSoundsDestinationElement.parentNode.parentNode.getElementsByClassName('helptext')[0]
if(maxSounds !== "None"){
if (soundsInput.value.split(',').length >= maxSounds){
addSoundsButton.disabled = true
maxSoundsHelpText.style.display = 'block';
}
}

removeSoundsButton.addEventListener('click', (evt) => {
evt.preventDefault();
const soundCheckboxes = selectedSoundsDestinationElement.querySelectorAll('input.bw-checkbox');
Expand All @@ -62,6 +81,12 @@ const prepareAddSoundsModalAndFields = (container) => {
updateObjectSelectorDataProperties(selectedSoundsDestinationElement);
const selectedSoundsHiddenInput = document.getElementById(addSoundsButton.dataset.selectedSoundsHiddenInputId);
selectedSoundsHiddenInput.value = selectedSoundsDestinationElement.dataset.unselectedIds;
if(maxSounds !== "None" && selectedSoundsHiddenInput.value.split(',').length < maxSounds){
addSoundsButton.disabled = false;
Copy link
Member

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.

maxSoundsHelpText.style.display = 'none';
}
if (soundsLabel){
soundsLabel.innerHTML = "Sounds in collection (" + selectedSoundsDestinationElement.children.length + ")"}
removeSoundsButton.disabled = true;
});

Expand All @@ -73,11 +98,17 @@ const prepareAddSoundsModalAndFields = (container) => {
const newSoundIds = serializedIdListToIntList(selectedSoundIds);
const combinedIds = combineIdsLists(currentSoundIds, newSoundIds);
selectedSoundsHiddenInput.value = combinedIds.join(',');
if(maxSounds !== "None" && selectedSoundsHiddenInput.value.split(',').length >= maxSounds){
addSoundsButton.disabled = true;
maxSoundsHelpText.style.display = 'block';
}
if (soundsLabel){
soundsLabel.innerHTML = "Sounds in collection (" + selectedSoundsDestinationElement.children.length + ")"}
Copy link
Contributor Author

@quimmrc quimmrc Apr 24, 2025

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).

initializeObjectSelector(selectedSoundsDestinationElement, (element) => {
removeSoundsButton.disabled = element.dataset.selectedIds == ""
});

});

});
});
}
Expand Down
159 changes: 159 additions & 0 deletions freesound/static/bw-frontend/src/components/collections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {dismissModal, handleGenericModal, handleGenericModalWithForm} from "./modal";
import {showToast} from "./toast";
import { initializeStuffInContainer } from "../utils/initHelper";
import {initializeObjectSelector, updateObjectSelectorDataProperties} from "./objectSelector";
import {combineIdsLists, serializedIdListToIntList} from "../utils/data";

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 initCollectionFormModal = () => {

// Modify the form structure to add a "Category" label inline with the select dropdown
const modalContainer = document.getElementById('addSoundToCollectionModal');
// To display the selector in case of an error in form, the following function is needed, despite it being called in
// handleGenericModal.
initializeStuffInContainer(modalContainer, false, false);
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 categorySelectElement = document.getElementById('id_collection');
const newCategoryNameElement = document.getElementById('id_new_collection_name');
toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement);
categorySelectElement.addEventListener('change', (event) => {
toggleNewCollectionNameDiv(categorySelectElement, newCategoryNameElement);
});}

const bindCollectionModals = (container) => {
const collectionButtons = [...container.querySelectorAll('[data-toggle="collection-modal"]')];
collectionButtons.forEach(element => {
if (element.dataset.alreadyBinded !== undefined){
return;
}
element.dataset.alreadyBinded = true;
element.addEventListener('click', (evt) => {
evt.preventDefault();
const modalUrlSplitted = element.dataset.modalContentUrl.split('/')
const soundId = parseInt(modalUrlSplitted[modalUrlSplitted.length - 3], 10)
if (!evt.altKey) {
handleGenericModalWithForm(element.dataset.modalContentUrl, () => {
Copy link
Contributor Author

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.

initCollectionFormModal(soundId, element.dataset.modalContentUrl);
}, undefined, (req) => {showToast(JSON.parse(req.responseText).message);}, () => {showToast('There were errors processing the form...');}, true, true, undefined, false);
}
});
});
}

// TODO: the AddMaintainerModal works really similarly to the AddSoundsModal, so maybe they could share behaviour
// However, it'd be interesting that checked users could be temporarly stored in the modal while other queries are performed
const handleAddMaintainersModal = (modalId, modalUrl, selectedMaintainersDestinationElement, onMaintainersSelectedCallback) => {
handleGenericModalWithForm(modalUrl,(modalContainer) => {
const inputElement = modalContainer.getElementsByTagName('input')[1];
inputElement.addEventListener('keypress', (evt) => {
if (evt.key === 'Enter'){
evt.preventDefault();
const baseUrl = modalUrl.split('?')[0];
handleAddMaintainersModal(modalId, `${baseUrl}?q=${inputElement.value}`, selectedMaintainersDestinationElement, onMaintainersSelectedCallback);
}
});

const objectSelectorElement = modalContainer.getElementsByClassName('bw-object-selector-container')[0];
initializeObjectSelector(objectSelectorElement, (element) => {
addSelectedMaintainersButton.disabled = element.dataset.selectedIds == ""
});

const addSelectedMaintainersButton = modalContainer.getElementsByTagName('button')[0];
addSelectedMaintainersButton.disabled = true;
addSelectedMaintainersButton.addEventListener('click', evt => {
evt.preventDefault();
const selectableMaintainerElements = [...modalContainer.getElementsByClassName('bw-selectable-object')];
selectableMaintainerElements.forEach(element => {
const checkbox = element.querySelectorAll('input.bw-checkbox')[0];
if (checkbox.checked) {
const clonedCheckbox = checkbox.cloneNode();
delete(clonedCheckbox.dataset.initialized);
clonedCheckbox.checked = false;
checkbox.parentNode.replaceChild(clonedCheckbox, checkbox)
element.classList.remove('selected');
selectedMaintainersDestinationElement.appendChild(element.parentNode);
}
});
onMaintainersSelectedCallback(objectSelectorElement.dataset.selectedIds)
dismissModal(modalId)
});
}, undefined, showToast('Maintainers added successfully'), showToast('There were some errors handling the modal'), true, true, undefined, false);
};

const prepareAddMaintainersModalAndFields = (container) => {
// select all buttons with a toggle that triggers the maintainers modal
const addMaintainersButtons = [...container.querySelectorAll('[data-toggle="add-maintainers-modal"]')];
// for each button, assign the next sibling to the remove maintainer button and disable it (since nothing will be selected by default)
addMaintainersButtons.forEach(addMaintainersButton => {
const removeMaintainersButton = addMaintainersButton.nextElementSibling;
removeMaintainersButton.disabled = true;

const selectedMaintainersDestinationElement = addMaintainersButton.parentNode.parentNode.querySelector('.bw-object-selector-container[data-type="users"]');
initializeObjectSelector(selectedMaintainersDestinationElement, (element) => {
removeMaintainersButton.disabled = element.dataset.selectedIds == ""
})


const maintainersInput = selectedMaintainersDestinationElement.parentNode.parentNode.getElementsByTagName('input')[0];
if(maintainersInput.disabled !== false){
addMaintainersButton.disabled = true;
addMaintainersButton.nextElementSibling.remove();
addMaintainersButton.remove();
const checkboxes = selectedMaintainersDestinationElement.querySelectorAll('span.bw-checkbox-container');
checkboxes.forEach(checkbox => {
checkbox.remove()
})
}

removeMaintainersButton.addEventListener('click', (evt) => {
evt.preventDefault();
const maintainerCheckboxes = selectedMaintainersDestinationElement.querySelectorAll('input.bw-checkbox');
maintainerCheckboxes.forEach(checkbox => {
if (checkbox.checked) {
checkbox.closest('.bw-selectable-object').parentNode.remove();
}
});
updateObjectSelectorDataProperties(selectedMaintainersDestinationElement);
const selectedMaintainersHiddenInput = document.getElementById(addMaintainersButton.dataset.selectedMaintainersHiddenInputId);
selectedMaintainersHiddenInput.value = selectedMaintainersDestinationElement.dataset.unselectedIds;
removeMaintainersButton.disabled = true;
});

addMaintainersButton.addEventListener('click', (evt) => {
evt.preventDefault();
handleAddMaintainersModal('addMaintainersModal', addMaintainersButton.dataset.modalUrl, selectedMaintainersDestinationElement, (selectedMaintainersIds) => {
const selectedMaintainersHiddenInput = document.getElementById(addMaintainersButton.dataset.selectedMaintainersHiddenInputId);
const currentMaintainersIds = serializedIdListToIntList(selectedMaintainersHiddenInput.value);
const newMaintainersIds = serializedIdListToIntList(selectedMaintainersIds);
const combinedIds = combineIdsLists(currentMaintainersIds, newMaintainersIds)
selectedMaintainersHiddenInput.value = combinedIds.join(',')
initializeObjectSelector(selectedMaintainersDestinationElement, (element) => {
removeMaintainersButton.disabled = element.dataset.selectedIds == ""
});
});
});
})};

export { bindCollectionModals, prepareAddMaintainersModalAndFields };
1 change: 0 additions & 1 deletion freesound/static/bw-frontend/src/components/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ const activateDefaultModalsIfParameters = () => {
activateModalsIfParameters('[data-toggle="modal-default-with-form"]', handleDefaultModalWithForm);
}


// Generic modals logic
const genericModalWrapper = document.getElementById('genericModalWrapper');

Expand Down
Loading