From 4b4c5f5ce500df95c63622c9828cc8513aa78c8b Mon Sep 17 00:00:00 2001 From: ffont Date: Fri, 8 Mar 2024 12:33:46 +0100 Subject: [PATCH 1/3] Add basic (non-functional) attribution modal https://github.com/MTG/freesound/issues/1681 --- freesound/urls.py | 1 + sounds/views.py | 9 +++++++++ templates/sounds/modal_attribution.html | 20 ++++++++++++++++++++ templates/sounds/sound.html | 4 ++-- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 templates/sounds/modal_attribution.html diff --git a/freesound/urls.py b/freesound/urls.py index 0a87522af..26ad58917 100644 --- a/freesound/urls.py +++ b/freesound/urls.py @@ -62,6 +62,7 @@ path('people//sounds//similar/', sounds.views.similar, name="sound-similar"), path('people//sounds//downloaders/', sounds.views.downloaders, name="sound-downloaders"), path('people//sounds//comments/', comments.views.for_sound, name="sound-comments"), + path('people//sounds//attribution/', sounds.views.attribution_modal, name="sound-attribution"), path('people//packs/', sounds.views.packs_for_user, name="packs-for-user"), path('people//packs//', sounds.views.pack, name="pack"), path('people//packs//section/stats/', sounds.views.pack_stats_section, name="pack-stats-section"), diff --git a/sounds/views.py b/sounds/views.py index 63508fd69..385415a1b 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -959,6 +959,15 @@ def flag(request, username, sound_id): return render(request, 'sounds/modal_flag_sound.html', tvars) +def attribution_modal(request, username, sound_id): + if not request.GET.get('ajax'): + # If not loaded as a modal, redirect to the sound page with parameter to open modal + return HttpResponseRedirect(reverse('sound', args=[username, sound_id]) + '?attribution=1') + sound = get_object_or_404(Sound, id=sound_id) + tvars = {'sound': sound} + return render(request, 'sounds/modal_attribution.html', tvars) + + def sound_short_link(request, sound_id): sound = get_object_or_404(Sound, id=sound_id) return redirect('sound', username=sound.user.username, sound_id=sound.id) diff --git a/templates/sounds/modal_attribution.html b/templates/sounds/modal_attribution.html new file mode 100644 index 000000000..c46a58c3f --- /dev/null +++ b/templates/sounds/modal_attribution.html @@ -0,0 +1,20 @@ +{% extends "molecules/modal_base.html" %} +{% load util %} + +{% block id %}soundAttributionModal{% endblock %} +{% block extra-class %}{% endblock %} +{% block aria-label %}Get attribution text{% endblock %} + +{% block body %} +
+
+

Get attribution text

