localcosmos-app-kit 0.9.15__py3-none-any.whl → 0.9.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. app_kit/admin_urls.py +5 -0
  2. app_kit/appbuilder/TaxonBuilder.py +31 -23
  3. app_kit/appbuilder/__pycache__/TaxonBuilder.cpython-313.pyc +0 -0
  4. app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/delete_all_manually_added_images.html +37 -0
  5. app_kit/features/taxon_profiles/templates/taxon_profiles/manage_taxon_profiles.html +16 -0
  6. app_kit/features/taxon_profiles/tests/__pycache__/test_views.cpython-313.pyc +0 -0
  7. app_kit/features/taxon_profiles/tests/__pycache__/test_zip_import.cpython-313.pyc +0 -0
  8. app_kit/features/taxon_profiles/tests/test_views.py +100 -3
  9. app_kit/features/taxon_profiles/tests/test_zip_import.py +4 -0
  10. app_kit/features/taxon_profiles/urls.py +3 -0
  11. app_kit/features/taxon_profiles/views.py +38 -1
  12. app_kit/forms.py +8 -0
  13. app_kit/generic_content_zip_import.py +174 -0
  14. app_kit/locale/de/LC_MESSAGES/django.mo +0 -0
  15. app_kit/locale/de/LC_MESSAGES/django.po +247 -93
  16. app_kit/taxonomy/lazy.py +6 -6
  17. app_kit/taxonomy/sources/custom/forms.py +1 -0
  18. app_kit/taxonomy/views.py +1 -1
  19. app_kit/templates/app_kit/ajax/list_images_and_licences_content.html +44 -0
  20. app_kit/templates/app_kit/ajax/manage_content_licence.html +25 -0
  21. app_kit/templates/app_kit/list_images_and_licences.html +25 -0
  22. app_kit/templates/app_kit/manage_app.html +10 -1
  23. app_kit/templatetags/app_tags.py +23 -3
  24. app_kit/tests/TESTS_ROOT/media_for_tests/test/imagestore/31/a6a11b61d65ee19c4c22caa0682288ff.jpg +0 -0
  25. app_kit/tests/__pycache__/test_generic_content_zip_import.cpython-313.pyc +0 -0
  26. app_kit/tests/test_generic_content_zip_import.py +145 -2
  27. app_kit/views.py +61 -2
  28. {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/METADATA +2 -2
  29. {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/RECORD +32 -27
  30. {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/WHEEL +1 -1
  31. {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/licenses/LICENCE +0 -0
  32. {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/top_level.txt +0 -0
app_kit/admin_urls.py CHANGED
@@ -136,4 +136,9 @@ urlpatterns = [
136
136
  views.ManageAppKitExternalMedia.as_view(), name='update_app_kit_external_media'),
137
137
  path('delete-external-media/<int:meta_app_id>/<int:pk>/',
138
138
  views.DeleteAppKitExternalMedia.as_view(), name='delete_app_kit_external_media'),
139
+ # licences
140
+ path('list-images-and-licences/<int:meta_app_id>/',
141
+ views.ListImagesAndLicences.as_view(), name='list_images_and_licences'),
142
+ path('manage-content-licence/<int:meta_app_id>/<int:registry_entry_id>/',
143
+ views.ManageContentLicence.as_view(), name='manage_content_licence'),
139
144
  ]
@@ -332,33 +332,41 @@ class TaxonSerializer:
332
332
  if name_type not in self.taxa_builder.cache['search']:
333
333
  self.taxa_builder.cache['search'][name_type] = {}
334
334
 
335
- if name in self.taxa_builder.cache['search'][name_type]:
336
- search_taxon_json = self.taxa_builder.cache['search'][name_type][name]
335
+ if name not in self.taxa_builder.cache['search'][name_type]:
336
+ # the same name might occur in different taxonomies
337
+ self.taxa_builder.cache['search'][name_type][name] = {}
337
338
 
338
- else:
339
-
340
- if not accepted_name_uuid:
341
- accepted_name_uuid = self.lazy_taxon.name_uuid
342
-
343
- if accepted_name_uuid:
344
- accepted_name_uuid = str(accepted_name_uuid)
345
-
346
- has_taxon_profile = False
347
- taxon_profile = self.get_taxon_profile()
348
- if taxon_profile:
349
- has_taxon_profile = True
339
+ if name in self.taxa_builder.cache['search'][name_type]:
340
+ # the same name might occur in different taxonomies
341
+ taxon_source = self.lazy_taxon.taxon_source
350
342
 
351
- search_taxon_json = self.serialize_extended()
343
+ if taxon_source in self.taxa_builder.cache['search'][name_type][name]:
344
+ search_taxon_json = self.taxa_builder.cache['search'][name_type][name][taxon_source]
352
345
 
353
- search_taxon_json.update({
354
- 'nameType': name_type,
355
- 'name': name,
356
- 'isPreferredName': is_preferred_name,
357
- 'acceptedNameUuid': accepted_name_uuid,
358
- 'hasTaxonProfile': has_taxon_profile,
359
- })
346
+ else:
360
347
 
361
- self.taxa_builder.cache['search'][name_type][name] = search_taxon_json
348
+ if not accepted_name_uuid:
349
+ accepted_name_uuid = self.lazy_taxon.name_uuid
350
+
351
+ if accepted_name_uuid:
352
+ accepted_name_uuid = str(accepted_name_uuid)
353
+
354
+ has_taxon_profile = False
355
+ taxon_profile = self.get_taxon_profile()
356
+ if taxon_profile:
357
+ has_taxon_profile = True
358
+
359
+ search_taxon_json = self.serialize_extended()
360
+
361
+ search_taxon_json.update({
362
+ 'nameType': name_type,
363
+ 'name': name,
364
+ 'isPreferredName': is_preferred_name,
365
+ 'acceptedNameUuid': accepted_name_uuid,
366
+ 'hasTaxonProfile': has_taxon_profile,
367
+ })
368
+
369
+ self.taxa_builder.cache['search'][name_type][name][taxon_source] = search_taxon_json
362
370
 
363
371
  search_taxon_json_copy = copy.deepcopy(search_taxon_json)
364
372
 
@@ -0,0 +1,37 @@
1
+ {% extends "localcosmos_server/modals/modal_form.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block title %}
5
+ {% trans 'Delete All Manually Added Images' %}
6
+ {% endblock %}
7
+
8
+
9
+ {% block action %}
10
+ {% url 'delete_all_manually_added_taxon_profile_images' meta_app.id taxon_profiles.id %}
11
+ {% endblock %}
12
+
13
+ {% block body %}
14
+ <p>
15
+ {% if success %}
16
+ <div class="alert alert-success">
17
+ {% trans 'All manually added images have been successfully deleted.' %}
18
+ </div>
19
+ {% else %}
20
+ <div class="alert alert-danger">
21
+ {% trans 'Are you sure you want to delete all manually added images across all taxon profiles? This action cannot be undone.' %}
22
+ </div>
23
+ {% endif %}
24
+ </p>
25
+ {% endblock %}
26
+
27
+ {% block footer %}
28
+ {% if success %}
29
+ {% include 'localcosmos_server/modals/footers/close.html' %}
30
+ {% else %}
31
+ {% include 'localcosmos_server/modals/footers/delete.html' %}
32
+ {% endif %}
33
+ {% endblock %}
34
+
35
+ {% block script %}
36
+
37
+ {% endblock %}
@@ -168,6 +168,22 @@
168
168
  </div>
169
169
  </div>
170
170
  </div>
171
+
172
+ <div class="mt-5">
173
+ <h3 class="text-danger">
174
+ {% trans 'Danger Zone' %}
175
+ </h3>
176
+ </div>
177
+ <div class="card mt-3">
178
+ <div class="card-body">
179
+ <div>
180
+ <button type="button" class="btn btn-outline-danger xhr" ajax-target="ModalContent" data-url="{% url 'delete_all_manually_added_taxon_profile_images' meta_app.id generic_content.id %}">{% trans 'Delete all Taxon Profile images that have been added manually' %}</button>
181
+ </div>
182
+ <div class="mt-3">
183
+ {% trans 'This will delete all manually added images across all taxon profiles.' %}
184
+ </div>
185
+ </div>
186
+ </div>
171
187
  {% endblock %}
172
188
 
173
189
  {% block extra_script %}
@@ -5,7 +5,7 @@ from django.urls import reverse
5
5
 
6
6
  from app_kit.tests.common import test_settings
7
7
 
8
- from app_kit.models import MetaAppGenericContent, ContentImage
8
+ from app_kit.models import MetaAppGenericContent, ContentImage, MetaApp
9
9
 
10
10
  from app_kit.tests.mixins import (WithMetaApp, WithTenantClient, WithUser, WithLoggedInUser, WithAjaxAdminOnly,
11
11
  WithAdminOnly, ViewTestMixin, WithImageStore, WithMedia, WithFormTest)
@@ -20,7 +20,8 @@ from app_kit.features.taxon_profiles.views import (ManageTaxonProfiles, ManageTa
20
20
  DeleteTaxonProfilesNavigationEntry, GetTaxonProfilesNavigation, ManageNavigationImage,
21
21
  DeleteNavigationImage, DeleteTaxonProfilesNavigationEntryTaxon, DeleteTaxonTextTypeCategory,
22
22
  ChangeNavigationEntryPublicationStatus, ManageTaxonTextTypeCategory, ManageTaxonTextSet,
23
- DeleteTaxonTextSet, GetTaxonTextsManagement, SetTaxonTextSetForTaxonProfile)
23
+ DeleteTaxonTextSet, GetTaxonTextsManagement, SetTaxonTextSetForTaxonProfile,
24
+ DeleteAllManuallyAddedTaxonProfileImages)
24
25
 
25
26
  from app_kit.features.taxon_profiles.models import (TaxonProfiles, TaxonProfile, TaxonTextType,
26
27
  TaxonText, TaxonProfilesNavigation, TaxonProfilesNavigationEntry,
@@ -2590,4 +2591,100 @@ class TestSetTaxonTextSetForTaxonProfile(WithTaxonProfile, WithTaxonProfiles, Vi
2590
2591
 
2591
2592
  self.taxon_profile.refresh_from_db()
2592
2593
 
2593
- self.assertEqual(self.taxon_profile.taxon_text_set, text_set)
2594
+ self.assertEqual(self.taxon_profile.taxon_text_set, text_set)
2595
+
2596
+
2597
+ class TestDeleteAllManuallyAddedTaxonProfileImages(WithTaxonProfile, WithTaxonProfiles, ViewTestMixin,
2598
+ WithImageStore, WithMedia, WithAjaxAdminOnly, WithUser, WithLoggedInUser, WithMetaApp, WithTenantClient, TenantTestCase):
2599
+
2600
+ url_name = 'delete_all_manually_added_taxon_profile_images'
2601
+ view_class = DeleteAllManuallyAddedTaxonProfileImages
2602
+
2603
+ def get_url_kwargs(self):
2604
+ url_kwargs = {
2605
+ 'meta_app_id': self.meta_app.id,
2606
+ 'taxon_profiles_id': self.generic_content.id,
2607
+ }
2608
+ return url_kwargs
2609
+
2610
+ def create_content_images(self):
2611
+
2612
+ # taxon image
2613
+ self.taxon_image_store = self.create_image_store()
2614
+
2615
+ # add image to nature guide meta node
2616
+ self.meta_node_image = self.create_content_image(self.meta_app, self.user)
2617
+
2618
+ # add image to taxon profile
2619
+ self.taxon_profile_image = self.create_content_image(self.taxon_profile, self.user)
2620
+
2621
+
2622
+
2623
+ @test_settings
2624
+ def test_set_taxon_profiles(self):
2625
+
2626
+ view = self.get_view()
2627
+ view.meta_app = self.meta_app
2628
+ view.set_taxon_profiles(**view.kwargs)
2629
+
2630
+ self.assertEqual(view.taxon_profiles, self.generic_content)
2631
+
2632
+ @test_settings
2633
+ def test_get_context_data(self):
2634
+
2635
+ view = self.get_view()
2636
+ view.meta_app = self.meta_app
2637
+ view.set_taxon_profiles(**view.kwargs)
2638
+
2639
+ context = view.get_context_data(**view.kwargs)
2640
+
2641
+ self.assertEqual(context['taxon_profiles'], self.generic_content)
2642
+ self.assertFalse(context['success'])
2643
+
2644
+ @test_settings
2645
+ def test_post(self):
2646
+
2647
+ view = self.get_view()
2648
+ view.meta_app = self.meta_app
2649
+ view.set_taxon_profiles(**view.kwargs)
2650
+
2651
+ self.create_content_images()
2652
+
2653
+ taxon_profile_ctype = ContentType.objects.get_for_model(TaxonProfile)
2654
+ meta_app_ctype = ContentType.objects.get_for_model(MetaApp)
2655
+
2656
+ # verify image exists
2657
+ images_qry = ContentImage.objects.filter(
2658
+ content_type=taxon_profile_ctype,
2659
+ object_id=self.taxon_profile.id,
2660
+ )
2661
+ self.assertTrue(images_qry.exists())
2662
+
2663
+ meta_app_images_qry = ContentImage.objects.filter(
2664
+ content_type=meta_app_ctype,
2665
+ object_id=self.meta_app.id,
2666
+ )
2667
+
2668
+ self.assertTrue(meta_app_images_qry.exists())
2669
+
2670
+ # perform post to delete images
2671
+ response = view.post(view.request, **view.kwargs)
2672
+
2673
+ self.assertEqual(response.status_code, 200)
2674
+ self.assertTrue(response.context_data['success'])
2675
+
2676
+ # verify images deleted
2677
+ images_qry = ContentImage.objects.filter(
2678
+ content_type=taxon_profile_ctype,
2679
+ object_id=self.taxon_profile.id,
2680
+ )
2681
+ self.assertFalse(images_qry.exists())
2682
+
2683
+ # verify meta app image still exists
2684
+ meta_app_images_qry = ContentImage.objects.filter(
2685
+ content_type=meta_app_ctype,
2686
+ object_id=self.meta_app.id,
2687
+ )
2688
+ self.assertTrue(meta_app_images_qry.exists())
2689
+
2690
+
@@ -352,6 +352,10 @@ class TestTaxonProfilesZipImporter(WithMedia, WithTaxonProfiles, WithUser, WithM
352
352
  taxon_latname='Fraxinus excelsior', morphotype='leaf')
353
353
  self.assertEqual(fraxinus_excielior_leaf_profile.short_profile,'Fraxinus excelsior morphotype leaf short profile')
354
354
 
355
+
356
+
357
+
358
+
355
359
  @test_settings
356
360
  def test_partial_import(self):
357
361
  importer = self.get_zip_importer()
@@ -119,4 +119,7 @@ urlpatterns = [
119
119
  # move images
120
120
  path('move-taxon-profile-image-to-section/<int:meta_app_id>/<int:taxon_profile_id>/<int:content_image_id>/',
121
121
  views.MoveImageToSection.as_view(), name='move_taxon_profile_image_to_section'),
122
+ # delete all manually added images
123
+ path('delete-all-manually-added-taxon-profile-images/<int:meta_app_id>/<int:taxon_profiles_id>/',
124
+ views.DeleteAllManuallyAddedTaxonProfileImages.as_view(), name='delete_all_manually_added_taxon_profile_images'),
122
125
  ]
@@ -1580,4 +1580,41 @@ class MoveImageToSection(MetaAppMixin, FormView):
1580
1580
  context['success'] = True
1581
1581
 
1582
1582
  return self.render_to_response(context)
1583
-
1583
+
1584
+
1585
+ class DeleteAllManuallyAddedTaxonProfileImages(MetaAppMixin, TemplateView):
1586
+
1587
+ template_name = 'taxon_profiles/ajax/delete_all_manually_added_images.html'
1588
+
1589
+ @method_decorator(ajax_required)
1590
+ def dispatch(self, request, *args, **kwargs):
1591
+ self.set_taxon_profiles(**kwargs)
1592
+ return super().dispatch(request, *args, **kwargs)
1593
+
1594
+ def set_taxon_profiles(self, **kwargs):
1595
+ self.taxon_profiles = TaxonProfiles.objects.get(pk=kwargs['taxon_profiles_id'])
1596
+
1597
+ def get_context_data(self, **kwargs):
1598
+ context = super().get_context_data(**kwargs)
1599
+ context['taxon_profiles'] = self.taxon_profiles
1600
+ context['success'] = False
1601
+ return context
1602
+
1603
+ def post(self, request, *args, **kwargs):
1604
+
1605
+ taxon_profile_ctype = ContentType.objects.get_for_model(TaxonProfile)
1606
+ all_taxon_profiles = TaxonProfile.objects.filter(taxon_profiles=self.taxon_profiles)
1607
+
1608
+ images_to_delete = ContentImage.objects.filter(
1609
+ content_type=taxon_profile_ctype,
1610
+ object_id__in=all_taxon_profiles.values_list('id', flat=True)
1611
+ )
1612
+
1613
+ deleted_count = images_to_delete.count()
1614
+ images_to_delete.delete()
1615
+
1616
+ context = self.get_context_data(**self.kwargs)
1617
+ context['deleted_count'] = deleted_count
1618
+ context['success'] = True
1619
+
1620
+ return self.render_to_response(context)
app_kit/forms.py CHANGED
@@ -18,6 +18,8 @@ from app_kit.appbuilder.AppBuilderBase import AppBuilderBase
18
18
  from taxonomy.models import MetaVernacularNames
19
19
  from taxonomy.lazy import LazyTaxon
20
20
 
21
+ from content_licencing.mixins import LicencingFormMixin
22
+
21
23
  import base64, math, uuid
22
24
 
23
25
  from .definitions import TEXT_LENGTH_RESTRICTIONS
@@ -130,7 +132,13 @@ class AddLanguageForm(forms.Form):
130
132
  '''
131
133
  from localcosmos_server.forms import (ManageContentImageForm, ManageContentImageWithTextForm,
132
134
  ManageLocalizedContentImageForm, OptionalContentImageForm)
135
+
136
+ class ManageContentLicenceForm(LicencingFormMixin):
137
+ content_field = None
133
138
 
139
+ def __init__(self, content_field, *args, **kwargs):
140
+ self.content_field = content_field
141
+ super().__init__(*args, **kwargs)
134
142
 
135
143
  class GenericContentOptionsForm(forms.Form):
136
144
 
@@ -570,6 +570,180 @@ class GenericContentZipImporter:
570
570
  'valid_media_types': ', '.join(AVAILABLE_EXTERNAL_MEDIA_TYPES),
571
571
  }
572
572
  self.add_cell_error(self.workbook_filename, sheet_name, 'B', row_index, message)
573
+
574
+ if external_media_data['media_type']:
575
+ # call specific validator method for each media type
576
+ validator_method_name = 'validate_external_media_type_{0}'.format(
577
+ external_media_data['media_type'].lower()
578
+ )
579
+ validator_method = getattr(self, validator_method_name, None)
580
+ if validator_method:
581
+ validator_method(external_media_data, sheet_name, row_index)
582
+
583
+
584
+
585
+ '''
586
+ image, youTube, vimeo, mp3, wav, pdf, website, file
587
+ '''
588
+ def validate_external_media_type_image(self, external_media_data, sheet_name, row_index):
589
+ # check that the url ends with a valid image extension
590
+ image_extension = os.path.splitext(external_media_data['url'])[1].lower()
591
+ if image_extension not in VALID_IMAGE_FORMATS:
592
+ message = _('Invalid image format in URL: %(cell_value)s. Valid formats are: %(valid_formats)s') % {
593
+ 'cell_value': image_extension,
594
+ 'valid_formats': ', '.join(VALID_IMAGE_FORMATS),
595
+ }
596
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
597
+
598
+
599
+
600
+ def validate_external_media_type_youtube(self, external_media_data, sheet_name, row_index):
601
+ url = (external_media_data.get('url') or '').strip()
602
+ if not url:
603
+ message = _('Invalid YouTube URL: empty value')
604
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
605
+ return
606
+
607
+ try:
608
+ from urllib.parse import urlparse, parse_qs
609
+ parsed = urlparse(url)
610
+ netloc = (parsed.netloc or '').lower()
611
+ path = parsed.path or ''
612
+ query = parse_qs(parsed.query or '')
613
+ except Exception:
614
+ message = _('Invalid YouTube URL: %(cell_value)s') % {'cell_value': url}
615
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
616
+ return
617
+
618
+ if parsed.scheme not in ('http', 'https'):
619
+ message = _('Invalid YouTube URL (scheme must be http/https): %(cell_value)s') % {'cell_value': url}
620
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
621
+ return
622
+
623
+ video_id = None
624
+
625
+ # youtu.be/<id>
626
+ if netloc.endswith('youtu.be'):
627
+ candidate = path.lstrip('/')
628
+ m = re.match(r'^([A-Za-z0-9_-]{11})(?:$|[/?#])', candidate)
629
+ if m:
630
+ video_id = m.group(1)
631
+
632
+ # *.youtube.com/... variants
633
+ if not video_id and netloc.endswith('youtube.com'):
634
+ # watch?v=<id>
635
+ vvals = query.get('v')
636
+ if vvals:
637
+ v = vvals[0]
638
+ if re.fullmatch(r'[A-Za-z0-9_-]{11}', v or ''):
639
+ video_id = v
640
+ # /embed/<id>, /v/<id>, /shorts/<id>
641
+ if not video_id:
642
+ m = re.search(r'/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})(?:$|[/?#])', path)
643
+ if m:
644
+ video_id = m.group(1)
645
+
646
+ if not video_id:
647
+ message = _('Invalid YouTube URL or video id not found: %(cell_value)s') % {'cell_value': url}
648
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
649
+ return
650
+
651
+ # Optionally: could normalize/store the ID if needed
652
+ # external_media_data['video_id'] = video_id
653
+
654
+ def validate_external_media_type_vimeo(self, external_media_data, sheet_name, row_index):
655
+ url = (external_media_data.get('url') or '').strip()
656
+ if not url:
657
+ message = _('Invalid Vimeo URL: empty value')
658
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
659
+ return
660
+
661
+ try:
662
+ from urllib.parse import urlparse
663
+ parsed = urlparse(url)
664
+ netloc = (parsed.netloc or '').lower()
665
+ path = parsed.path or ''
666
+ except Exception:
667
+ message = _('Invalid Vimeo URL: %(cell_value)s') % {'cell_value': url}
668
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
669
+ return
670
+
671
+ if parsed.scheme not in ('http', 'https'):
672
+ message = _('Invalid Vimeo URL (scheme must be http/https): %(cell_value)s') % {'cell_value': url}
673
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
674
+ return
675
+
676
+ video_id = None
677
+
678
+ if netloc.endswith('vimeo.com'):
679
+ # player.vimeo.com/video/<id>
680
+ m = re.search(r'/video/(\d+)(?:$|[/?#])', path)
681
+ if m:
682
+ video_id = m.group(1)
683
+ # vimeo.com/<id> or any path ending with /<digits>
684
+ if not video_id:
685
+ m = re.search(r'/(\d+)(?:$|[/?#])', path)
686
+ if m:
687
+ video_id = m.group(1)
688
+
689
+ if not video_id:
690
+ message = _('Invalid Vimeo URL or video id not found: %(cell_value)s') % {'cell_value': url}
691
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
692
+ return
693
+
694
+ # Optionally: could normalize/store the ID if needed
695
+ # external_media_data['video_id'] = video_id
696
+
697
+
698
+ def validate_external_media_type_mp3(self, external_media_data, sheet_name, row_index):
699
+ # has to end with .mp3
700
+ if not external_media_data['url'].lower().endswith('.mp3'):
701
+ message = _('Invalid mp3 format in URL: %(cell_value)s. URL has to end with .mp3') % {
702
+ 'cell_value': external_media_data['url'],
703
+ }
704
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
705
+
706
+ def validate_external_media_type_wav(self, external_media_data, sheet_name, row_index):
707
+ # has to end with .wav
708
+ if not external_media_data['url'].lower().endswith('.wav'):
709
+ message = _('Invalid wav format in URL: %(cell_value)s. URL has to end with .wav') % {
710
+ 'cell_value': external_media_data['url'],
711
+ }
712
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
713
+
714
+ def validate_external_media_type_pdf(self, external_media_data, sheet_name, row_index):
715
+ # has to end with .pdf
716
+ if not external_media_data['url'].lower().endswith('.pdf'):
717
+ message = _('Invalid pdf format in URL: %(cell_value)s. URL has to end with .pdf') % {
718
+ 'cell_value': external_media_data['url'],
719
+ }
720
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
721
+
722
+
723
+ def validate_external_media_type_website(self, external_media_data, sheet_name, row_index):
724
+ # Only flag as invalid if the PATH (not the domain) ends with a file extension
725
+ try:
726
+ from urllib.parse import urlparse
727
+ parsed = urlparse((external_media_data.get('url') or '').strip())
728
+ path = parsed.path or ''
729
+ except Exception:
730
+ path = ''
731
+
732
+ # Detect file-like endings in path (e.g., /file.jpg, /index.html)
733
+ if re.search(r'/[^/]+\.[a-zA-Z0-9]{2,5}$', path):
734
+ message = _('Invalid website format in URL: %(cell_value)s. URL should not end with a file extension.') % {
735
+ 'cell_value': external_media_data['url'],
736
+ }
737
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
738
+
739
+
740
+ def validate_external_media_type_file(self, external_media_data, sheet_name, row_index):
741
+ # file endings: check if the url has a file extension
742
+ if not re.search(r'\.[a-zA-Z0-9]{2,5}($|\?)', external_media_data['url']):
743
+ message = _('Invalid file format in URL: %(cell_value)s. URL has to end with a file extension.') % {
744
+ 'cell_value': external_media_data['url'],
745
+ }
746
+ self.add_cell_error(self.workbook_filename, sheet_name, 'A', row_index, message)
573
747
 
574
748
 
575
749
  def add_cell_error(self, filename, sheet_name, column, row, message):
Binary file