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.
- app_kit/admin_urls.py +5 -0
- app_kit/appbuilder/TaxonBuilder.py +31 -23
- app_kit/appbuilder/__pycache__/TaxonBuilder.cpython-313.pyc +0 -0
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/delete_all_manually_added_images.html +37 -0
- app_kit/features/taxon_profiles/templates/taxon_profiles/manage_taxon_profiles.html +16 -0
- app_kit/features/taxon_profiles/tests/__pycache__/test_views.cpython-313.pyc +0 -0
- app_kit/features/taxon_profiles/tests/__pycache__/test_zip_import.cpython-313.pyc +0 -0
- app_kit/features/taxon_profiles/tests/test_views.py +100 -3
- app_kit/features/taxon_profiles/tests/test_zip_import.py +4 -0
- app_kit/features/taxon_profiles/urls.py +3 -0
- app_kit/features/taxon_profiles/views.py +38 -1
- app_kit/forms.py +8 -0
- app_kit/generic_content_zip_import.py +174 -0
- app_kit/locale/de/LC_MESSAGES/django.mo +0 -0
- app_kit/locale/de/LC_MESSAGES/django.po +247 -93
- app_kit/taxonomy/lazy.py +6 -6
- app_kit/taxonomy/sources/custom/forms.py +1 -0
- app_kit/taxonomy/views.py +1 -1
- app_kit/templates/app_kit/ajax/list_images_and_licences_content.html +44 -0
- app_kit/templates/app_kit/ajax/manage_content_licence.html +25 -0
- app_kit/templates/app_kit/list_images_and_licences.html +25 -0
- app_kit/templates/app_kit/manage_app.html +10 -1
- app_kit/templatetags/app_tags.py +23 -3
- app_kit/tests/TESTS_ROOT/media_for_tests/test/imagestore/31/a6a11b61d65ee19c4c22caa0682288ff.jpg +0 -0
- app_kit/tests/__pycache__/test_generic_content_zip_import.cpython-313.pyc +0 -0
- app_kit/tests/test_generic_content_zip_import.py +145 -2
- app_kit/views.py +61 -2
- {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/METADATA +2 -2
- {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/RECORD +32 -27
- {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/WHEEL +1 -1
- {localcosmos_app_kit-0.9.15.dist-info → localcosmos_app_kit-0.9.17.dist-info}/licenses/LICENCE +0 -0
- {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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
Binary file
|
app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/delete_all_manually_added_images.html
ADDED
|
@@ -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 %}
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|