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/taxonomy/lazy.py
CHANGED
|
@@ -115,14 +115,14 @@ class LazyTaxon(LazyTaxonBase):
|
|
|
115
115
|
|
|
116
116
|
if db_lazy_taxon.taxon_nuid != self.taxon_nuid:
|
|
117
117
|
self.changed_taxon_nuid_in_reference = True
|
|
118
|
-
self.reference_errors.append(_('Taxon %s has changed its position in %s') %
|
|
118
|
+
self.reference_errors.append(_('Taxon %(taxon)s has changed its position in %(tree)s') % {'taxon': self, 'tree': verbose_taxon_source})
|
|
119
119
|
|
|
120
120
|
if str(taxon.name_uuid) != str(self.name_uuid):
|
|
121
121
|
self.changed_name_uuid_in_reference = True
|
|
122
|
-
self.reference_errors.append(_('Taxon %s has changed its identifier in %s') %
|
|
122
|
+
self.reference_errors.append(_('Taxon %(taxon)s has changed its identifier in %(tree)s') % {'taxon': self, 'tree': verbose_taxon_source})
|
|
123
123
|
|
|
124
124
|
if tree_query.count() > 1:
|
|
125
|
-
self.reference_errors.append(_('Taxon %s found multiple times in %s') %
|
|
125
|
+
self.reference_errors.append(_('Taxon %(taxon)s found multiple times in %(tree)s') % {'taxon': self, 'tree': verbose_taxon_source})
|
|
126
126
|
|
|
127
127
|
else:
|
|
128
128
|
synonyms_model = self.models.TaxonSynonymModel
|
|
@@ -135,11 +135,11 @@ class LazyTaxon(LazyTaxonBase):
|
|
|
135
135
|
self.reference_synonym = synonym
|
|
136
136
|
accepted_name = synonym.taxon
|
|
137
137
|
self.reference_accepted_name = LazyTaxon(instance=accepted_name)
|
|
138
|
-
self.reference_errors.append(_('Taxon %s not found as accepted name, but as synonym of %s') %
|
|
139
|
-
accepted_name)
|
|
138
|
+
self.reference_errors.append(_('Taxon %(taxon)s not found as accepted name, but as synonym of %(accepted_name)s') % {'taxon': self,
|
|
139
|
+
'accepted_name': accepted_name})
|
|
140
140
|
|
|
141
141
|
else:
|
|
142
|
-
self.reference_errors.append(_('Taxon %s not found in %s') %
|
|
142
|
+
self.reference_errors.append(_('Taxon %(taxon)s not found in %(tree)s') % {'taxon': self, 'tree': verbose_taxon_source})
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
taxon_latnames_query = self.models.TaxonTreeModel.objects.filter(
|
app_kit/taxonomy/views.py
CHANGED
|
@@ -79,7 +79,7 @@ class TaxonTreeView(TemplateView):
|
|
|
79
79
|
if self.taxon:
|
|
80
80
|
children_nuid_length = len(self.taxon.taxon_nuid) + 3
|
|
81
81
|
taxa = self.models.TaxonTreeModel.objects.annotate(nuid_len=Length('taxon_nuid')).filter(
|
|
82
|
-
taxon_nuid__startswith=self.taxon.taxon_nuid, nuid_len=children_nuid_length)
|
|
82
|
+
taxon_nuid__startswith=self.taxon.taxon_nuid, nuid_len=children_nuid_length).order_by('taxon_latname')
|
|
83
83
|
|
|
84
84
|
else:
|
|
85
85
|
taxa = self.get_root_taxa()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% load i18n el_pagination_tags app_tags %}
|
|
2
|
+
|
|
3
|
+
{% paginate 28 registry_entries %}
|
|
4
|
+
|
|
5
|
+
{% show_pages %}
|
|
6
|
+
|
|
7
|
+
<div class="row">
|
|
8
|
+
{% for entry in registry_entries %}
|
|
9
|
+
<div class="col-12 col-md-6 col-lg-3">
|
|
10
|
+
{% get_object_from_licence entry as licenced_object %}
|
|
11
|
+
{% if licenced_object.is_imagestore %}
|
|
12
|
+
<div class="row mb-2">
|
|
13
|
+
<div class="col-6">
|
|
14
|
+
{% if licenced_object.object %}
|
|
15
|
+
<img src="{{ licenced_object.object.source_image.url }}" class="img-thumbnail img-fluid" alt="Content Image">
|
|
16
|
+
{% else %}
|
|
17
|
+
deleted image
|
|
18
|
+
{% endif %}
|
|
19
|
+
</div>
|
|
20
|
+
<div class="col-6 p-1">
|
|
21
|
+
<small>
|
|
22
|
+
{% trans 'Author' %}: {{ entry.creator_name}}<br>
|
|
23
|
+
{% trans 'Licence' %}: {{ entry.licence }} {{ entry.licence_version }}
|
|
24
|
+
{% if entry.creator_link %}
|
|
25
|
+
<br>(<a href="{{ entry.creator_link }}" target="_blank" rel="noopener noreferrer">{% trans 'Link to author' %}</a>)
|
|
26
|
+
{% endif %}
|
|
27
|
+
{% if entry.source_link %}
|
|
28
|
+
<br>(<a href="{{ entry.source_link }}" target="_blank" rel="noopener noreferrer">{% trans 'Link to source' %}</a>)
|
|
29
|
+
{% endif %}
|
|
30
|
+
<br>
|
|
31
|
+
{% if licenced_object.object %}
|
|
32
|
+
<a class="btn btn-xs btn-outline-primary mt-2 xhr" ajax-target="ModalContent" href="{% url 'manage_content_licence' meta_app.id entry.id %}">{% trans 'Edit licence' %}</a>
|
|
33
|
+
{% endif %}
|
|
34
|
+
</small>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
{% else %}
|
|
38
|
+
|
|
39
|
+
{% endif %}
|
|
40
|
+
</div>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{% show_pages %}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{% extends 'localcosmos_server/modals/modal_form.html' %}
|
|
2
|
+
{% load i18n localcosmos_tags %}
|
|
3
|
+
|
|
4
|
+
{% block action %}
|
|
5
|
+
{% url 'manage_content_licence' meta_app.id registry_entry.id %}
|
|
6
|
+
{% endblock %}
|
|
7
|
+
|
|
8
|
+
{% block title %}
|
|
9
|
+
{% blocktrans %}Manage Licence{% endblocktrans %}
|
|
10
|
+
{% endblock %}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
{% block footer %}
|
|
14
|
+
{% include 'localcosmos_server/modals/footers/save.html' %}
|
|
15
|
+
{% endblock %}
|
|
16
|
+
|
|
17
|
+
{% block script %}
|
|
18
|
+
{% if success %}
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
$("#LargeModal").modal("hide");
|
|
22
|
+
window.location.reload();
|
|
23
|
+
</script>
|
|
24
|
+
{% endif %}
|
|
25
|
+
{% endblock %}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{% extends 'app_kit/base.html' %}
|
|
2
|
+
{% load i18n static app_tags localcosmos_tags %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
<br>
|
|
6
|
+
<div class="container">
|
|
7
|
+
<div class="row">
|
|
8
|
+
<div class="col-12">
|
|
9
|
+
<br>
|
|
10
|
+
<h3>
|
|
11
|
+
{% trans 'Images and Licences' %}
|
|
12
|
+
</h3>
|
|
13
|
+
|
|
14
|
+
<hr>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="row">
|
|
18
|
+
<div class="col-12">
|
|
19
|
+
<div id="images-and-licences-content">
|
|
20
|
+
{% include 'app_kit/ajax/list_images_and_licences_content.html' %}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
{% endblock %}
|
|
@@ -60,7 +60,16 @@
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
{% block generic_content_specific %}
|
|
63
|
-
|
|
63
|
+
<div>
|
|
64
|
+
<div class="card">
|
|
65
|
+
<div class="card-body">
|
|
66
|
+
<h5 class="card-title">{% trans 'Images and Licences' %}</h5>
|
|
67
|
+
<div>
|
|
68
|
+
<a href="{% url 'list_images_and_licences' meta_app.id %}" class="btn btn-outline-primary">{% trans 'Manage Images and Licences' %}</a>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
64
73
|
{% endblock %}
|
|
65
74
|
|
|
66
75
|
{% block extra_script %}
|
app_kit/templatetags/app_tags.py
CHANGED
|
@@ -9,7 +9,7 @@ from django.contrib.staticfiles import finders
|
|
|
9
9
|
from django.db.models import Q
|
|
10
10
|
|
|
11
11
|
from app_kit.models import MetaApp, AppKitExternalMedia
|
|
12
|
-
from localcosmos_server.models import EXTERNAL_MEDIA_TYPES
|
|
12
|
+
from localcosmos_server.models import EXTERNAL_MEDIA_TYPES, ServerImageStore
|
|
13
13
|
|
|
14
14
|
from app_kit.features.backbonetaxonomy.models import TaxonRelationship, TaxonRelationshipType
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ def ranged(number):
|
|
|
28
28
|
return range(1,int(number)+1)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
from app_kit.models import ContentImage
|
|
31
|
+
from app_kit.models import ContentImage, ImageStore
|
|
32
32
|
@register.simple_tag
|
|
33
33
|
def content_image(instance, image_type=None):
|
|
34
34
|
|
|
@@ -310,4 +310,24 @@ def render_taxon_relationships(context, meta_app, lazy_taxon):
|
|
|
310
310
|
'taxon_relationship_types': relationship_types,
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
return tag_context
|
|
313
|
+
return tag_context
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@register.simple_tag()
|
|
317
|
+
def get_object_from_licence(licence_registry_entry):
|
|
318
|
+
|
|
319
|
+
content = licence_registry_entry.content
|
|
320
|
+
content_type = licence_registry_entry.content_type
|
|
321
|
+
|
|
322
|
+
image_store_content_type = ContentType.objects.get_for_model(ImageStore)
|
|
323
|
+
server_image_store_content_type = ContentType.objects.get_for_model(ServerImageStore)
|
|
324
|
+
|
|
325
|
+
licenced_object = {
|
|
326
|
+
'is_imagestore': False,
|
|
327
|
+
'object': content,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if content_type == image_store_content_type or content_type == server_image_store_content_type:
|
|
331
|
+
licenced_object['is_imagestore'] = True
|
|
332
|
+
|
|
333
|
+
return licenced_object
|
app_kit/tests/TESTS_ROOT/media_for_tests/test/imagestore/31/a6a11b61d65ee19c4c22caa0682288ff.jpg
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -320,7 +320,7 @@ class TestGenericContentZipImporter(WithUser, WithMetaApp, TenantTestCase):
|
|
|
320
320
|
self.assertEqual(licence.licence, 'CC BY-NC')
|
|
321
321
|
self.assertEqual(licence.licence_version, '4.0')
|
|
322
322
|
self.assertEqual(licence.source_link, 'https://imageworld.com/lacerta-agilis.jpg')
|
|
323
|
-
self.assertEqual(licence.creator_name, '
|
|
323
|
+
self.assertEqual(licence.creator_name, 'New Author')
|
|
324
324
|
self.assertEqual(content_image.alt_text, 'A new alt text')
|
|
325
325
|
self.assertEqual(content_image.text, 'A new caption')
|
|
326
326
|
self.assertEqual(content_image.title, 'Lizard')
|
|
@@ -421,6 +421,146 @@ class TestGenericContentZipImporter(WithUser, WithMetaApp, TenantTestCase):
|
|
|
421
421
|
# No match for wrong author
|
|
422
422
|
matches = importer.get_taxa_with_taxon_author_tolerance(taxon_source, taxon_latname, 'Smith')
|
|
423
423
|
self.assertEqual(len(matches), 0)
|
|
424
|
+
|
|
425
|
+
# External Media URL validation
|
|
426
|
+
@test_settings
|
|
427
|
+
def test_validate_external_media_type_youtube_valid_urls(self):
|
|
428
|
+
importer = self.get_zip_importer()
|
|
429
|
+
importer.load_workbook()
|
|
430
|
+
importer.errors = []
|
|
431
|
+
|
|
432
|
+
valid_urls = [
|
|
433
|
+
'https://youtu.be/dQw4w9WgXcQ',
|
|
434
|
+
'http://youtu.be/dQw4w9WgXcQ',
|
|
435
|
+
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
436
|
+
'https://youtube.com/watch?v=dQw4w9WgXcQ',
|
|
437
|
+
'https://www.youtube.com/embed/dQw4w9WgXcQ',
|
|
438
|
+
'https://www.youtube.com/v/dQw4w9WgXcQ',
|
|
439
|
+
'https://www.youtube.com/shorts/dQw4w9WgXcQ',
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
for url in valid_urls:
|
|
443
|
+
importer.validate_external_media_type_youtube({'url': url}, importer.external_media_sheet_name, 2)
|
|
444
|
+
self.assertEqual(importer.errors, [])
|
|
445
|
+
|
|
446
|
+
@test_settings
|
|
447
|
+
def test_validate_external_media_type_youtube_invalid_urls(self):
|
|
448
|
+
importer = self.get_zip_importer()
|
|
449
|
+
importer.load_workbook()
|
|
450
|
+
importer.errors = []
|
|
451
|
+
|
|
452
|
+
# Empty URL
|
|
453
|
+
importer.validate_external_media_type_youtube({'url': ''}, importer.external_media_sheet_name, 2)
|
|
454
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid YouTube URL: empty value', importer.errors)
|
|
455
|
+
|
|
456
|
+
# Invalid scheme
|
|
457
|
+
bad_url = 'ftp://youtu.be/dQw4w9WgXcQ'
|
|
458
|
+
importer.validate_external_media_type_youtube({'url': bad_url}, importer.external_media_sheet_name, 2)
|
|
459
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid YouTube URL (scheme must be http/https): ftp://youtu.be/dQw4w9WgXcQ', importer.errors)
|
|
460
|
+
|
|
461
|
+
# No video id
|
|
462
|
+
bad_url = 'https://www.youtube.com/watch?v='
|
|
463
|
+
importer.validate_external_media_type_youtube({'url': bad_url}, importer.external_media_sheet_name, 2)
|
|
464
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid YouTube URL or video id not found: https://www.youtube.com/watch?v=', importer.errors)
|
|
465
|
+
|
|
466
|
+
@test_settings
|
|
467
|
+
def test_validate_external_media_type_vimeo_valid_urls(self):
|
|
468
|
+
importer = self.get_zip_importer()
|
|
469
|
+
importer.load_workbook()
|
|
470
|
+
importer.errors = []
|
|
471
|
+
|
|
472
|
+
valid_urls = [
|
|
473
|
+
'https://vimeo.com/123456789',
|
|
474
|
+
'http://vimeo.com/123456789',
|
|
475
|
+
'https://player.vimeo.com/video/123456789',
|
|
476
|
+
'https://www.vimeo.com/123456789',
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
for url in valid_urls:
|
|
480
|
+
importer.validate_external_media_type_vimeo({'url': url}, importer.external_media_sheet_name, 2)
|
|
481
|
+
self.assertEqual(importer.errors, [])
|
|
482
|
+
|
|
483
|
+
@test_settings
|
|
484
|
+
def test_validate_external_media_type_vimeo_invalid_urls(self):
|
|
485
|
+
importer = self.get_zip_importer()
|
|
486
|
+
importer.load_workbook()
|
|
487
|
+
importer.errors = []
|
|
488
|
+
|
|
489
|
+
# Empty URL
|
|
490
|
+
importer.validate_external_media_type_vimeo({'url': ''}, importer.external_media_sheet_name, 2)
|
|
491
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid Vimeo URL: empty value', importer.errors)
|
|
492
|
+
|
|
493
|
+
# Invalid scheme
|
|
494
|
+
bad_url = 'ftp://vimeo.com/123456789'
|
|
495
|
+
importer.validate_external_media_type_vimeo({'url': bad_url}, importer.external_media_sheet_name, 2)
|
|
496
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid Vimeo URL (scheme must be http/https): ftp://vimeo.com/123456789', importer.errors)
|
|
497
|
+
|
|
498
|
+
# No video id
|
|
499
|
+
bad_url = 'https://vimeo.com/'
|
|
500
|
+
importer.validate_external_media_type_vimeo({'url': bad_url}, importer.external_media_sheet_name, 2)
|
|
501
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid Vimeo URL or video id not found: https://vimeo.com/', importer.errors)
|
|
502
|
+
|
|
503
|
+
@test_settings
|
|
504
|
+
def test_validate_external_media_type_image_url_extension(self):
|
|
505
|
+
importer = self.get_zip_importer()
|
|
506
|
+
importer.load_workbook()
|
|
507
|
+
importer.errors = []
|
|
508
|
+
|
|
509
|
+
# Valid image URL
|
|
510
|
+
importer.validate_external_media_type_image({'url': 'https://example.com/pic.jpg'}, importer.external_media_sheet_name, 2)
|
|
511
|
+
# Invalid image URL extension
|
|
512
|
+
importer.validate_external_media_type_image({'url': 'https://example.com/pic.bmp'}, importer.external_media_sheet_name, 2)
|
|
513
|
+
|
|
514
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid image format in URL: .bmp. Valid formats are: .jpg, .jpeg, .png, .webp, .gif', importer.errors)
|
|
515
|
+
|
|
516
|
+
@test_settings
|
|
517
|
+
def test_validate_external_media_type_audio_pdf(self):
|
|
518
|
+
importer = self.get_zip_importer()
|
|
519
|
+
importer.load_workbook()
|
|
520
|
+
importer.errors = []
|
|
521
|
+
|
|
522
|
+
# mp3
|
|
523
|
+
importer.validate_external_media_type_mp3({'url': 'https://example.com/audio.txt'}, importer.external_media_sheet_name, 2)
|
|
524
|
+
importer.validate_external_media_type_mp3({'url': 'https://example.com/audio.mp3'}, importer.external_media_sheet_name, 2)
|
|
525
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid mp3 format in URL: https://example.com/audio.txt. URL has to end with .mp3', importer.errors)
|
|
526
|
+
|
|
527
|
+
# wav
|
|
528
|
+
importer.validate_external_media_type_wav({'url': 'https://example.com/audio.txt'}, importer.external_media_sheet_name, 2)
|
|
529
|
+
importer.validate_external_media_type_wav({'url': 'https://example.com/audio.wav'}, importer.external_media_sheet_name, 2)
|
|
530
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid wav format in URL: https://example.com/audio.txt. URL has to end with .wav', importer.errors)
|
|
531
|
+
|
|
532
|
+
# pdf
|
|
533
|
+
importer.validate_external_media_type_pdf({'url': 'https://example.com/doc.txt'}, importer.external_media_sheet_name, 2)
|
|
534
|
+
importer.validate_external_media_type_pdf({'url': 'https://example.com/doc.pdf'}, importer.external_media_sheet_name, 2)
|
|
535
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid pdf format in URL: https://example.com/doc.txt. URL has to end with .pdf', importer.errors)
|
|
536
|
+
|
|
537
|
+
@test_settings
|
|
538
|
+
def test_validate_external_media_type_website(self):
|
|
539
|
+
importer = self.get_zip_importer()
|
|
540
|
+
importer.load_workbook()
|
|
541
|
+
importer.errors = []
|
|
542
|
+
|
|
543
|
+
# Invalid: ends with file extension
|
|
544
|
+
importer.validate_external_media_type_website({'url': 'https://example.com/image.jpg'}, importer.external_media_sheet_name, 2)
|
|
545
|
+
# Valid: no file extension
|
|
546
|
+
importer.validate_external_media_type_website({'url': 'https://example.com/some/path?query=x'}, importer.external_media_sheet_name, 2)
|
|
547
|
+
# Valid: domain-only with TLD
|
|
548
|
+
importer.validate_external_media_type_website({'url': 'https://example.com'}, importer.external_media_sheet_name, 2)
|
|
549
|
+
|
|
550
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid website format in URL: https://example.com/image.jpg. URL should not end with a file extension.', importer.errors)
|
|
551
|
+
|
|
552
|
+
@test_settings
|
|
553
|
+
def test_validate_external_media_type_file(self):
|
|
554
|
+
importer = self.get_zip_importer()
|
|
555
|
+
importer.load_workbook()
|
|
556
|
+
importer.errors = []
|
|
557
|
+
|
|
558
|
+
# Invalid: no file extension
|
|
559
|
+
importer.validate_external_media_type_file({'url': 'https://example.com/download/'}, importer.external_media_sheet_name, 2)
|
|
560
|
+
# Valid: has file extension
|
|
561
|
+
importer.validate_external_media_type_file({'url': 'https://example.com/download.zip'}, importer.external_media_sheet_name, 2)
|
|
562
|
+
|
|
563
|
+
self.assertIn('[Generic Content.xlsx][Sheet:External Media][cell:A3] Invalid file format in URL: https://example.com/download/. URL has to end with a file extension.', importer.errors)
|
|
424
564
|
|
|
425
565
|
# real world test
|
|
426
566
|
# algaebase entry is : Desmarestia viridis (O.F.Müller) J.V.Lamouroux 1813
|
|
@@ -587,4 +727,7 @@ class TestGenericContentZipImporterInvalidData(WithUser, WithMetaApp, TenantTest
|
|
|
587
727
|
ignorin_importer.load_workbook()
|
|
588
728
|
ignorin_importer.errors = []
|
|
589
729
|
ignorin_importer.validate_listing_in_images_sheet('unlisted.jpg', 'A', 2)
|
|
590
|
-
self.assertEqual(ignorin_importer.errors, [])
|
|
730
|
+
self.assertEqual(ignorin_importer.errors, [])
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
|
app_kit/views.py
CHANGED
|
@@ -22,7 +22,7 @@ from .forms import (AddLanguageForm, MetaAppOptionsForm, TagAnyElementForm, Gene
|
|
|
22
22
|
CreateGenericContentForm, AddExistingGenericContentForm, TranslateAppForm,
|
|
23
23
|
EditGenericContentNameForm, ManageContentImageWithTextForm,
|
|
24
24
|
ZipImportForm, BuildAppForm, CreateAppForm, ManageLocalizedContentImageForm,
|
|
25
|
-
TranslateVernacularNamesForm)
|
|
25
|
+
TranslateVernacularNamesForm, ManageContentLicenceForm)
|
|
26
26
|
|
|
27
27
|
from django_tenants.utils import get_tenant_domain_model
|
|
28
28
|
Domain = get_tenant_domain_model()
|
|
@@ -53,6 +53,8 @@ from django.db import connection
|
|
|
53
53
|
# activate permission rules
|
|
54
54
|
from .permission_rules import *
|
|
55
55
|
|
|
56
|
+
from content_licencing.models import ContentLicenceRegistry
|
|
57
|
+
|
|
56
58
|
LOCALCOSMOS_COMMERCIAL_BUILDER = getattr(settings, 'LOCALCOSMOS_COMMERCIAL_BUILDER', True)
|
|
57
59
|
|
|
58
60
|
|
|
@@ -1732,6 +1734,62 @@ class DeleteAppKitExternalMedia(MetaAppMixin, AjaxDeleteView):
|
|
|
1732
1734
|
context = super().get_context_data(**kwargs)
|
|
1733
1735
|
context['external_media_object'] = self.object.content_object
|
|
1734
1736
|
return context
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
class ListImagesAndLicences(MetaAppMixin, TemplateView):
|
|
1740
|
+
|
|
1741
|
+
template_name = 'app_kit/list_images_and_licences.html'
|
|
1742
|
+
ajax_template_name = 'app_kit/ajax/list_images_and_licences_content.html'
|
|
1743
|
+
|
|
1744
|
+
def get_context_data(self, **kwargs):
|
|
1745
|
+
context = super().get_context_data(**kwargs)
|
|
1746
|
+
registry_entries = ContentLicenceRegistry.objects.all().order_by('creator_name', 'pk')
|
|
1747
|
+
context['registry_entries'] = registry_entries
|
|
1748
|
+
return context
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
class ManageContentLicence(MetaAppMixin, LicencingFormViewMixin, FormView):
|
|
1752
|
+
|
|
1753
|
+
form_class = ManageContentLicenceForm
|
|
1754
|
+
|
|
1755
|
+
template_name = 'app_kit/ajax/manage_content_licence.html'
|
|
1756
|
+
|
|
1757
|
+
@method_decorator(ajax_required)
|
|
1758
|
+
def dispatch(self, request, *args, **kwargs):
|
|
1759
|
+
self.set_instance(**kwargs)
|
|
1760
|
+
return super().dispatch(request, *args, **kwargs)
|
|
1761
|
+
|
|
1762
|
+
def set_instance(self, **kwargs):
|
|
1763
|
+
self.licence_registry_entry = ContentLicenceRegistry.objects.get(pk=kwargs['registry_entry_id'])
|
|
1764
|
+
|
|
1765
|
+
def get_form(self, form_class=None):
|
|
1766
|
+
if form_class is None:
|
|
1767
|
+
form_class = self.get_form_class()
|
|
1768
|
+
return form_class(self.licence_registry_entry.model_field, **self.get_form_kwargs())
|
|
1769
|
+
|
|
1770
|
+
def get_initial(self):
|
|
1771
|
+
initial = super().get_initial()
|
|
1772
|
+
|
|
1773
|
+
licencing_initial = self.get_licencing_initial()
|
|
1774
|
+
initial.update(licencing_initial)
|
|
1775
|
+
|
|
1776
|
+
return initial
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def get_context_data(self, **kwargs):
|
|
1780
|
+
context = super().get_context_data(**kwargs)
|
|
1781
|
+
context['registry_entry'] = self.licence_registry_entry
|
|
1782
|
+
return context
|
|
1783
|
+
|
|
1784
|
+
def form_valid(self, form):
|
|
1785
|
+
if self.licence_registry_entry.content:
|
|
1786
|
+
self.register_content_licence(form, self.licence_registry_entry.content, self.licence_registry_entry.model_field)
|
|
1787
|
+
|
|
1788
|
+
context = self.get_context_data(**self.kwargs)
|
|
1789
|
+
context['form'] = form
|
|
1790
|
+
context['success'] = True
|
|
1791
|
+
return self.render_to_response(context)
|
|
1792
|
+
|
|
1735
1793
|
|
|
1736
1794
|
# LEGAL
|
|
1737
1795
|
class IdentityMixin:
|
|
@@ -1759,4 +1817,5 @@ class PrivacyStatement(IdentityMixin, TemplateView):
|
|
|
1759
1817
|
@method_decorator(csrf_exempt)
|
|
1760
1818
|
@method_decorator(requires_csrf_token)
|
|
1761
1819
|
def dispatch(self, request, *args, **kwargs):
|
|
1762
|
-
return super().dispatch(request, *args, **kwargs)
|
|
1820
|
+
return super().dispatch(request, *args, **kwargs)
|
|
1821
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localcosmos_app_kit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.17
|
|
4
4
|
Summary: LocalCosmos App Kit. Web Portal to build Android and iOS apps
|
|
5
5
|
Home-page: https://github.com/localcosmos/app-kit
|
|
6
6
|
Author: Thomas Uher
|
|
@@ -14,7 +14,7 @@ Classifier: Operating System :: OS Independent
|
|
|
14
14
|
Requires-Python: >=3.8
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENCE
|
|
17
|
-
Requires-Dist: localcosmos-server==0.24.
|
|
17
|
+
Requires-Dist: localcosmos-server==0.24.11
|
|
18
18
|
Requires-Dist: localcosmos-cordova-builder==0.9.6
|
|
19
19
|
Requires-Dist: django-tenants==3.7.0
|
|
20
20
|
Requires-Dist: django-cleanup==9.0.0
|