localcosmos-app-kit 0.9.17__py3-none-any.whl → 0.10.0__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/appbuilder/AppReleaseBuilder.py +111 -53
- app_kit/appbuilder/JSONBuilders/TaxonProfilesJSONBuilder.py +38 -15
- app_kit/appbuilder/JSONBuilders/__pycache__/TaxonProfilesJSONBuilder.cpython-313.pyc +0 -0
- app_kit/appbuilder/TaxonBuilder.py +35 -28
- app_kit/appbuilder/__pycache__/AppReleaseBuilder.cpython-313.pyc +0 -0
- app_kit/appbuilder/__pycache__/TaxonBuilder.cpython-313.pyc +0 -0
- app_kit/features/backbonetaxonomy/templates/backbonetaxonomy/manage_taxon.html +1 -1
- app_kit/features/taxon_profiles/forms.py +12 -37
- app_kit/features/taxon_profiles/models.py +29 -36
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/create_taxon_profile.html +1 -1
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/manage_taxon_profile_form.html +35 -54
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/manage_taxon_profile_morphotype.html +2 -1
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/nature_guide_taxonlist.html +2 -2
- app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/non_nature_guide_taxonlist.html +1 -1
- app_kit/features/taxon_profiles/templates/taxon_profiles/manage_taxon_profile.html +4 -4
- app_kit/features/taxon_profiles/urls.py +3 -0
- app_kit/features/taxon_profiles/views.py +44 -32
- app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_species_csv.cpython-313.pyc +0 -0
- app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_taxonomy_csv.cpython-313.pyc +0 -0
- app_kit/taxonomy/sources/custom/management/commands/import_custom_species_csv.py +177 -0
- app_kit/taxonomy/sources/custom/management/commands/import_custom_taxonomy_csv.py +177 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.10.0.dist-info}/METADATA +1 -1
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.10.0.dist-info}/RECORD +26 -23
- app_kit/tests/TESTS_ROOT/media_for_tests/test/imagestore/31/a6a11b61d65ee19c4c22caa0682288ff.jpg +0 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.10.0.dist-info}/WHEEL +0 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.10.0.dist-info}/licenses/LICENCE +0 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.10.0.dist-info}/top_level.txt +0 -0
|
@@ -87,51 +87,44 @@ class TaxonProfiles(GenericContent):
|
|
|
87
87
|
def get_primary_localization(self, meta_app=None):
|
|
88
88
|
locale = super().get_primary_localization(meta_app)
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'taxon_source' : lazy_taxon.taxon_source,
|
|
96
|
-
'taxon_latname' : lazy_taxon.taxon_latname,
|
|
97
|
-
'taxon_author' : lazy_taxon.taxon_author,
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
taxon_profile = TaxonProfile.objects.filter(taxon_profiles=self, **taxon_query).first()
|
|
101
|
-
|
|
102
|
-
if taxon_profile:
|
|
90
|
+
all_taxon_profiles = TaxonProfile.objects.filter(taxon_profiles=self)
|
|
91
|
+
for taxon_profile in all_taxon_profiles:
|
|
92
|
+
|
|
93
|
+
if taxon_profile.publication_status == 'draft':
|
|
94
|
+
continue
|
|
103
95
|
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
if taxon_profile.morphotype:
|
|
97
|
+
locale[taxon_profile.morphotype] = taxon_profile.morphotype
|
|
106
98
|
|
|
107
|
-
|
|
99
|
+
for text in taxon_profile.texts():
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
# text_type_key = 'taxon_text_{0}'.format(text.taxon_text_type.id)
|
|
102
|
+
# short: use name as key (-> no duplicates in translation matrix)
|
|
103
|
+
text_type_key = text.taxon_text_type.text_type
|
|
104
|
+
locale[text_type_key] = text.taxon_text_type.text_type
|
|
105
|
+
|
|
106
|
+
# text.text is a bad key, because if text.text changes, the translation is gone
|
|
107
|
+
# text.text are long texts, so use a different key which survives text changes
|
|
108
|
+
# locale[text.text] = text.text
|
|
117
109
|
|
|
118
|
-
|
|
110
|
+
short_text_key = self.get_short_text_key(text)
|
|
119
111
|
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
if text.text:
|
|
113
|
+
locale[short_text_key] = text.text
|
|
122
114
|
|
|
123
|
-
|
|
115
|
+
long_text_key = self.get_long_text_key(text)
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
if text.long_text:
|
|
118
|
+
locale[long_text_key] = text.long_text
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
content_images_primary_localization = taxon_profile.get_content_images_primary_localization()
|
|
121
|
+
locale.update(content_images_primary_localization)
|
|
122
|
+
|
|
123
|
+
short_profile = taxon_profile.short_profile
|
|
124
|
+
if short_profile:
|
|
125
|
+
locale[short_profile] = short_profile
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
if short_profile:
|
|
133
|
-
locale[short_profile] = short_profile
|
|
134
|
-
|
|
127
|
+
|
|
135
128
|
navigation = TaxonProfilesNavigation.objects.filter(taxon_profiles=self).first()
|
|
136
129
|
|
|
137
130
|
if navigation:
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<script>
|
|
33
33
|
var modal = $("#Modal");
|
|
34
34
|
modal.modal("hide");
|
|
35
|
-
window.location = "{% url 'manage_taxon_profile' meta_app.id
|
|
35
|
+
window.location = "{% url 'manage_taxon_profile' meta_app.id taxon_profile.id %}";
|
|
36
36
|
</script>
|
|
37
37
|
{% endif %}
|
|
38
38
|
{% endblock %}
|
app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/manage_taxon_profile_form.html
CHANGED
|
@@ -1,70 +1,51 @@
|
|
|
1
1
|
{% load i18n static localcosmos_tags %}
|
|
2
2
|
{% if text_types %}
|
|
3
|
-
<form id="taxon-text-types-form" method="POST" action="{% url 'manage_taxon_profile' meta_app.id
|
|
3
|
+
<form id="taxon-text-types-form" method="POST" action="{% url 'manage_taxon_profile' meta_app.id taxon_profile.id %}"
|
|
4
|
+
>{% csrf_token %}
|
|
4
5
|
<div id="text-types-form-fields">
|
|
5
6
|
{% for field in form %}
|
|
6
|
-
{% if field.field.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<div id="order-ctype-{{ text_type_content_type.id }}{% if field.field.category %}-{{ field.field.category.id }}{% endif %}-container" {% if field.field.category %}data-object-id="{{ field.field.category.id }}"{% endif %} class="text-type-category-container">
|
|
7
|
+
{% if field.field.category %}
|
|
8
|
+
<h2>
|
|
9
|
+
{{ field.field.category }}
|
|
10
|
+
</h2>
|
|
11
11
|
{% endif %}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{% if field.is_hidden %}
|
|
15
|
-
{% if field.field.category %}
|
|
16
|
-
<h2>
|
|
17
|
-
{{ field.field.category }}
|
|
18
|
-
</h2>
|
|
12
|
+
<div class="row">
|
|
13
|
+
<div class="col-12">
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{%
|
|
15
|
+
{% if field.is_hidden %}
|
|
16
|
+
{{ field }}
|
|
17
|
+
{% else %}
|
|
18
|
+
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
|
19
|
+
|
|
20
|
+
<h4>
|
|
21
|
+
{{ field.label }}
|
|
22
|
+
{% if field.field.taxon_text_type %}
|
|
23
|
+
{% if show_text_length_badges %}{% if field.field.is_short_version %}<span class="badge badge-info">{% trans 'short version' %}</span>{% else %}<span class="badge badge-primary">{% trans 'long version' %}</span>{% endif %} {% endif %}
|
|
28
24
|
{% endif %}
|
|
29
|
-
|
|
30
|
-
{% else %}
|
|
31
|
-
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
|
25
|
+
</h4>
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{% if show_text_length_badges %}{% if field.field.is_short_version %}<span class="badge badge-info">{% trans 'short version' %}</span>{% else %}<span class="badge badge-primary">{% trans 'long version' %}</span>{% endif %} {% endif %}
|
|
37
|
-
{% endif %}
|
|
38
|
-
</h4>
|
|
27
|
+
{% if field.field.language %}
|
|
28
|
+
<img src="{% static 'localcosmos_server/images/countries/' %}{{ field.field.language }}.gif" /> {{ field.field.language }}
|
|
29
|
+
{% endif %}
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
{{ field }}
|
|
32
|
+
{% if field.help_text %}
|
|
33
|
+
<small class="form-text text-muted">{{ field.help_text }}</small>
|
|
34
|
+
{% endif %}
|
|
35
|
+
</div>
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{%
|
|
37
|
+
{% if field.field.taxon_text and field.field.is_short_version %}
|
|
38
|
+
{% with content_instance=field.field.taxon_text %}
|
|
39
|
+
<div id="content-images-list-{{ content_instance|ctype_id }}-{{ content_instance.id }}" class="taxon-text-images-container">
|
|
40
|
+
{% include 'app_kit/ajax/content_images_list.html' %}
|
|
48
41
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{% include 'app_kit/ajax/content_images_list.html' %}
|
|
54
|
-
</div>
|
|
55
|
-
{% endwith %}
|
|
56
|
-
{% endif %}
|
|
57
|
-
<br><br>
|
|
58
|
-
{% endif %}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
{% if field.field.is_last %}
|
|
42
|
+
{% endwith %}
|
|
43
|
+
{% endif %}
|
|
44
|
+
<br><br>
|
|
45
|
+
{% endif %}
|
|
62
46
|
</div>
|
|
63
|
-
|
|
47
|
+
</div>
|
|
64
48
|
{% endfor %}
|
|
65
|
-
{% if form.has_categories %}
|
|
66
|
-
<!--</div>-->
|
|
67
|
-
{% endif %}
|
|
68
49
|
</div>
|
|
69
50
|
|
|
70
51
|
<div class="row">
|
app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/manage_taxon_profile_morphotype.html
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
{% if success %}
|
|
24
24
|
<script>
|
|
25
25
|
{% if created %}
|
|
26
|
-
|
|
26
|
+
// taxon_profile is the created morphotype profile
|
|
27
|
+
window.location.href = "{% url 'manage_taxon_profile' meta_app.id taxon_profile.id %}";
|
|
27
28
|
{% else %}
|
|
28
29
|
// Reload the page to show the updated morphotype
|
|
29
30
|
$('#Modal').modal('hide');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{% if meta_node.taxon %}
|
|
8
8
|
{% get_taxon_profile meta_app meta_node.taxon as profile %}
|
|
9
9
|
{% if profile %}
|
|
10
|
-
<a href="{% url 'manage_taxon_profile' meta_app.id
|
|
10
|
+
<a href="{% url 'manage_taxon_profile' meta_app.id profile.id %}">
|
|
11
11
|
{{ meta_node.name }} <i>{{ meta_node.taxon_latname }} {{ meta_node.taxon_author }}</i>
|
|
12
12
|
</a>
|
|
13
13
|
{% if profile.publication_status == 'draft' %}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
{% get_nature_guide_taxon meta_node nature_guide as taxon %}
|
|
24
24
|
{% get_taxon_profile meta_app taxon as non_taxon_profile %}
|
|
25
25
|
{% if non_taxon_profile %}
|
|
26
|
-
<a href="{% url 'manage_taxon_profile' meta_app.id
|
|
26
|
+
<a href="{% url 'manage_taxon_profile' meta_app.id non_taxon_profile.id %}">
|
|
27
27
|
{{ meta_node.name }}
|
|
28
28
|
</a>
|
|
29
29
|
{% if non_taxon_profile.publication_status == 'draft' %}
|
app_kit/features/taxon_profiles/templates/taxon_profiles/ajax/non_nature_guide_taxonlist.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<ul>
|
|
4
4
|
{% for profile in non_nature_guide_taxon_profiles %}
|
|
5
5
|
<li>
|
|
6
|
-
<a href="{% url 'manage_taxon_profile' meta_app.id
|
|
6
|
+
<a href="{% url 'manage_taxon_profile' meta_app.id profile.id %}">
|
|
7
7
|
<i>{{ profile.taxon_latname }} {{ profile.taxon_author }}</i>
|
|
8
8
|
</a>
|
|
9
9
|
{% if profile.publication_status == 'draft' %}
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
</div>
|
|
93
93
|
{% for duplicate in possible_duplicates %}
|
|
94
94
|
<i>
|
|
95
|
-
<a href="{% url 'manage_taxon_profile' meta_app.id
|
|
95
|
+
<a href="{% url 'manage_taxon_profile' meta_app.id duplicate.id %}">{{ duplicate.taxon_latname }} {{ duplicate.taxon_author }}</a>
|
|
96
96
|
</i>
|
|
97
97
|
{% endfor %}
|
|
98
98
|
</div>
|
|
@@ -111,10 +111,10 @@
|
|
|
111
111
|
</div>
|
|
112
112
|
</div>
|
|
113
113
|
{% endif %}
|
|
114
|
-
{% if taxon_profile.morphotype %}
|
|
114
|
+
{% if taxon_profile.morphotype and taxon_profile.parent_profile %}
|
|
115
115
|
<div class="row">
|
|
116
116
|
<div class="col-12">
|
|
117
|
-
{% trans 'This is a morphotype of:' %} <a href="{% url 'manage_taxon_profile' meta_app.id
|
|
117
|
+
{% trans 'This is a morphotype of:' %} <a href="{% url 'manage_taxon_profile' meta_app.id taxon_profile.parent_profile.id %}">
|
|
118
118
|
{{ taxon }}
|
|
119
119
|
</a>
|
|
120
120
|
</div>
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
{% if taxon_profile.morphotype_profiles %}
|
|
132
132
|
{% for morphotype in taxon_profile.morphotype_profiles %}
|
|
133
133
|
<div>
|
|
134
|
-
<a href="{% url 'manage_taxon_profile' meta_app.id
|
|
134
|
+
<a href="{% url 'manage_taxon_profile' meta_app.id morphotype.id %}">
|
|
135
135
|
{{ morphotype.morphotype }}
|
|
136
136
|
</a>
|
|
137
137
|
</div>
|
|
@@ -10,6 +10,9 @@ urlpatterns = [
|
|
|
10
10
|
views.CreateTaxonProfile.as_view(), name='create_taxon_profile'),
|
|
11
11
|
path('manage-taxon-profile/<int:meta_app_id>/<int:taxon_profiles_id>/<str:taxon_source>/<uuid:name_uuid>/',
|
|
12
12
|
views.ManageTaxonProfile.as_view(), name='manage_taxon_profile'),
|
|
13
|
+
# this should be the future wherever possible, as it is more robust (does not rely on taxon_source and name_uuid to be unchanged)
|
|
14
|
+
path('manage-taxon-profile/<int:meta_app_id>/<int:taxon_profile_id>/',
|
|
15
|
+
views.ManageTaxonProfile.as_view(), name='manage_taxon_profile'),
|
|
13
16
|
path('delete-taxon-profile/<int:meta_app_id>/<int:pk>/',
|
|
14
17
|
views.DeleteTaxonProfile.as_view(), name='delete_taxon_profile'),
|
|
15
18
|
# morphotypes
|
|
@@ -298,16 +298,22 @@ class ManageTaxonProfile(CreateTaxonProfileMixin, MetaAppFormLanguageMixin, Form
|
|
|
298
298
|
|
|
299
299
|
def set_taxon(self, request, **kwargs):
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
if 'taxon_profile_id' in kwargs:
|
|
302
|
+
self.taxon_profile = TaxonProfile.objects.get(pk=kwargs['taxon_profile_id'])
|
|
303
|
+
self.taxon_profiles = self.taxon_profile.taxon_profiles
|
|
304
|
+
|
|
305
|
+
else:
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
self.taxon_profiles = TaxonProfiles.objects.get(pk=kwargs['taxon_profiles_id'])
|
|
307
308
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
taxon_source = kwargs['taxon_source']
|
|
310
|
+
name_uuid = kwargs['name_uuid']
|
|
311
|
+
|
|
312
|
+
morphotype = kwargs.get('morphotype', None)
|
|
313
|
+
|
|
314
|
+
self.taxon_profile = TaxonProfile.objects.get(taxon_profiles=self.taxon_profiles,
|
|
315
|
+
taxon_source=taxon_source, name_uuid=name_uuid,
|
|
316
|
+
morphotype=morphotype)
|
|
311
317
|
|
|
312
318
|
self.taxon = LazyTaxon(instance=self.taxon_profile)
|
|
313
319
|
|
|
@@ -744,44 +750,50 @@ class CollectTaxonImages(MetaAppFormLanguageMixin, TemplateView):
|
|
|
744
750
|
return images
|
|
745
751
|
|
|
746
752
|
def get_taxon_images(self, exclude=[]):
|
|
747
|
-
|
|
748
|
-
|
|
753
|
+
|
|
754
|
+
images = []
|
|
755
|
+
|
|
756
|
+
if not self.taxon_profile.morphotype:
|
|
757
|
+
images = ContentImage.objects.filter(image_store__taxon_source=self.taxon.taxon_source,
|
|
758
|
+
image_store__taxon_latname=self.taxon.taxon_latname).exclude(pk__in=exclude)
|
|
749
759
|
|
|
750
760
|
return images
|
|
751
761
|
|
|
752
762
|
# images can be on MetNode or NatureGuidesTaxonTree
|
|
753
763
|
def get_nature_guide_images(self, exclude=[]):
|
|
754
764
|
|
|
755
|
-
meta_nodes = MetaNode.objects.filter(taxon_source=self.taxon.taxon_source,
|
|
756
|
-
taxon_latname=self.taxon.taxon_latname, taxon_author=self.taxon.taxon_author, morphotype=self.morphotype)
|
|
757
|
-
|
|
758
765
|
nature_guide_images = []
|
|
766
|
+
|
|
767
|
+
if not self.taxon_profile.morphotype:
|
|
768
|
+
|
|
769
|
+
meta_nodes = MetaNode.objects.filter(taxon_source=self.taxon.taxon_source,
|
|
770
|
+
taxon_latname=self.taxon.taxon_latname, taxon_author=self.taxon.taxon_author, morphotype=self.morphotype)
|
|
759
771
|
|
|
760
|
-
|
|
772
|
+
if meta_nodes:
|
|
761
773
|
|
|
762
|
-
|
|
774
|
+
meta_node_ids = meta_nodes.values_list('id', flat=True)
|
|
763
775
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
776
|
+
meta_node_content_type = ContentType.objects.get_for_model(MetaNode)
|
|
777
|
+
meta_node_images = ContentImage.objects.filter(content_type=meta_node_content_type,
|
|
778
|
+
object_id__in=meta_node_ids).exclude(pk__in=exclude)
|
|
779
|
+
|
|
780
|
+
exclude += list(meta_node_images.values_list('id', flat=True))
|
|
781
|
+
nature_guide_images += list(meta_node_images)
|
|
770
782
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
783
|
+
|
|
784
|
+
nodes = NatureGuidesTaxonTree.objects.filter(meta_node__taxon_source=self.taxon.taxon_source,
|
|
785
|
+
meta_node__taxon_latname=self.taxon.taxon_latname,
|
|
786
|
+
meta_node__taxon_author=self.taxon.taxon_author)
|
|
775
787
|
|
|
776
|
-
|
|
788
|
+
if nodes:
|
|
777
789
|
|
|
778
|
-
|
|
790
|
+
node_ids = nodes.values_list('id', flat=True)
|
|
779
791
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
792
|
+
node_content_type = ContentType.objects.get_for_model(NatureGuidesTaxonTree)
|
|
793
|
+
node_images = ContentImage.objects.filter(content_type=node_content_type,
|
|
794
|
+
object_id__in=node_ids).exclude(pk__in=exclude)
|
|
795
|
+
|
|
796
|
+
nature_guide_images += list(node_images)
|
|
785
797
|
|
|
786
798
|
|
|
787
799
|
return nature_guide_images
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
6
|
+
from django.db import transaction
|
|
7
|
+
|
|
8
|
+
from taxonomy.models import TaxonomyModelRouter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = (
|
|
13
|
+
'Importiert die Arten-/Taxa-CSV (|-getrennt). Erkennt Headerzeile automatisch und überspringt sie.\n'
|
|
14
|
+
'Unterstütztes Layout (aktuell):\n'
|
|
15
|
+
' 0: scientific_name | 1: rank | 2: parent_lat | 3: parent_code | 4: de | 5: en | 6: nl | 7: da'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def add_arguments(self, parser):
|
|
19
|
+
parser.add_argument('csv_path', type=str, help='Pfad zur Arten-CSV (|-getrennt).')
|
|
20
|
+
parser.add_argument('--delimiter', type=str, default='|', help='Trennzeichen (Standard: |).')
|
|
21
|
+
parser.add_argument('--encoding', type=str, default='utf-8', help='Datei-Kodierung (Standard: utf-8).')
|
|
22
|
+
parser.add_argument('--dry-run', action='store_true', help='Nur prüfen, am Ende zurückrollen.')
|
|
23
|
+
|
|
24
|
+
def handle(self, *args, **options):
|
|
25
|
+
csv_path = options['csv_path']
|
|
26
|
+
delimiter = options['delimiter'] or '|'
|
|
27
|
+
encoding = options['encoding']
|
|
28
|
+
dry_run = options['dry_run']
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(csv_path):
|
|
31
|
+
raise CommandError(f'Datei nicht gefunden: {csv_path}')
|
|
32
|
+
|
|
33
|
+
models = TaxonomyModelRouter('taxonomy.sources.custom')
|
|
34
|
+
self.stdout.write(self.style.NOTICE('Quelle: taxonomy.sources.custom'))
|
|
35
|
+
self.stdout.write(self.style.NOTICE(f'Trennzeichen: {repr(delimiter)}'))
|
|
36
|
+
|
|
37
|
+
created = 0
|
|
38
|
+
skipped = 0
|
|
39
|
+
existing = 0
|
|
40
|
+
rows = 0
|
|
41
|
+
self._missing_parents = {}
|
|
42
|
+
self._header_skipped = False
|
|
43
|
+
self._existing_names = []
|
|
44
|
+
|
|
45
|
+
with open(csv_path, 'r', encoding=encoding, newline='') as f:
|
|
46
|
+
reader = csv.reader(f, delimiter=delimiter)
|
|
47
|
+
|
|
48
|
+
with transaction.atomic():
|
|
49
|
+
for parts in reader:
|
|
50
|
+
rows += 1
|
|
51
|
+
if not parts:
|
|
52
|
+
continue
|
|
53
|
+
# Skip header line if present
|
|
54
|
+
if not self._header_skipped and self._looks_like_header(parts):
|
|
55
|
+
self._header_skipped = True
|
|
56
|
+
continue
|
|
57
|
+
c, e, s, existing_name = self._import_species_row(models, parts)
|
|
58
|
+
created += c
|
|
59
|
+
existing += e
|
|
60
|
+
skipped += s
|
|
61
|
+
if e and existing_name:
|
|
62
|
+
# preserve insertion order, avoid duplicates
|
|
63
|
+
if existing_name not in self._existing_names:
|
|
64
|
+
self._existing_names.append(existing_name)
|
|
65
|
+
|
|
66
|
+
if dry_run:
|
|
67
|
+
# Rollback alle DB-Änderungen, aber dennoch Auswertung anzeigen
|
|
68
|
+
transaction.set_rollback(True)
|
|
69
|
+
|
|
70
|
+
self.stdout.write(self.style.SUCCESS(
|
|
71
|
+
f'Fertig. Erstellt: {created}, vorhanden: {existing}, übersprungen: {skipped}, Zeilen: {rows}'))
|
|
72
|
+
if dry_run:
|
|
73
|
+
self.stdout.write(self.style.WARNING('Dry-Run: Alle Änderungen wurden zurückgerollt.'))
|
|
74
|
+
|
|
75
|
+
if existing and self._existing_names:
|
|
76
|
+
self.stdout.write(self.style.NOTICE('Bereits vorhandene Taxa (exakt):'))
|
|
77
|
+
for nm in self._existing_names:
|
|
78
|
+
self.stdout.write(f'- {nm}')
|
|
79
|
+
|
|
80
|
+
if skipped and self._missing_parents:
|
|
81
|
+
self.stdout.write(self.style.WARNING('Übersprungene Zeilen wegen fehlendem Eltern-Taxon (Auszug):'))
|
|
82
|
+
# Zeige bis zu 25 fehlende Eltern mit Häufigkeit
|
|
83
|
+
shown = 0
|
|
84
|
+
for parent_name, count in sorted(self._missing_parents.items(), key=lambda x: -x[1]):
|
|
85
|
+
self.stdout.write(f'- {parent_name or "<leer>"}: {count}')
|
|
86
|
+
shown += 1
|
|
87
|
+
if shown >= 25:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
def _looks_like_header(self, parts: List[str]) -> bool:
|
|
91
|
+
if not parts:
|
|
92
|
+
return False
|
|
93
|
+
p0 = (parts[0] or '').strip().lower()
|
|
94
|
+
p1 = (parts[1] or '').strip().lower() if len(parts) > 1 else ''
|
|
95
|
+
# Typical header tokens
|
|
96
|
+
header_tokens = {'scientific name', 'name', 'latname', 'parent taxon', 'rank', 'de', 'en', 'nl', 'dk'}
|
|
97
|
+
return (p0 in header_tokens) or (p1 in header_tokens)
|
|
98
|
+
|
|
99
|
+
def _get_parent(self, models, parent_latname: str):
|
|
100
|
+
if not parent_latname:
|
|
101
|
+
return None
|
|
102
|
+
# exakter Treffer zuerst
|
|
103
|
+
parent = models.TaxonTreeModel.objects.filter(taxon_latname=parent_latname).first()
|
|
104
|
+
if parent:
|
|
105
|
+
return parent
|
|
106
|
+
# Fallback: case-insensitive Vergleich
|
|
107
|
+
return models.TaxonTreeModel.objects.filter(taxon_latname__iexact=parent_latname).first()
|
|
108
|
+
|
|
109
|
+
def _get_or_create_taxon(self, models, parent, latname: str, rank: str):
|
|
110
|
+
if not latname:
|
|
111
|
+
return None, False
|
|
112
|
+
|
|
113
|
+
r = (rank or 'species').strip().lower()
|
|
114
|
+
qs = models.TaxonTreeModel.objects.filter(taxon_latname=latname, rank=r)
|
|
115
|
+
if parent is None:
|
|
116
|
+
qs = qs.filter(is_root_taxon=True)
|
|
117
|
+
else:
|
|
118
|
+
qs = qs.filter(parent=parent)
|
|
119
|
+
|
|
120
|
+
instance = qs.first()
|
|
121
|
+
created = False
|
|
122
|
+
if not instance:
|
|
123
|
+
extra = {'rank': r}
|
|
124
|
+
if parent is None:
|
|
125
|
+
extra['is_root_taxon'] = True
|
|
126
|
+
else:
|
|
127
|
+
extra['parent'] = parent
|
|
128
|
+
instance = models.TaxonTreeModel.objects.create(latname, None, **extra)
|
|
129
|
+
created = True
|
|
130
|
+
return instance, created
|
|
131
|
+
|
|
132
|
+
def _add_locale_if_present(self, models, taxon, name: Optional[str], language: str):
|
|
133
|
+
if not taxon or not name:
|
|
134
|
+
return False
|
|
135
|
+
exists = models.TaxonLocaleModel.objects.filter(taxon=taxon, name=name, language=language).exists()
|
|
136
|
+
if not exists:
|
|
137
|
+
models.TaxonLocaleModel.objects.create(taxon, name, language, preferred=True)
|
|
138
|
+
return True
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _import_species_row(self, models, parts: List[str]):
|
|
142
|
+
# Unterstütztes Layout (aktuell):
|
|
143
|
+
# 0: scientific_name | 1: rank | 2: parent_lat | 3: parent_code | 4: de | 5: en | 6: nl | 7: da
|
|
144
|
+
|
|
145
|
+
# Normalize parts
|
|
146
|
+
parts = [(p or '').strip() for p in parts]
|
|
147
|
+
|
|
148
|
+
# Minimalprüfung: mindestens 4 Spalten (bis parent_code) erwartet
|
|
149
|
+
if len(parts) < 4:
|
|
150
|
+
return 0, 0, 1, None
|
|
151
|
+
|
|
152
|
+
name = parts[0]
|
|
153
|
+
rank = (parts[1] or 'species').strip().lower()
|
|
154
|
+
parent_latname = parts[2]
|
|
155
|
+
# parent_code aktuell informativ, optional verwendbar
|
|
156
|
+
# parent_code = parts[3]
|
|
157
|
+
locale_start_idx = 4
|
|
158
|
+
|
|
159
|
+
parent = self._get_parent(models, parent_latname) if parent_latname else None
|
|
160
|
+
# If no parent and this should be a root-level taxon, allow creation with no parent
|
|
161
|
+
if not parent and parent_latname:
|
|
162
|
+
key = parent_latname
|
|
163
|
+
self._missing_parents[key] = self._missing_parents.get(key, 0) + 1
|
|
164
|
+
return 0, 0, 1, None
|
|
165
|
+
|
|
166
|
+
taxon, created = self._get_or_create_taxon(models, parent, name, rank)
|
|
167
|
+
if not taxon:
|
|
168
|
+
return 0, 0, 1, None
|
|
169
|
+
|
|
170
|
+
# Attach vernacular locales if present (de, en, nl, da)
|
|
171
|
+
locale_langs = ['de', 'en', 'nl', 'da']
|
|
172
|
+
for offset, lang in enumerate(locale_langs):
|
|
173
|
+
idx = locale_start_idx + offset
|
|
174
|
+
val = parts[idx] if idx < len(parts) else ''
|
|
175
|
+
self._add_locale_if_present(models, taxon, val, lang)
|
|
176
|
+
|
|
177
|
+
return (1, 0, 0, None) if created else (0, 1, 0, name)
|