+
+
+
+ +
+ +
+
+{% endblock %} diff --git a/templates/sounds/sound.html b/templates/sounds/sound.html index c9b936bcf..eb572da51 100644 --- a/templates/sounds/sound.html +++ b/templates/sounds/sound.html @@ -250,10 +250,10 @@
Comments
- {% bw_icon sound.license_bw_icon_name 'text-light-grey text-30' %} {{ sound.license.name_with_version }} + {% bw_icon sound.license_bw_icon_name 'text-light-grey text-30' %} {{ sound.license.name_with_version }}
- {{ sound.license.get_short_summary|safe }} More... + {{ sound.license.get_short_summary|safe }} Get attribution text...
{% endcache %} From 125b7f3fd816023f7874ba618362edbe2514250e Mon Sep 17 00:00:00 2001 From: ffont Date: Mon, 11 Mar 2024 16:46:56 +0100 Subject: [PATCH 2/3] Implement attribution modal --- accounts/views.py | 35 +++++++++++-- .../bw-frontend/src/components/modal.js | 6 +++ .../bw-frontend/src/components/select.js | 2 +- .../static/bw-frontend/src/pages/sound.js | 50 +++++++++++++++---- sounds/models.py | 16 ++++++ templates/accounts/attribution.html | 6 +-- templates/sounds/modal_attribution.html | 12 ++++- 7 files changed, 107 insertions(+), 20 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index d737edbdd..f4976519d 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -55,6 +55,7 @@ from django.utils.http import int_to_base36 from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt +from general.templatetags.absurl import url2absurl from oauth2_provider.models import AccessToken import tickets.views as TicketViews @@ -858,7 +859,9 @@ def attribution(request): @login_required def download_attribution(request): - content = {'csv': 'csv', 'txt': 'plain'} + content = {'csv': 'csv', + 'txt': 'plain', + 'json': 'json'} qs_sounds = Download.objects.annotate(download_type=Value('sound', CharField()))\ .values('download_type', 'sound_id', 'sound__user__username', 'sound__original_filename', @@ -871,9 +874,9 @@ def download_attribution(request): qs = qs_sounds.union(qs_packs).order_by('-created') download = request.GET.get('dl', '') + now = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + filename = f'{request.user}_{now}_attribution.{download}' if download in ['csv', 'txt']: - now = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') - filename = f'{request.user}_{now}_attribution.{download}' response = HttpResponse(content_type=f'text/{content[download]}') response['Content-Disposition'] = f'attachment; filename="{filename}"' output = io.StringIO() @@ -896,6 +899,32 @@ def download_attribution(request): row['created'])) response.writelines(output.getvalue()) return response + elif download == 'json': + output = [] + for row in qs: + if row['download_type'][0].upper() == 'S': + output.append({ + 'sound_url': url2absurl(reverse("sound", args=[row['sound__user__username'], row['sound_id']])), + 'sound_name': row['sound__original_filename'], + 'author_url': url2absurl(reverse("account", args=[row['sound__user__username']])), + 'author_name': row['sound__user__username'], + 'license_url': row['license__deed_url'] or row['sound__license__deed_url'], + 'license_name': license_with_version(row['license__name'] or row['sound__license__name'], + row['license__deed_url'] or row['sound__license__deed_url']), + 'timestamp': str(row['created']) + }) + elif row['download_type'][0].upper() == 'P': + output.append({ + 'pack_url': url2absurl(reverse("pack", args=[row['sound__user__username'], row['sound_id']])), + 'pack_name': row['sound__original_filename'], + 'author_url': url2absurl(reverse("account", args=[row['sound__user__username']])), + 'author_name': row['sound__user__username'], + 'license_url': row['license__deed_url'] or row['sound__license__deed_url'], + 'license_name': license_with_version(row['license__name'] or row['sound__license__name'], + row['license__deed_url'] or row['sound__license__deed_url']), + 'timestamp': str(row['created']) + }) + return JsonResponse(output, safe=False) else: return HttpResponseRedirect(reverse('accounts-attribution')) diff --git a/freesound/static/bw-frontend/src/components/modal.js b/freesound/static/bw-frontend/src/components/modal.js index 4b54bbd57..32738a00a 100644 --- a/freesound/static/bw-frontend/src/components/modal.js +++ b/freesound/static/bw-frontend/src/components/modal.js @@ -160,6 +160,12 @@ const handleGenericModal = (fetchContentUrl, onLoadedCallback, onClosedCallback, if (onLoadedCallback !== undefined){ onLoadedCallback(modalContainer); } + + // Trigger modal loaded event in case it should be used by other components + const event = new CustomEvent("modalLoaded", { + detail: {fetchContentUrl, modalContainer}, + }); + document.dispatchEvent(event); // If modal is activated with a param, add the param to the URL when opening the modal if (modalActivationParam !== undefined){ diff --git a/freesound/static/bw-frontend/src/components/select.js b/freesound/static/bw-frontend/src/components/select.js index f6734f749..550b562a7 100644 --- a/freesound/static/bw-frontend/src/components/select.js +++ b/freesound/static/bw-frontend/src/components/select.js @@ -21,7 +21,7 @@ function makeSelect(container) { selectElement.style.display = 'none'; const wrapper = wrapElement( - document.getElementById(selectElement.id), + selectElement, document.createElement('div'), select_i, selected_index_text diff --git a/freesound/static/bw-frontend/src/pages/sound.js b/freesound/static/bw-frontend/src/pages/sound.js index 53b03bdd4..282fcfb13 100644 --- a/freesound/static/bw-frontend/src/pages/sound.js +++ b/freesound/static/bw-frontend/src/pages/sound.js @@ -1,8 +1,8 @@ import './page-polyfills'; -import {showToast} from '../components/toast'; -import {playAtTime} from '../components/player/utils'; -import {handleGenericModalWithForm} from '../components/modal'; -import {addRecaptchaScriptTagToMainHead} from '../utils/recaptchaDynamicReload' +import { showToast } from '../components/toast'; +import { playAtTime } from '../components/player/utils'; +import { handleGenericModalWithForm, dismissModal } from '../components/modal'; +import { addRecaptchaScriptTagToMainHead } from '../utils/recaptchaDynamicReload' import { prepareAfterDownloadSoundModals } from '../components/afterDownloadModal.js'; const toggleEmbedCodeElement = document.getElementById('toggle-embed-code'); @@ -17,20 +17,23 @@ const urlParams = new URLSearchParams(window.location.search); prepareAfterDownloadSoundModals(); +const copyFromInputElement = (inputElement) => { + inputElement.select(); + inputElement.setSelectionRange(0, 99999); + inputElement.execCommand("copy"); + inputElement.getSelection().removeAllRanges(); +} + + const copyShareUrlToClipboard = (useFileURL) => { var shareLinkInputElement = shareLinkElement.getElementsByTagName("input")[0]; - console.log(shareLinkElement.dataset.staticFileUrl - , shareLinkElement.dataset.soundPageUrl) if (useFileURL) { shareLinkInputElement.value = shareLinkElement.dataset.staticFileUrl; } else { shareLinkInputElement.value = shareLinkElement.dataset.soundPageUrl; } - shareLinkInputElement.select(); - shareLinkInputElement.setSelectionRange(0, 99999); - document.execCommand("copy"); - showToast('Sound URL copied in the clipboard'); - document.getSelection().removeAllRanges(); + copyFromInputElement(shareLinkInputElement); + showToast('Sound URL copied to the clipboard'); } const toggleEmbedCode = () => { @@ -143,3 +146,28 @@ if (flagSoundModalParamValue) { flagSoundButton.addEventListener('click', (evt) => { handleFlagSoundModal(); }) + + +// Attribution modal +const handleAttributionModal = (modalContainer) => { + const selectElement = modalContainer.getElementsByTagName('select')[0]; + const textArea = modalContainer.getElementsByTagName('textarea')[0]; + textArea.value = selectElement.value; + selectElement.addEventListener('change', () => { + textArea.value = selectElement.value; + }); + + const buttons = modalContainer.getElementsByTagName('button'); + const copyButton = buttons[buttons.length - 1]; + copyButton.addEventListener('click', () => { + copyFromInputElement(textArea); + showToast('Attribution text copied to the clipboard'); + dismissModal(modalContainer.id); + }); +} + +document.addEventListener('modalLoaded', (evt) => { + if (evt.detail.modalContainer.id === 'soundAttributionModal') { + handleAttributionModal(evt.detail.modalContainer); + } +}); \ No newline at end of file diff --git a/sounds/models.py b/sounds/models.py index 10f139e02..514d3b661 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -53,6 +53,7 @@ from comments.models import Comment from freesound.celery import app as celery_app from general import tasks +from general.templatetags.absurl import url2absurl from geotags.models import GeoTag from ratings.models import SoundRating from general.templatetags.util import formatnumber @@ -928,6 +929,21 @@ def get_license_history(self): """ return [(slh.created, slh.license) for slh in self.soundlicensehistory_set.select_related('license').order_by('-created')] + + @cached_property + def attribution_texts(self): + return { + 'plain_text': f'{self.original_filename} by {self.user.username} -- {url2absurl(reverse("short-sound-link", args=[self.id]))} -- License: {self.license.name_with_version}', + 'html': f'{self.original_filename} by {self.user.username} | License: {self.license.name_with_version}', + 'json': json.dumps({ + 'sound_url': url2absurl(self.get_absolute_url()), + 'sound_name': self.original_filename, + 'author_url': url2absurl(reverse("account", args=[self.user.username])), + 'author_name': self.user.username, + 'license_url': self.license.deed_url, + 'license_name': self.license.name_with_version, + }) + } def get_sound_tags(self, limit=None): """ diff --git a/templates/accounts/attribution.html b/templates/accounts/attribution.html index 1646a3118..b146aa906 100644 --- a/templates/accounts/attribution.html +++ b/templates/accounts/attribution.html @@ -11,7 +11,7 @@ This is the list of files you have downloaded. When you use freesound samples under the Attribution or Attribution NonCommercial license, you have to credit the original creator of the sound in your work. This list makes it easy to do so. "S" means sound, "P" means pack. There are 3 flavors of this list: regular, html or plain text. - Alternatively, you can download the complete record of your downloaded sounds in csv or plain text formats. + Alternatively, you can download the complete record of your downloaded sounds in csv, plain text, or json formats.

@@ -26,7 +26,7 @@

Downloaded on {{group.grouper}}

    {% for download_item in group.list %} {% if download_item.download_type == 'sound' %} -
  • S: {{download_item.sound__original_filename }} by {{download_item.sound__user__username }} | License: {% if download_item.license__name %}{{ download_item.license__name|license_with_version:download_item.license__deed_url}}{% else %}{{ download_item.sound__license__name|license_with_version:download_item.sound__license__deed_url }}{% endif %}
  • +
  • S: {{download_item.sound__original_filename }} by {{download_item.sound__user__username }} | License: {% if download_item.license__name %}{{ download_item.license__name|license_with_version:download_item.license__deed_url}}{% else %}{{ download_item.sound__license__name|license_with_version:download_item.sound__license__deed_url }}{% endif %}
  • {% else %} {% comment %}NOTE: in the line below, even though we're displaying information about a pack download, we use download_item.X where X takes the same names as in the line above when we were displaying information about sound download. This is beacuse after doing the uninon of the two QuerySets (see accounts.views.attribution) the names of the columns are "unified" and taken from the main QuerySet{% endcomment %} @@ -45,7 +45,7 @@

    Downloaded on {{group.grouper}}

    {% for download_item in group.list %}     {% filter force_escape %} {% if download_item.download_type == 'sound' %} -
  • S: {{download_item.sound__original_filename }} by {{download_item.sound__user__username }} | License: {% if download_item.license__name %}{{ download_item.license__name|license_with_version:download_item.license__deed_url}}{% else %}{{ download_item.sound__license__name|license_with_version:download_item.sound__license__deed_url }}{% endif %}
  • +
  • S: {{download_item.sound__original_filename }} by {{download_item.sound__user__username }} | License: {% if download_item.license__name %}{{ download_item.license__name|license_with_version:download_item.license__deed_url}}{% else %}{{ download_item.sound__license__name|license_with_version:download_item.sound__license__deed_url }}{% endif %}
  • {% else %} {% comment %}NOTE: in the line below, even though we're displaying information about a pack download, we use download_item.X where X takes the same names as in the line above when we were displaying information about sound download. This is beacuse after doing the uninon of the two QuerySets (see accounts.views.attribution) the names of the columns are "unified" and taken from the main QuerySet{% endcomment %} diff --git a/templates/sounds/modal_attribution.html b/templates/sounds/modal_attribution.html index c46a58c3f..6a5f8430e 100644 --- a/templates/sounds/modal_attribution.html +++ b/templates/sounds/modal_attribution.html @@ -12,9 +12,17 @@

    Get attribution text

    - +
    + Format: + +
    +
    - +
    {% endblock %} From b564d47fdabc74745df3e563b696c632f6169579 Mon Sep 17 00:00:00 2001 From: ffont Date: Mon, 11 Mar 2024 16:55:01 +0100 Subject: [PATCH 3/3] Attribute sources of sounds as well https://github.com/MTG/freesound/issues/1681 --- sounds/models.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/sounds/models.py b/sounds/models.py index 514d3b661..79792c2c3 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -932,18 +932,27 @@ def get_license_history(self): @cached_property def attribution_texts(self): - return { - 'plain_text': f'{self.original_filename} by {self.user.username} -- {url2absurl(reverse("short-sound-link", args=[self.id]))} -- License: {self.license.name_with_version}', - 'html': f'{self.original_filename} by {self.user.username} | License: {self.license.name_with_version}', - 'json': json.dumps({ - 'sound_url': url2absurl(self.get_absolute_url()), - 'sound_name': self.original_filename, - 'author_url': url2absurl(reverse("account", args=[self.user.username])), - 'author_name': self.user.username, - 'license_url': self.license.deed_url, - 'license_name': self.license.name_with_version, - }) - } + attribution_texts = { + 'plain_text': f'{self.original_filename} by {self.user.username} -- {url2absurl(reverse("short-sound-link", args=[self.id]))} -- License: {self.license.name_with_version}', + 'html': f'{self.original_filename} by {self.user.username} | License: {self.license.name_with_version}', + 'json': json.dumps({ + 'sound_url': url2absurl(self.get_absolute_url()), + 'sound_name': self.original_filename, + 'author_url': url2absurl(reverse("account", args=[self.user.username])), + 'author_name': self.user.username, + 'license_url': self.license.deed_url, + 'license_name': self.license.name_with_version, + }) + } + if not self.sources.exists(): + return attribution_texts + else: + sources_attribution_texts = [s.attribution_texts for s in self.sources.all()] + attribution_texts['plain_text'] = "\n\n".join([attribution_texts['plain_text']] + [st['plain_text'] for st in sources_attribution_texts]) + attribution_texts['html'] = "
    \n".join([attribution_texts['html']] + [st['html'] for st in sources_attribution_texts]) + attribution_texts['json'] = json.dumps([json.loads(attribution_texts['json'])] + [json.loads(st['json']) for st in sources_attribution_texts]) + return attribution_texts + def get_sound_tags(self, limit=None): """