From 804fcecd3a3b476038e467d9587339634cb680c4 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 24 Nov 2022 09:45:57 +0100 Subject: [PATCH 01/49] issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/api/serializers.py | 18 ++- geonode/base/forms.py | 7 +- geonode/base/models.py | 134 ++++++++++++------ .../backends/pycsw_local_mappings.py | 15 +- geonode/geoserver/helpers.py | 4 +- geonode/layers/views.py | 5 +- 6 files changed, 122 insertions(+), 61 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 714709a2949..102e2501b06 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -368,7 +368,7 @@ def get_attribute(self, instance): return getattr(instance, self.contat_type) def to_representation(self, value): - return UserSerializer(embed=True, many=False).to_representation(value) + return [UserSerializer(embed=True, many=False).to_representation(v) for v in value] class DataBlobField(DynamicRelationField): @@ -447,9 +447,17 @@ def __init__(self, *args, **kwargs): self.fields['uuid'] = serializers.CharField(read_only=True) self.fields['resource_type'] = serializers.CharField(required=False) self.fields['polymorphic_ctype_id'] = serializers.CharField(read_only=True) - self.fields['owner'] = DynamicRelationField(UserSerializer, embed=True, many=False, read_only=True, required=False) - self.fields['poc'] = ContactRoleField('poc', read_only=True) - self.fields['metadata_author'] = ContactRoleField('metadata_author', read_only=True) + self.fields['owner'] = DynamicRelationField(UserSerializer, embed=True, many=False, read_only=True) + self.fields['metadata_author'] = ContactRoleField('metadata_author', read_only=True, required=False) + self.fields['processor'] = ContactRoleField('processor', read_only=True, required=False) + self.fields['publisher'] = ContactRoleField('publisher', read_only=True, required=False) + self.fields['custodian'] = ContactRoleField('custodian', read_only=True, required=False) + self.fields['poc'] = ContactRoleField('poc', read_only=True, required=False) + self.fields['distributor'] = ContactRoleField('distributor', read_only=True, required=False) + self.fields['resource_user'] = ContactRoleField('resource_user', read_only=True, required=False) + self.fields['resource_provider'] = ContactRoleField('resource_provider', read_only=True, required=False) + self.fields['originator'] = ContactRoleField('originator', read_only=True, required=False) + self.fields['principal_investigator'] = ContactRoleField('principal_investigator', read_only=True, required=False) self.fields['title'] = serializers.CharField() self.fields['abstract'] = serializers.CharField(required=False) self.fields['attribution'] = serializers.CharField(required=False) @@ -520,7 +528,7 @@ class Meta: view_name = 'base-resources-list' fields = ( 'pk', 'uuid', 'resource_type', 'polymorphic_ctype_id', 'perms', - 'owner', 'poc', 'metadata_author', + 'owner', 'poc', 'metadata_author', 'processor', 'publisher', 'custodian', 'distributor', 'resource_user', 'resource_provider', 'originator', 'principal_investigator', 'keywords', 'tkeywords', 'regions', 'category', 'title', 'abstract', 'attribution', 'alternate', 'doi', 'bbox_polygon', 'll_bbox_polygon', 'srid', 'date', 'date_type', 'edition', 'purpose', 'maintenance_frequency', diff --git a/geonode/base/forms.py b/geonode/base/forms.py index a3146b6ecd9..67ff67c7e9b 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -440,7 +440,6 @@ class ResourceBaseForm(TranslationModelForm): input_formats=['%Y-%m-%d %H:%M %p'], widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD HH:mm a"}) ) - poc = forms.ModelChoiceField( empty_label=_("Person outside GeoNode (fill form)"), label=_("Point of Contact"), @@ -449,13 +448,13 @@ class ResourceBaseForm(TranslationModelForm): username='AnonymousUser'), widget=autocomplete.ModelSelect2(url='autocomplete_profile')) - metadata_author = forms.ModelChoiceField( - empty_label=_("Person outside GeoNode (fill form)"), + metadata_author = forms.ModelMultipleChoiceField( + #empty_label=_("Person outside GeoNode (fill form)"), label=_("Metadata Author"), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), - widget=autocomplete.ModelSelect2(url='autocomplete_profile')) + widget=TaggitSelect2Custom(url='autocomplete_profile')) keywords = TagField( label=_("Free-text Keywords"), diff --git a/geonode/base/models.py b/geonode/base/models.py index 99207f07269..0624e83b395 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -24,6 +24,7 @@ import uuid import logging import traceback +from typing import Union, List, Optional from sequences.models import Sequence from sequences import get_next_value @@ -1278,14 +1279,6 @@ def organizationname(self): def restriction_code(self): return self.restriction_code_type.gn_description if self.restriction_code_type else None - @property - def publisher(self): - return self.poc.get_full_name() or self.poc.username - - @property - def contributor(self): - return self.metadata_author.get_full_name() or self.metadata_author.username - @property def topiccategory(self): return self.category.identifier if self.category else None @@ -1847,54 +1840,105 @@ def maintenance_frequency_title(self): def language_title(self): return [v for v in enumerations.ALL_LANGUAGES if v[0] == self.language][0][1].title() - def _set_poc(self, poc): - # reset any poc assignation to this resource - ContactRole.objects.filter( - role='pointOfContact', - resource=self).delete() - # create the new assignation - ContactRole.objects.create( - role='pointOfContact', - resource=self, - contact=poc) + def add_missing_metadata_author_or_poc(self): + """ + Set metadata_author and/or point of contact (poc) to a resource when any of them is missing + """ + if not self.metadata_author: + self.metadata_author = [self.owner] + if not self.poc: + self.poc = [self.owner] + + def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: + """ + generell getter of for all contact roles except owner - def _get_poc(self): + param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested + return List(ContactRole): returns the requested contact role from the database + """ try: - the_poc = ContactRole.objects.get( - role='pointOfContact', resource=self).contact + contact_role = ContactRole.objects.filter( + role=role, resource=self) + contacts = [cr.contact for cr in contact_role] except ContactRole.DoesNotExist: - the_poc = None - return the_poc + contacts = None + return contacts - poc = property(_get_poc, _set_poc) + def _set_contact_role_element(self, user_profile, role: str): + """ + generell setter for all contact roles except owner in resource base - def _set_metadata_author(self, metadata_author): - # reset any metadata_author assignation to this resource - ContactRole.objects.filter(role='author', resource=self).delete() + param contact_role (ContactRole): + param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set + """ + if not isinstance(user_profile, get_user_model()) : + return None + ContactRole.objects.filter(role=role, resource=self).delete() # create the new assignation ContactRole.objects.create( - role='author', + role=role, resource=self, - contact=metadata_author) - - def _get_metadata_author(self): - try: - the_ma = ContactRole.objects.get( - role='author', resource=self).contact - except ContactRole.DoesNotExist: - the_ma = None - return the_ma + contact=user_profile) - def add_missing_metadata_author_or_poc(self): - """ - Set metadata_author and/or point of contact (poc) to a resource when any of them is missing - """ - if not self.metadata_author: - self.metadata_author = self.owner - if not self.poc: - self.poc = self.owner + def _get_poc(self): return self._get_contact_role_elements(role="pointOfContact") + def _set_poc(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="pointOfContact") + poc = property(_get_poc, _set_poc) + @property + def poc_csv(self): return ','.join(p.get_full_name() or p.username for p in self.poc) + def _get_metadata_author(self): return self._get_contact_role_elements(role="author") + def _set_metadata_author(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="author") metadata_author = property(_get_metadata_author, _set_metadata_author) + @property + def metadata_author_csv(self): return ','.join(p.get_full_name() or p.username for p in self.metadata_author) + + def _get_processor(self): return self._get_contact_role_elements(role="processor") + def _set_processor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="processor") + processor = property(_get_processor, _set_processor) + @property + def processor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.processor) + + def _get_publisher(self): return self._get_contact_role_elements(role="publisher") + def _set_publisher(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="publisher") + publisher = property(_get_publisher, _set_publisher) + @property + def publisher_csv(self): return ','.join(p.get_full_name() or p.username for p in self.publisher) + + def _get_custodian(self): return self._get_contact_role_elements(role="custodian") + def _set_custodian(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="custodian") + custodian = property(_get_custodian, _set_custodian) + @property + def custodian_csv(self): return ','.join(p.get_full_name() or p.username for p in self.custodian) + + def _get_distributor(self): return self._get_contact_role_elements(role="distributor") + def _set_distributor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="distributor") + distributor = property(_get_distributor, _set_distributor) + @property + def distributor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.distributor) + + def _get_resource_user(self): return self._get_contact_role_elements(role="resource_user") + def _set_resource_user(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="resource_user") + resource_user = property(_get_resource_user, _set_resource_user) + @property + def resource_user_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_user) + + def _get_resource_provider(self): return self._get_contact_role_elements(role="resource_provider") + def _set_resource_provider(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="resource_provider") + resource_provider = property(_get_resource_provider, _set_resource_provider) + @property + def resource_provider_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_provider) + + def _get_originator(self): return self._get_contact_role_elements(role="originator") + def _set_originator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="originator") + originator = property(_get_originator, _set_originator) + @property + def originator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.originator) + + def _get_principal_investigator(self): return self._get_contact_role_elements(role="principal_investigator") + def _set_principal_investigator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="principal_investigator") + principal_investigator = property(_get_principal_investigator, _set_principal_investigator) + @property + def principal_investigator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.principal_investigator) class LinkManager(models.Manager): diff --git a/geonode/catalogue/backends/pycsw_local_mappings.py b/geonode/catalogue/backends/pycsw_local_mappings.py index ead12f37b9a..71cb6dafaba 100644 --- a/geonode/catalogue/backends/pycsw_local_mappings.py +++ b/geonode/catalogue/backends/pycsw_local_mappings.py @@ -17,6 +17,7 @@ # ######################################################################### +# based on https://github.com/geopython/pycsw/blob/master/pycsw/core/config.py MD_CORE_MODEL = { 'typename': 'pycsw:CoreMetadata', 'outputschema': 'http://pycsw.org/metadata', @@ -74,8 +75,18 @@ 'pycsw:SpecificationDate': 'specificationdate', 'pycsw:SpecificationDateType': 'specificationdatetype', 'pycsw:Creator': 'creator', - 'pycsw:Publisher': 'publisher', - 'pycsw:Contributor': 'contributor', + 'pycsw:Publisher': 'publisher_csv', + 'pycsw:Contributor': 'contributor_csv', + 'pycsw:Processor': 'processor_csw', + + # 'pycsw:MetadataAuthor': 'metadata_author_csv', + # 'pycsw:Custodian': 'custodian_csv', + # 'pycsw:Distributor': 'distributor_csv', + # 'pycsw:ResourceUser': 'resource_user_csv', + # 'pycsw:ResourceProvider': 'resource_provider_csv', + # 'pycsw:Originator': 'originator_csv', + # 'pycsw:PrincipalInvestigator': 'principal_investigator_csv', + 'pycsw:Relation': 'relation', 'pycsw:Links': 'download_links', } diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index ba235b0dce6..2b4c3d949f3 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2123,13 +2123,13 @@ def sync_instance_with_geoserver( if instance.poc: # gsconfig now utilizes an attribution dictionary gs_resource.attribution = { - 'title': str(instance.poc), + 'title': str(instance.poc_csv), 'width': None, 'height': None, 'href': None, 'url': None, 'type': None} - profile = get_user_model().objects.get(username=instance.poc.username) + profile = get_user_model().objects.get(username=instance.poc[0].username) site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL gs_resource.attribution_link = site_url + profile.get_absolute_url() diff --git a/geonode/layers/views.py b/geonode/layers/views.py index c94feccb9fe..8b66844a68f 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -630,7 +630,6 @@ def dataset_metadata( values = [] values = [keyword.id for keyword in topic_thesaurus if int(tid) == keyword.thesaurus.id] tkeywords_form.fields[tid].initial = values - if request.method == "POST" and dataset_form.is_valid() and attribute_form.is_valid( ) and category_form.is_valid() and tkeywords_form.is_valid() and timeseries_form.is_valid(): new_poc = dataset_form.cleaned_data['poc'] @@ -778,7 +777,7 @@ def dataset_metadata( dataset_form.fields['is_approved'].widget.attrs.update({'disabled': 'true'}) if poc is not None: - dataset_form.fields['poc'].initial = poc.id + dataset_form.fields['poc'].initial = poc[0].id # [ p.username for p in poc ] poc_form = ProfileForm(prefix="poc") poc_form.hidden = True else: @@ -786,7 +785,7 @@ def dataset_metadata( poc_form.hidden = False if metadata_author is not None: - dataset_form.fields['metadata_author'].initial = metadata_author.id + dataset_form.fields['metadata_author'].initial = [ma.username for ma in metadata_author] author_form = ProfileForm(prefix="author") author_form.hidden = True else: From 7e23afbed3fa24b5b441925b7408477572180db3 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 25 Nov 2022 14:33:07 +0100 Subject: [PATCH 02/49] issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/forms.py | 85 +++++++++++++-- geonode/base/models.py | 64 ++++++++--- geonode/base/widgets.py | 16 +++ .../templates/datasets/dataset_metadata.html | 8 +- .../datasets/dataset_metadata_advanced.html | 8 +- geonode/layers/templates/layouts/panels.html | 83 +++++++++++++-- geonode/layers/views.py | 100 ++++++------------ 7 files changed, 263 insertions(+), 101 deletions(-) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 67ff67c7e9b..0d1abd2c8ab 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -47,7 +47,7 @@ License, Region, ResourceBase, Thesaurus, ThesaurusKeyword, ThesaurusKeywordLabel, ThesaurusLabel, TopicCategory) -from geonode.base.widgets import TaggitSelect2Custom +from geonode.base.widgets import TaggitSelect2Custom, TaggitProfileSelect2Custom from geonode.base.fields import MultiThesauriField from geonode.documents.models import Document from geonode.layers.models import Dataset @@ -369,6 +369,18 @@ def _get_thesauro_title_label(item, lang): return tname.first() +class ContactRoleMultipleChoiceField(forms.ModelMultipleChoiceField): + # TODO ERROR HANDLING + def clean(self, value): + # try: + users = get_user_model().objects.filter(username__in=value) + # except: + # raise forms.ValidationError(_("Something went wrong in finding the profiles")) + # if len(users) < len(value): + # raise forms.ValidationError(_("not alle given profiles are found, maybe a typo?")) + return users + + class ResourceBaseDateTimePicker(DateTimePicker): def build_attrs(self, base_attrs=None, extra_attrs=None, **kwargs): @@ -440,21 +452,76 @@ class ResourceBaseForm(TranslationModelForm): input_formats=['%Y-%m-%d %H:%M %p'], widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD HH:mm a"}) ) - poc = forms.ModelChoiceField( - empty_label=_("Person outside GeoNode (fill form)"), - label=_("Point of Contact"), + + metadata_author = ContactRoleMultipleChoiceField( + label=_("Metadata Author"), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), - widget=autocomplete.ModelSelect2(url='autocomplete_profile')) + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) - metadata_author = forms.ModelMultipleChoiceField( - #empty_label=_("Person outside GeoNode (fill form)"), - label=_("Metadata Author"), + processor = ContactRoleMultipleChoiceField( + label=_("Processor"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + publisher = ContactRoleMultipleChoiceField( + label=_("Publisher"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + custodian = ContactRoleMultipleChoiceField( + label=_("Custodian"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + poc = ContactRoleMultipleChoiceField( + label=_("Person of Contact"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + distributor = ContactRoleMultipleChoiceField( + label=_("Distributor"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + resource_user = ContactRoleMultipleChoiceField( + label=_("Resource User"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + resource_provider = ContactRoleMultipleChoiceField( + label=_("Resource Provider"), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + originator = ContactRoleMultipleChoiceField( + label=_('Originator'), + required=False, + queryset=get_user_model().objects.exclude( + username='AnonymousUser'), + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) + + principal_investigator = ContactRoleMultipleChoiceField( + label=_('Principal Investigator'), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), - widget=TaggitSelect2Custom(url='autocomplete_profile')) + widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) keywords = TagField( label=_("Free-text Keywords"), diff --git a/geonode/base/models.py b/geonode/base/models.py index 0624e83b395..51b96b0e3e3 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -24,7 +24,7 @@ import uuid import logging import traceback -from typing import Union, List, Optional +from typing import List, Optional from sequences.models import Sequence from sequences import get_next_value @@ -39,6 +39,7 @@ from django.contrib.auth.models import Group from django.core.files.base import ContentFile from django.contrib.auth import get_user_model +from django.db.models.query import QuerySet from django.db.models.fields.json import JSONField from django.utils.functional import cached_property, classproperty from django.contrib.gis.geos import Polygon, Point @@ -1845,9 +1846,41 @@ def add_missing_metadata_author_or_poc(self): Set metadata_author and/or point of contact (poc) to a resource when any of them is missing """ if not self.metadata_author: - self.metadata_author = [self.owner] + self.metadata_author = self.owner if not self.poc: - self.poc = [self.owner] + self.poc = self.owner + + def get_multivalue_role_property_names(self) -> List[str]: + """ _summary_: returns list of property names for all contact roles able to + handle multiple profile_users + + Returns: + _type_: List(str) + _description: list of names + """ + return [ + 'metadata_author', + 'publisher', + 'custodian', + 'poc', + 'distributor', + 'resource_user', + 'resource_provider', + 'originator', + 'principal_investigator' + ] + + def get_multivalue_required_role_property_names(self) -> List[str]: + """ _summary_: returns list of property names for all contact roles that are required + + Returns: + _type_: List(str) + _description: list of names + """ + return [ + 'metadata_author', + 'poc', + ] def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: """ @@ -1866,19 +1899,26 @@ def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: def _set_contact_role_element(self, user_profile, role: str): """ - generell setter for all contact roles except owner in resource base + general setter for all contact roles except owner in resource base param contact_role (ContactRole): param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set """ - if not isinstance(user_profile, get_user_model()) : - return None - ContactRole.objects.filter(role=role, resource=self).delete() - # create the new assignation - ContactRole.objects.create( - role=role, - resource=self, - contact=user_profile) + + def __create_role__(resource, role, user_profile): + ContactRole.objects.create( + role=role, + resource=resource, + contact=user_profile) + + if isinstance(user_profile, QuerySet): + ContactRole.objects.filter(role=role, resource=self).delete() + [__create_role__(self, role, user) for user in user_profile] + elif isinstance(user_profile, get_user_model()): + ContactRole.objects.filter(role=role, resource=self).delete() + __create_role__(self, role, user_profile) + else: + logger.error(f'Bad profile format for role: {role} ...') def _get_poc(self): return self._get_contact_role_elements(role="pointOfContact") def _set_poc(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="pointOfContact") diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py index 08e41930e23..5e7b9597d86 100644 --- a/geonode/base/widgets.py +++ b/geonode/base/widgets.py @@ -20,3 +20,19 @@ def value_from_datadict(self, data, files, name): return value except TypeError: return "" + + +class TaggitProfileSelect2Custom(TaggitSelect2): + """Overriding Select2 tag widget for ContactRoleField. + """ + + def value_from_datadict(self, data, files, name): + """Handle multi-profiles. + + returns list of selected elements + """ + try: + ret_list = data.getlist(name) + return ret_list + except TypeError: + return [] diff --git a/geonode/layers/templates/datasets/dataset_metadata.html b/geonode/layers/templates/datasets/dataset_metadata.html index 9a1fb5aaaee..945c5d5f9b2 100644 --- a/geonode/layers/templates/datasets/dataset_metadata.html +++ b/geonode/layers/templates/datasets/dataset_metadata.html @@ -43,12 +43,12 @@

{% trans "Metadata" %} {% blocktrans with dataset.ti Some of your original metadata may have been lost.{% endblocktrans %}

{% endif %} - {% if dataset_form.errors or attribute_form.errors or category_form.errors or author_form.errors or poc.errors or tkeywords_form.errors %} + {% if dataset_form.errors or attribute_form.errors or category_form.errors or metadata_author_form.errors or poc.errors or tkeywords_form.errors %}

{% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

    - {% if author_form.errors %} + {% if metadata_author_form.errors %}
  • {% trans "Metadata Author" %}
  • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
  • {% trans "Point of Contact" %}
  • @@ -91,7 +91,7 @@

    {% trans "Point of Contact" %}

    diff --git a/geonode/layers/templates/datasets/dataset_metadata_advanced.html b/geonode/layers/templates/datasets/dataset_metadata_advanced.html index fff3b8aef0f..0b1589109c8 100644 --- a/geonode/layers/templates/datasets/dataset_metadata_advanced.html +++ b/geonode/layers/templates/datasets/dataset_metadata_advanced.html @@ -52,12 +52,12 @@

    {% trans "Edit Metadata" %}

    {% endblock metadata_uploaded_check %} {% block dataset_form_errors %} - {% if dataset_form.errors or attribute_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if dataset_form.errors or attribute_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

    {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

      - {% if author_form.errors %} + {% if metadata_author.errors %}
    • {% trans "Metadata Author" %}
    • - {{ author_form.errors }} + {{ metadata_author.errors }} {% endif %} {% if poc_form.errors %}
    • {% trans "Point of Contact" %}
    • @@ -210,7 +210,7 @@

      {% trans "Point of Contact" %}

      {% block metadata_provider %} {% endblock metadata_provider %} diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index 435804aac75..613eaa427be 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -472,7 +472,6 @@ {% endblock dataset_constraints %}
      -
      @@ -557,8 +556,8 @@ {% endblock layer_extra_metadata %} -
      -
      +
      +
      {% trans "Responsible Parties" %}
      {% block dataset_poc %}
      @@ -573,22 +572,94 @@ {% block dataset_owner %}
      - {{ dataset_form.owner }}
      {% endblock dataset_owner %} +
      +
      + + {% trans "toggle more Contact Roles" %} +
      +
      {% trans "more Metadata Contact Roles" %}
      +
      {% block dataset_metadata_author %}
      - {{ dataset_form.metadata_author }}
      {% endblock dataset_metadata_author %}
      + +
      + {% block dataset_processor %} +
      + + {{ dataset_form.processor }} +
      + {% endblock dataset_processor %} +
      +
      + {% block dataset_publisher %} +
      + + {{ dataset_form.publisher }} +
      + {% endblock dataset_publisher %} +
      +
      + {% block dataset_custodian %} +
      + + {{ dataset_form.custodian }} +
      + {% endblock dataset_custodian %} +
      +
      + {% block dataset_distributor %} +
      + + {{ dataset_form.distributor }} +
      + {% endblock dataset_distributor %} +
      +
      + {% block dataset_resource_user %} +
      + + {{ dataset_form.resource_user }} +
      + {% endblock dataset_resource_user %} +
      +
      + {% block dataset_resource_provider %} +
      + + {{ dataset_form.resource_provider }} +
      + {% endblock dataset_resource_provider %} +
      +
      + {% block dataset_originator %} +
      + + {{ dataset_form.originator }} +
      + {% endblock dataset_originator %} +
      +
      + {% block dataset_principal_investigator %} +
      + + {{ dataset_form.principal_investigator }} +
      + {% endblock dataset_principal_investigator %} +
      +
      +
      - + {% endblock %} {% block dataset %} diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 8b66844a68f..2bec0b67388 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -433,7 +433,6 @@ def dataset_metadata( raise Http404(Exception(_("Not found"), e)) if not layer: raise Http404(_("Not found")) - dataset_attribute_set = inlineformset_factory( Dataset, Attribute, @@ -445,10 +444,8 @@ def dataset_metadata( topic_thesaurus = layer.tkeywords.all() # Add metadata_author or poc if missing - layer.add_missing_metadata_author_or_poc() - poc = layer.poc - metadata_author = layer.metadata_author + layer.add_missing_metadata_author_or_poc() # assert False, str(dataset_bbox) config = layer.attribute_config() @@ -597,7 +594,6 @@ def dataset_metadata( ) timeseries_form.fields.get('attribute').queryset = layer.attributes.filter(attribute_type__in=['xsd:dateTime']) timeseries_form.fields.get('end_attribute').queryset = layer.attributes.filter(attribute_type__in=['xsd:dateTime']) - # Create THESAURUS widgets lang = settings.THESAURUS_DEFAULT_LANG if hasattr(settings, 'THESAURUS_DEFAULT_LANG') else 'en' if hasattr(settings, 'THESAURUS') and settings.THESAURUS: @@ -632,44 +628,6 @@ def dataset_metadata( tkeywords_form.fields[tid].initial = values if request.method == "POST" and dataset_form.is_valid() and attribute_form.is_valid( ) and category_form.is_valid() and tkeywords_form.is_valid() and timeseries_form.is_valid(): - new_poc = dataset_form.cleaned_data['poc'] - new_author = dataset_form.cleaned_data['metadata_author'] - - if new_poc is None: - if poc is None: - poc_form = ProfileForm( - request.POST, - prefix="poc", - instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.is_valid(): - if len(poc_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = poc_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set a point of contact for this resource')) - poc = None - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", - instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.is_valid(): - if len(author_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = author_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set an author for this resource')) - metadata_author = None - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() new_category = None if category_form and 'category_choice_field' in category_form.cleaned_data and\ @@ -686,11 +644,31 @@ def dataset_metadata( la.featureinfo_type = form["featureinfo_type"] la.save() - if new_poc is not None or new_author is not None: - if new_poc is not None: - layer.poc = new_poc - if new_author is not None: - layer.metadata_author = new_author + for role in layer.get_multivalue_role_property_names(): + new_role = dataset_form.cleaned_data[role] + old_role = layer.__getattribute__(role) + if new_role is None or len(new_role) == 0: + if old_role is None: + role_form = ProfileForm( + request.POST, + prefix=role, + instance=old_role) + else: + role_form = ProfileForm(request.POST, prefix=role) + + # check if form is valid and required but empty + if role_form.is_valid() and\ + role in layer.get_multivalue_required_role_property_names() and\ + len(role_form.cleaned_data['profile']) == 0: + pass + # role_form.add_error(role,_('You must set a {} for this resource').format(role)) + old_role = None + + # if values have changed and are valid + if role_form.has_changed and role_form.is_valid(): + print(new_role) + #role_form.save() + layer.__setattr__(role, new_role) new_keywords = current_keywords if request.keyword_readonly else dataset_form.cleaned_data['keywords'] new_regions = [x.strip() for x in dataset_form.cleaned_data['regions']] @@ -776,21 +754,13 @@ def dataset_metadata( if not AdvancedSecurityWorkflowManager.is_allowed_to_approve(request.user, layer): dataset_form.fields['is_approved'].widget.attrs.update({'disabled': 'true'}) - if poc is not None: - dataset_form.fields['poc'].initial = poc[0].id # [ p.username for p in poc ] - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - else: - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = False - - if metadata_author is not None: - dataset_form.fields['metadata_author'].initial = [ma.username for ma in metadata_author] - author_form = ProfileForm(prefix="author") - author_form.hidden = True - else: - author_form = ProfileForm(prefix="author") - author_form.hidden = False + # define contact role forms + contact_role_forms_context = {} + for role in layer.get_multivalue_role_property_names(): + dataset_form.fields[role].initial = [p.username for p in layer.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) @@ -799,8 +769,6 @@ def dataset_metadata( "resource": layer, "dataset": layer, "dataset_form": dataset_form, - "poc_form": poc_form, - "author_form": author_form, "attribute_form": attribute_form, "timeseries_form": timeseries_form, "category_form": category_form, @@ -820,7 +788,7 @@ def dataset_metadata( | set(getattr(settings, 'UI_REQUIRED_FIELDS', [])) ) - }) + } | contact_role_forms_context) @login_required From 2209f5ef456b531935bfeb962c53afb5d9b1c723 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 28 Nov 2022 16:13:15 +0100 Subject: [PATCH 03/49] issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/api/serializers.py | 20 ++++----- geonode/base/forms.py | 4 +- geonode/base/models.py | 1 + .../datasets/dataset_metadata_advanced.html | 6 +-- geonode/layers/views.py | 43 ++++++++----------- geonode/people/forms.py | 3 +- 6 files changed, 36 insertions(+), 41 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 102e2501b06..c37669bd0ae 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -448,16 +448,16 @@ def __init__(self, *args, **kwargs): self.fields['resource_type'] = serializers.CharField(required=False) self.fields['polymorphic_ctype_id'] = serializers.CharField(read_only=True) self.fields['owner'] = DynamicRelationField(UserSerializer, embed=True, many=False, read_only=True) - self.fields['metadata_author'] = ContactRoleField('metadata_author', read_only=True, required=False) - self.fields['processor'] = ContactRoleField('processor', read_only=True, required=False) - self.fields['publisher'] = ContactRoleField('publisher', read_only=True, required=False) - self.fields['custodian'] = ContactRoleField('custodian', read_only=True, required=False) - self.fields['poc'] = ContactRoleField('poc', read_only=True, required=False) - self.fields['distributor'] = ContactRoleField('distributor', read_only=True, required=False) - self.fields['resource_user'] = ContactRoleField('resource_user', read_only=True, required=False) - self.fields['resource_provider'] = ContactRoleField('resource_provider', read_only=True, required=False) - self.fields['originator'] = ContactRoleField('originator', read_only=True, required=False) - self.fields['principal_investigator'] = ContactRoleField('principal_investigator', read_only=True, required=False) + self.fields['metadata_author'] = ContactRoleField('metadata_author', required=False) + self.fields['processor'] = ContactRoleField('processor', required=False) + self.fields['publisher'] = ContactRoleField('publisher', required=False) + self.fields['custodian'] = ContactRoleField('custodian', required=False) + self.fields['poc'] = ContactRoleField('poc', required=False) + self.fields['distributor'] = ContactRoleField('distributor', required=False) + self.fields['resource_user'] = ContactRoleField('resource_user', required=False) + self.fields['resource_provider'] = ContactRoleField('resource_provider', required=False) + self.fields['originator'] = ContactRoleField('originator', required=False) + self.fields['principal_investigator'] = ContactRoleField('principal_investigator', required=False) self.fields['title'] = serializers.CharField() self.fields['abstract'] = serializers.CharField(required=False) self.fields['attribution'] = serializers.CharField(required=False) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 0d1abd2c8ab..b58ae4e610b 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -455,7 +455,7 @@ class ResourceBaseForm(TranslationModelForm): metadata_author = ContactRoleMultipleChoiceField( label=_("Metadata Author"), - required=False, + required=True, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) @@ -483,7 +483,7 @@ class ResourceBaseForm(TranslationModelForm): poc = ContactRoleMultipleChoiceField( label=_("Person of Contact"), - required=False, + required=True, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) diff --git a/geonode/base/models.py b/geonode/base/models.py index 51b96b0e3e3..353f3e0ff5d 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1860,6 +1860,7 @@ def get_multivalue_role_property_names(self) -> List[str]: """ return [ 'metadata_author', + 'processor', 'publisher', 'custodian', 'poc', diff --git a/geonode/layers/templates/datasets/dataset_metadata_advanced.html b/geonode/layers/templates/datasets/dataset_metadata_advanced.html index 0b1589109c8..c7169f960cb 100644 --- a/geonode/layers/templates/datasets/dataset_metadata_advanced.html +++ b/geonode/layers/templates/datasets/dataset_metadata_advanced.html @@ -55,9 +55,9 @@

      {% trans "Edit Metadata" %}

      {% if dataset_form.errors or attribute_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

      {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

        - {% if metadata_author.errors %} + {% if metadata_author_form.errors %}
      • {% trans "Metadata Author" %}
      • - {{ metadata_author.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
      • {% trans "Point of Contact" %}
      • @@ -210,7 +210,7 @@

        {% trans "Point of Contact" %}

        {% block metadata_provider %} {% endblock metadata_provider %} diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 2bec0b67388..d5789c2a1af 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -35,7 +35,6 @@ from django.contrib import messages from django.shortcuts import render from django.utils.html import escape -from django.forms.utils import ErrorList from django.contrib.auth import get_user_model from django.template.loader import get_template from django.utils.translation import ugettext as _ @@ -644,31 +643,25 @@ def dataset_metadata( la.featureinfo_type = form["featureinfo_type"] la.save() + profile_errors = [] for role in layer.get_multivalue_role_property_names(): - new_role = dataset_form.cleaned_data[role] - old_role = layer.__getattribute__(role) - if new_role is None or len(new_role) == 0: - if old_role is None: - role_form = ProfileForm( - request.POST, - prefix=role, - instance=old_role) - else: - role_form = ProfileForm(request.POST, prefix=role) - - # check if form is valid and required but empty - if role_form.is_valid() and\ - role in layer.get_multivalue_required_role_property_names() and\ - len(role_form.cleaned_data['profile']) == 0: - pass - # role_form.add_error(role,_('You must set a {} for this resource').format(role)) - old_role = None - - # if values have changed and are valid - if role_form.has_changed and role_form.is_valid(): - print(new_role) - #role_form.save() - layer.__setattr__(role, new_role) + new_profiles = dataset_form.cleaned_data[role] + + # if new defined profile is None|empty and required set previous profiles + if (new_profiles is None or len(new_profiles) == 0) and role in layer.get_multivalue_required_role_property_names(): + new_profiles = layer.__getattribute__(role) + + for profile in new_profiles: + role_form = ProfileForm(request.POST,prefix=role,instance=profile) + if not role_form.is_valid(): + profile_errors.append(role_form.errors) + + if len(profile_errors) == 0: + layer.__setattr__(role, new_profiles) + else: + return HttpResponse(json.dumps({'message': profile_errors}, status_code=400)) + + layer.save() new_keywords = current_keywords if request.keyword_readonly else dataset_form.cleaned_data['keywords'] new_regions = [x.strip() for x in dataset_form.cleaned_data['regions']] diff --git a/geonode/people/forms.py b/geonode/people/forms.py index 540056e99ce..6830eedc7cd 100644 --- a/geonode/people/forms.py +++ b/geonode/people/forms.py @@ -90,5 +90,6 @@ class Meta: 'is_staff', 'is_superuser', 'is_active', - 'date_joined' + 'date_joined', + 'language' ) From d268a7010861fc3a7faf0708592beedd648125bd Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Tue, 29 Nov 2022 17:25:13 +0100 Subject: [PATCH 04/49] issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 70 +-- .../base/_resourcebase_info_panel.html | 4 +- .../templates/geonode_metadata_full.html | 7 +- geonode/catalogue/views.py | 38 +- .../documents/document_metadata.html | 8 +- .../documents/document_metadata_advanced.html | 8 +- geonode/documents/views.py | 63 +- .../harvesting/harvesters/geonodeharvester.py | 2 + geonode/layers/views.py | 24 +- geonode/maps/models.py | 2 +- geonode/maps/templates/maps/map_metadata.html | 2 +- .../templates/maps/map_metadata_advanced.html | 2 +- geonode/maps/views.py | 58 +- geonode/templates/metadata_detail.html | 555 ++++++++++++++++-- 14 files changed, 597 insertions(+), 246 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index 353f3e0ff5d..e9135089a30 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -110,43 +110,6 @@ class ContactRole(models.Model): 'function performed by the responsible ' 'party')) - def clean(self): - """ - Make sure there is only one poc and author per resource - """ - - if not hasattr(self, 'resource'): - # The ModelForm will already raise a Validation error for a missing resource. - # Re-raising an empty error here ensures the rest of this method isn't - # executed. - raise ValidationError('') - - if (self.role == self.resource.poc) or ( - self.role == self.resource.metadata_author): - contacts = self.resource.contacts.filter( - contactrole__role=self.role) - if contacts.count() == 1: - # only allow this if we are updating the same contact - if self.contact != contacts.get(): - raise ValidationError( - f'There can be only one {self.role} for a given resource') - if self.contact is None: - # verify that any unbound contact is only associated to one - # resource - bounds = ContactRole.objects.filter(contact=self.contact).count() - if bounds > 1: - raise ValidationError( - 'There can be one and only one resource linked to an unbound contact' % - self.role) - elif bounds == 1: - # verify that if there was one already, it corresponds to this - # instance - if ContactRole.objects.filter( - contact=self.contact).get().id != self.id: - raise ValidationError( - 'There can be one and only one resource linked to an unbound contact' % - self.role) - class Meta: unique_together = (("contact", "resource", "role"),) @@ -1850,7 +1813,8 @@ def add_missing_metadata_author_or_poc(self): if not self.poc: self.poc = self.owner - def get_multivalue_role_property_names(self) -> List[str]: + @staticmethod + def get_multivalue_role_property_names() -> List[str]: """ _summary_: returns list of property names for all contact roles able to handle multiple profile_users @@ -1871,7 +1835,8 @@ def get_multivalue_role_property_names(self) -> List[str]: 'principal_investigator' ] - def get_multivalue_required_role_property_names(self) -> List[str]: + @staticmethod + def get_multivalue_required_role_property_names() -> List[str]: """ _summary_: returns list of property names for all contact roles that are required Returns: @@ -1882,6 +1847,23 @@ def get_multivalue_required_role_property_names(self) -> List[str]: 'metadata_author', 'poc', ] + # from geonode.base.forms import ResourceBaseForm; unable due to circular ... + + def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: + """ gets a ResourceBaseForm and extracts the Contact Role elements from it + + Args: + resource_base_form (ResourceBaseForm): ResourceBaseForm with contact roles set + return (bool): returns true if all contact roles could be set, else false + """ + failed = False + for role in self.get_multivalue_role_property_names(): + try: + self.__setattr__(role, resource_base_form.cleaned_data[role]) + except: + logger.warning(f"unable to set contact role {role} for {self} ...") + failed = True + return failed def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: """ @@ -1906,20 +1888,20 @@ def _set_contact_role_element(self, user_profile, role: str): param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set """ - def __create_role__(resource, role, user_profile): - ContactRole.objects.create( + def __create_role__(resource, role: str, user_profile): + return ContactRole.objects.create( role=role, resource=resource, contact=user_profile) if isinstance(user_profile, QuerySet): ContactRole.objects.filter(role=role, resource=self).delete() - [__create_role__(self, role, user) for user in user_profile] + return [__create_role__(self, role, user) for user in user_profile] elif isinstance(user_profile, get_user_model()): ContactRole.objects.filter(role=role, resource=self).delete() - __create_role__(self, role, user_profile) + return [__create_role__(self, role, user_profile)] else: - logger.error(f'Bad profile format for role: {role} ...') + logger.error(f"Bad profile format for role: {role} ...") def _get_poc(self): return self._get_contact_role_elements(role="pointOfContact") def _set_poc(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="pointOfContact") diff --git a/geonode/base/templates/base/_resourcebase_info_panel.html b/geonode/base/templates/base/_resourcebase_info_panel.html index a5f6110b6d6..4696ac98a90 100644 --- a/geonode/base/templates/base/_resourcebase_info_panel.html +++ b/geonode/base/templates/base/_resourcebase_info_panel.html @@ -109,8 +109,10 @@ {% endif %} {% if resource.poc.user %} + {% for poc in resource.poc %}
        {% trans "Point of Contact" %}
        -
        {{ resource.poc.user.username }}
        +
        {{ poc.user.username }}
        + {% endfor%} {% endif %} {% if resource.group %} diff --git a/geonode/catalogue/templates/geonode_metadata_full.html b/geonode/catalogue/templates/geonode_metadata_full.html index adcddf43918..a5cd9277ce1 100644 --- a/geonode/catalogue/templates/geonode_metadata_full.html +++ b/geonode/catalogue/templates/geonode_metadata_full.html @@ -73,9 +73,10 @@

        {{ resource.title }}

        {{resource.owner}}
        {% trans "Point of Contact" %}
        -
        {{extra_res_md.poc_last_name}}
        -
        {{extra_res_md.poc_email}}
        - + {% for poc in extra_res_md.poc %} +
        {{poc.last_name}}
        +
        {{ poc.email}}
        + {% endfor %}
        {% trans "Purpose" %}
        {% if resource.purpose %} diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index 021063e0bc3..bc91ac9b709 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -288,12 +288,26 @@ def csw_render_extra_format_txt(request, layeruuid, resname): content += fst(attr.attribute_label) + s content += fst(attr.description) + sc - pocr = ContactRole.objects.get( - resource_id=resource.id, role='pointOfContact') - pocp = get_user_model().objects.get(id=pocr.contact_id) - content += f"Point of Contact{sc}" - content += f"name{s}{fst(pocp.last_name)}{sc}" - content += f"e-mail{s}{fst(pocp.email)}{sc}" + @staticmethod + def __append_contact_role__(content, cr_attr_name, title_in_txt): + cr = resource.__getattribute(cr_attr_name) + if cr is not None or (isinstance(list, cr) and len(0)): + content += f"{title_in_txt}{sc}" + for user in cr.contacts: + content += f"name{s}{fst(user.last_name)}{sc}" + content += f"e-mail{s}{fst(user.email)}{sc}" + return content + + content = __append_contact_role__(content, "metadata_author", "Metadata Author") + content = __append_contact_role__(content, "processor", "Processor") + content = __append_contact_role__(content, "publisher", "Publisher") + content = __append_contact_role__(content, "custodian", "Custodian") + content = __append_contact_role__(content, "poc", "Point of Contact") + content = __append_contact_role__(content, "distributor", "Distributor") + content = __append_contact_role__(content, "resource_user", "User") + content = __append_contact_role__(content, "resource_provider", "Resource Provider") + content = __append_contact_role__(content, "originator", "Originator") + content = __append_contact_role__(content, "principal_investigator", "Principal Investigator") logger = logging.getLogger(__name__) logger.error(content) @@ -323,10 +337,12 @@ def csw_render_extra_format_html(request, layeruuid, resname): s = f"{attr.attribute}{attr.attribute_label}{attr.description}" extra_res_md['atrributes'] += s - pocr = ContactRole.objects.get( - resource_id=resource.id, role='pointOfContact') - pocp = get_user_model().objects.get(id=pocr.contact_id) - extra_res_md['poc_last_name'] = pocp.last_name - extra_res_md['poc_email'] = pocp.email + for role in resource.get_multivalue_role_property_names(): + cr = resource.__getattribute__(role) + for user in cr.contacts: + extra_res_md[role][user.id] = {} + extra_res_md[role][user.id]['last_name'] = user.last_name + extra_res_md[role][user.id]['email'] = user.email + return render(request, "geonode_metadata_full.html", context={"resource": resource, "extra_res_md": extra_res_md}) diff --git a/geonode/documents/templates/documents/document_metadata.html b/geonode/documents/templates/documents/document_metadata.html index 9f27df4310b..a47df390c65 100644 --- a/geonode/documents/templates/documents/document_metadata.html +++ b/geonode/documents/templates/documents/document_metadata.html @@ -31,12 +31,12 @@

        {% trans "Metadata" %} {% blocktrans with document.t
        - {% if document_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if document_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

        {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

          - {% if author_form.errors %} + {% if metadata_author_form.errors %}
        • {% trans "Metadata Author" %}
        • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
        • {% trans "Point of Contact" %}
        • @@ -69,7 +69,7 @@

          {% trans "Point of Contact" %}

          diff --git a/geonode/documents/templates/documents/document_metadata_advanced.html b/geonode/documents/templates/documents/document_metadata_advanced.html index ddb70a929ac..f7f8dc083a9 100644 --- a/geonode/documents/templates/documents/document_metadata_advanced.html +++ b/geonode/documents/templates/documents/document_metadata_advanced.html @@ -37,12 +37,12 @@

          {% trans "Edit Metadata" %}

          - {% if document_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if document_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

          {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

            - {% if author_form.errors %} + {% if metadata_author_form.errors %}
          • {% trans "Metadata Author" %}
          • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
          • {% trans "Point of Contact" %}
          • @@ -132,7 +132,7 @@

            {% trans "Point of Contact" %}

            diff --git a/geonode/documents/views.py b/geonode/documents/views.py index d2d8b7deff3..2441a8b0537 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -342,8 +342,7 @@ def document_metadata( # Add metadata_author or poc if missing document.add_missing_metadata_author_or_poc() - poc = document.poc - metadata_author = document.metadata_author + topic_category = document.category current_keywords = [keyword.name for keyword in document.keywords.all()] @@ -405,8 +404,6 @@ def document_metadata( if request.method == "POST" and document_form.is_valid( ) and category_form.is_valid() and tkeywords_form.is_valid(): - new_poc = document_form.cleaned_data['poc'] - new_author = document_form.cleaned_data['metadata_author'] new_keywords = current_keywords if request.keyword_readonly else document_form.cleaned_data['keywords'] new_regions = document_form.cleaned_data['regions'] @@ -416,39 +413,9 @@ def document_metadata( new_category = TopicCategory.objects.get( id=int(category_form.cleaned_data['category_choice_field'])) - if new_poc is None: - if poc is None: - poc_form = ProfileForm( - request.POST, - prefix="poc", - instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.is_valid(): - if len(poc_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = poc_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set a point of contact for this resource')) - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", - instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.is_valid(): - if len(author_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = author_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set an author for this resource')) - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() + # update contact roles + document.set_contact_roles_from_metadata_edit(document_form) + document.save() document = document_form.instance resource_manager.update( @@ -457,8 +424,6 @@ def document_metadata( keywords=new_keywords, regions=new_regions, vals=dict( - poc=new_poc or document.poc, - metadata_author=new_author or document.metadata_author, category=new_category ), notify=True, @@ -524,15 +489,13 @@ def document_metadata( # - POST Request Ends here - # Request.GET - if poc is not None: - document_form.fields['poc'].initial = poc.id - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - - if metadata_author is not None: - document_form.fields['metadata_author'].initial = metadata_author.id - author_form = ProfileForm(prefix="author") - author_form.hidden = True + # define contact role forms + contact_role_forms_context = {} + for role in document.get_multivalue_role_property_names(): + document.fields[role].initial = [p.username for p in document.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) @@ -546,8 +509,6 @@ def document_metadata( "resource": document, "document": document, "document_form": document_form, - "poc_form": poc_form, - "author_form": author_form, "category_form": category_form, "tkeywords_form": tkeywords_form, "metadata_author_groups": metadata_author_groups, @@ -558,7 +519,7 @@ def document_metadata( | set(getattr(settings, 'UI_REQUIRED_FIELDS', [])) ) - }) + } | contact_role_forms_context) @login_required diff --git a/geonode/harvesting/harvesters/geonodeharvester.py b/geonode/harvesting/harvesters/geonodeharvester.py index 2e3088daa69..06589cadaba 100644 --- a/geonode/harvesting/harvesters/geonodeharvester.py +++ b/geonode/harvesting/harvesters/geonodeharvester.py @@ -380,6 +380,8 @@ def _get_resource_descriptor( # these work for both datasets and documents uuid=resource["uuid"], language=resource["language"], + + # TODO issue#10290 point_of_contact=self._get_contact_descriptor("pointOfContact", resource["poc"]), author=self._get_contact_descriptor("author", resource["metadata_author"]), date_stamp=resource_datestamp, diff --git a/geonode/layers/views.py b/geonode/layers/views.py index d5789c2a1af..b70e16eeaa0 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -442,8 +442,8 @@ def dataset_metadata( topic_category = layer.category topic_thesaurus = layer.tkeywords.all() - # Add metadata_author or poc if missing + # Add metadata_author or poc if missing layer.add_missing_metadata_author_or_poc() # assert False, str(dataset_bbox) @@ -620,7 +620,7 @@ def dataset_metadata( tkeywords_form = TKeywordForm(instance=layer) else: tkeywords_form = ThesaurusAvailableForm(prefix='tkeywords') - # set initial values for thesaurus form + # set initial values for thesaurus form for tid in tkeywords_form.fields: values = [] values = [keyword.id for keyword in topic_thesaurus if int(tid) == keyword.thesaurus.id] @@ -643,24 +643,8 @@ def dataset_metadata( la.featureinfo_type = form["featureinfo_type"] la.save() - profile_errors = [] - for role in layer.get_multivalue_role_property_names(): - new_profiles = dataset_form.cleaned_data[role] - - # if new defined profile is None|empty and required set previous profiles - if (new_profiles is None or len(new_profiles) == 0) and role in layer.get_multivalue_required_role_property_names(): - new_profiles = layer.__getattribute__(role) - - for profile in new_profiles: - role_form = ProfileForm(request.POST,prefix=role,instance=profile) - if not role_form.is_valid(): - profile_errors.append(role_form.errors) - - if len(profile_errors) == 0: - layer.__setattr__(role, new_profiles) - else: - return HttpResponse(json.dumps({'message': profile_errors}, status_code=400)) - + # update contact roles + layer.set_contact_roles_from_metadata_edit(dataset_form) layer.save() new_keywords = current_keywords if request.keyword_readonly else dataset_form.cleaned_data['keywords'] diff --git a/geonode/maps/models.py b/geonode/maps/models.py index 8d3c7040a8d..8f25cbf60d8 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -76,7 +76,7 @@ def json(self, dataset_filter): layers = [lyr for lyr in layers if dataset_filter(lyr)] # the readme text will appear in a README file in the zip - readme = f"Title: {self.title}\nAuthor: {self.poc}\nAbstract: {self.abstract}\n" + readme = f"Title: {self.title}\nAuthor: {self.poc_csv()}\nAbstract: {self.abstract}\n" if self.license: readme += f"License: {self.license}" if self.license.url: diff --git a/geonode/maps/templates/maps/map_metadata.html b/geonode/maps/templates/maps/map_metadata.html index 2a3969e5c2a..30ad5b215ad 100644 --- a/geonode/maps/templates/maps/map_metadata.html +++ b/geonode/maps/templates/maps/map_metadata.html @@ -70,7 +70,7 @@

            {% trans "Point of Contact" %}

            diff --git a/geonode/maps/templates/maps/map_metadata_advanced.html b/geonode/maps/templates/maps/map_metadata_advanced.html index 6b3bbc1d8dd..81d0a4b59c3 100644 --- a/geonode/maps/templates/maps/map_metadata_advanced.html +++ b/geonode/maps/templates/maps/map_metadata_advanced.html @@ -132,7 +132,7 @@

            {% trans "Point of Contact" %}

            diff --git a/geonode/maps/views.py b/geonode/maps/views.py index 9246d7c36f3..ec416d4f00a 100644 --- a/geonode/maps/views.py +++ b/geonode/maps/views.py @@ -94,10 +94,9 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): # Add metadata_author or poc if missing map_obj.add_missing_metadata_author_or_poc() + current_keywords = [keyword.name for keyword in map_obj.keywords.all()] - poc = map_obj.poc topic_thesaurus = map_obj.tkeywords.all() - metadata_author = map_obj.metadata_author topic_category = map_obj.category @@ -157,8 +156,6 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): if request.method == "POST" and map_form.is_valid( ) and category_form.is_valid() and tkeywords_form.is_valid(): - new_poc = map_form.cleaned_data['poc'] - new_author = map_form.cleaned_data['metadata_author'] new_keywords = current_keywords if request.keyword_readonly else map_form.cleaned_data['keywords'] new_regions = map_form.cleaned_data['regions'] new_title = map_form.cleaned_data['title'] @@ -170,29 +167,13 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): new_category = TopicCategory.objects.get( id=int(category_form.cleaned_data['category_choice_field'])) - if new_poc is None: - if poc is None: - poc_form = ProfileForm( - request.POST, - prefix="poc", - instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", - instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() + # update contact roles + map_obj.store_contact_roles_from_metadata_edit(request) + map_obj.save() + + map_obj.set_contact_roles_from_metadata_edit(map_form) + map_obj.save() - if new_poc is not None and new_author is not None: - map_obj.poc = new_poc - map_obj.metadata_author = new_author map_obj.title = new_title map_obj.abstract = new_abstract map_obj.keywords.clear() @@ -267,22 +248,15 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): # - POST Request Ends here - # Request.GET - if poc is None: - poc_form = ProfileForm(request.POST, prefix="poc") - else: - map_form.fields['poc'].initial = poc.id - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author") - else: - map_form.fields['metadata_author'].initial = metadata_author.id - author_form = ProfileForm(prefix="author") - author_form.hidden = True + # define contact role forms + contact_role_forms_context = {} + for role in map_obj.get_multivalue_role_property_names(): + map_form.fields[role].initial = [p.username for p in map_obj.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form layers = MapLayer.objects.filter(map=map_obj.id) - metadata_author_groups = get_user_visible_groups(request.user) if not AdvancedSecurityWorkflowManager.is_allowed_to_publish(request.user, map_obj): @@ -296,8 +270,6 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): "map": map_obj, "config": json.dumps(map_obj.blob), "map_form": map_form, - "poc_form": poc_form, - "author_form": author_form, "category_form": category_form, "tkeywords_form": tkeywords_form, "layers": layers, @@ -311,7 +283,7 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): | set(getattr(settings, 'UI_REQUIRED_FIELDS', [])) ) - }) + } | contact_role_forms_context) @login_required diff --git a/geonode/templates/metadata_detail.html b/geonode/templates/metadata_detail.html index 42cbfe2d6d5..0a9205c2952 100644 --- a/geonode/templates/metadata_detail.html +++ b/geonode/templates/metadata_detail.html @@ -360,58 +360,54 @@

            {% trans "Metadata" %} : {{ resource.title }}

            {% if resource.poc %} {% trans "Contact Points" %}

            - + {% for poc in resource.poc%}
            - {% block poc_name_long %}
            {% trans "Name" %}
            -
            {{ resource.poc.name_long }}
            +
            {{ poc.name_long }}
            {% endblock poc_name_long %} {% block poc_email %}
            {% trans "email" %}
            -
            {{ resource.poc.email }}
            +
            {{ poc.email }}
            {% endblock poc_email %} {% block poc_position %}
            {% trans "Position" %}
            -
            {{ resource.poc.position }}
            +
            {{ poc.position }}
            {% endblock poc_position %} {% block poc_organization %}
            {% trans "Organization" %}
            -
            {{ resource.poc.organization }}
            +
            {{ poc.organization }}
            {% endblock poc_organization %} {% block poc_location %}
            {% trans "Location" %}
            -
            {{ resource.poc.location }}
            +
            {{ poc.location }}
            {% endblock poc_location %} {% block poc_voice %}
            {% trans "Voice" %}
            -
            {{ resource.poc.voice }}
            +
            {{ poc.voice }}
            {% endblock poc_voice %} {% block poc_fax %}
            {% trans "Fax" %}
            -
            {{ resource.poc.fax }}
            +
            {{ poc.fax }}
            {% endblock poc_fax %} {% block poc_keyword %} {% if poc.keyword_list %}
            {% trans "Keywords" %}
            -
            {% for keyword in resource.poc.keyword_list %} +
            {% for keyword in poc.keyword_list %} {{ keyword }} {% endfor %}
            {% endif %} {% endblock poc_keyword %}
            -
            - - - + {% endfor %} {% endif %} {% trans "References" %} @@ -435,19 +431,14 @@

            {% trans "Metadata" %} : {{ resource.title }}

            {{ resource.get_absolute_url }}/download
            {% endif %} {% endblock doc_file %} -
            {% if "download_resourcebase" in perms_list %} - - {% for link in resource.link_set.download %}
            {{link.name}}
            {{resource.title}}.{{link.extension}}
            {% endfor %} -
            - {% endif %} {% for link in resource.link_set.ows %} @@ -457,64 +448,504 @@

            {% trans "Metadata" %} : {{ resource.title }}

            + {% comment %} Contact Role: Metadata Author {% endcomment %} {% if resource.metadata_author %} + {% trans "Metadata Author" %}

            + {% for metadata_author in resource.metadata_author %}
            + {% block metadata_author_doc_file %} +
            {% trans "Name" %}
            +
            {{ metadata_author.name_long }}
            + {% endblock metadata_author_doc_file %} + + {% block metadata_author_email %} +
            {% trans "email" %}
            +
            {{ metadata_author.email }}
            + {% endblock metadata_author_email %} + + {% block metadata_author_position %} +
            {% trans "Position" %}
            +
            {{ metadata_author.position }}
            + {% endblock metadata_author_position %} + + {% block metadata_author_organization %} +
            {% trans "Organization" %}
            +
            {{ metadata_author.organization }}
            + {% endblock metadata_author_organization %} + + {% block metadata_author_location %} +
            {% trans "Location" %}
            +
            {{ metadata_author.location }}
            + {% endblock metadata_author_location %} + + {% block metadata_author_voice %} +
            {% trans "Voice" %}
            +
            {{ metadata_author.voice }}
            + {% endblock metadata_author_voice %} + + {% block metadata_author_fax %} +
            {% trans "Fax" %}
            +
            {{ metadata_author.fax }}
            + {% endblock metadata_author_fax %} + + {% block metadata_author_keyword_list %} + {% if metadata_author.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in metadata_author.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock metadata_author_keyword_list %} +
            + {% endfor%} + {% endif %} - {% with resource.metadata_author as poc %} - - {% block metadata_author_doc_file %} -
            {% trans "Name" %}
            -
            {{ poc.name_long }}
            - {% endblock metadata_author_doc_file %} - - {% block metadata_author_email %} -
            {% trans "email" %}
            -
            {{ poc.email }}
            - {% endblock metadata_author_email %} - - {% block metadata_author_position %} -
            {% trans "Position" %}
            -
            {{ poc.position }}
            - {% endblock metadata_author_position %} - {% block metadata_author_organization %} -
            {% trans "Organization" %}
            -
            {{ poc.organization }}
            - {% endblock metadata_author_organization %} + {% comment %} Contact Role: Processor {% endcomment %} + {% if resource.processor %} - {% block metadata_author_location %} -
            {% trans "Location" %}
            -
            {{ poc.location }}
            - {% endblock metadata_author_location %} + {% trans "Processor" %} +

            - {% block metadata_author_voice %} -
            {% trans "Voice" %}
            -
            {{ poc.voice }}
            - {% endblock metadata_author_voice %} + {% for processor in resource.processor %} +
            + {% block processor_doc_file %} +
            {% trans "Name" %}
            +
            {{ processor.name_long }}
            + {% endblock processor_doc_file %} + + {% block processor_email %} +
            {% trans "email" %}
            +
            {{ processor.email }}
            + {% endblock processor_email %} + + {% block processor_position %} +
            {% trans "Position" %}
            +
            {{ processor.position }}
            + {% endblock processor_position %} + + {% block processor_organization %} +
            {% trans "Organization" %}
            +
            {{ processor.organization }}
            + {% endblock processor_organization %} + + {% block processor_location %} +
            {% trans "Location" %}
            +
            {{ processor.location }}
            + {% endblock processor_location %} + + {% block processor_voice %} +
            {% trans "Voice" %}
            +
            {{ processor.voice }}
            + {% endblock processor_voice %} + + {% block processor_fax %} +
            {% trans "Fax" %}
            +
            {{ processor.fax }}
            + {% endblock processor_fax %} + + {% block processor_keyword_list %} + {% if processor.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in processor.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock processor_keyword_list %} +
            + {% endfor%} + {% endif %} - {% block metadata_author_fax %} -
            {% trans "Fax" %}
            -
            {{ poc.fax }}
            - {% endblock metadata_author_fax %} + {% comment %} Contact Role: Publisher {% endcomment %} + {% if resource.publisher %} - {% block metadata_author_keyword_list %} - {% if poc.keyword_list %} -
            {% trans "Keywords" %}
            -
            {% for keyword in poc.keyword_list %} - {{ keyword }} - {% endfor %}
            - {% endif %} - {% endblock metadata_author_keyword_list %} - {% endwith %} + {% trans "Publisher" %} +

            + {% for publisher in resource.publisher %} +
            + {% block publisher_doc_file %} +
            {% trans "Name" %}
            +
            {{ publisher.name_long }}
            + {% endblock publisher_doc_file %} + + {% block publisher_email %} +
            {% trans "email" %}
            +
            {{ publisher.email }}
            + {% endblock publisher_email %} + + {% block publisher_position %} +
            {% trans "Position" %}
            +
            {{ publisher.position }}
            + {% endblock publisher_position %} + + {% block publisher_organization %} +
            {% trans "Organization" %}
            +
            {{ publisher.organization }}
            + {% endblock publisher_organization %} + + {% block publisher_location %} +
            {% trans "Location" %}
            +
            {{ publisher.location }}
            + {% endblock publisher_location %} + + {% block publisher_voice %} +
            {% trans "Voice" %}
            +
            {{ publisher.voice }}
            + {% endblock publisher_voice %} + + {% block publisher_fax %} +
            {% trans "Fax" %}
            +
            {{ publisher.fax }}
            + {% endblock publisher_fax %} + + {% block publisher_keyword_list %} + {% if publisher.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in publisher.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock publisher_keyword_list %}
            + {% endfor%} {% endif %} + {% comment %} Contact Role: Custodian {% endcomment %} + {% if resource.custodian %} + + {% trans "Custodian" %} +

            + + {% for custodian in resource.custodian %} +
            + {% block custodian_doc_file %} +
            {% trans "Name" %}
            +
            {{ custodian.name_long }}
            + {% endblock custodian_doc_file %} + + {% block custodian_email %} +
            {% trans "email" %}
            +
            {{ custodian.email }}
            + {% endblock custodian_email %} + + {% block custodian_position %} +
            {% trans "Position" %}
            +
            {{ custodian.position }}
            + {% endblock custodian_position %} + + {% block custodian_organization %} +
            {% trans "Organization" %}
            +
            {{ custodian.organization }}
            + {% endblock custodian_organization %} + + {% block custodian_location %} +
            {% trans "Location" %}
            +
            {{ custodian.location }}
            + {% endblock custodian_location %} + + {% block custodian_voice %} +
            {% trans "Voice" %}
            +
            {{ custodian.voice }}
            + {% endblock custodian_voice %} + + {% block custodian_fax %} +
            {% trans "Fax" %}
            +
            {{ custodian.fax }}
            + {% endblock custodian_fax %} + + {% block custodian_keyword_list %} + {% if custodian.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in custodian.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock custodian_keyword_list %} +
            + {% endfor%} + {% endif %} + + {% comment %} Contact Role: Distributor {% endcomment %} + {% if resource.distributor %} + + {% trans "Distributor" %} +

            + + {% for distributor in resource.distributor %} +
            + {% block distributor_doc_file %} +
            {% trans "Name" %}
            +
            {{ distributor.name_long }}
            + {% endblock distributor_doc_file %} + + {% block distributor_email %} +
            {% trans "email" %}
            +
            {{ distributor.email }}
            + {% endblock distributor_email %} + + {% block distributor_position %} +
            {% trans "Position" %}
            +
            {{ distributor.position }}
            + {% endblock distributor_position %} + + {% block distributor_organization %} +
            {% trans "Organization" %}
            +
            {{ distributor.organization }}
            + {% endblock distributor_organization %} + + {% block distributor_location %} +
            {% trans "Location" %}
            +
            {{ distributor.location }}
            + {% endblock distributor_location %} + + {% block distributor_voice %} +
            {% trans "Voice" %}
            +
            {{ distributor.voice }}
            + {% endblock distributor_voice %} + + {% block distributor_fax %} +
            {% trans "Fax" %}
            +
            {{ distributor.fax }}
            + {% endblock distributor_fax %} + + {% block distributor_keyword_list %} + {% if distributor.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in distributor.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock distributor_keyword_list %} +
            + {% endfor%} + {% endif %} + + + {% comment %} Contact Role: User {% endcomment %} + {% if resource.resource_user %} + + {% trans "User" %} +

            + + {% for resource_user in resource.resource_user %} +
            + {% block resource_user_doc_file %} +
            {% trans "Name" %}
            +
            {{ resource_user.name_long }}
            + {% endblock resource_user_doc_file %} + + {% block resource_user_email %} +
            {% trans "email" %}
            +
            {{ resource_user.email }}
            + {% endblock resource_user_email %} + + {% block resource_user_position %} +
            {% trans "Position" %}
            +
            {{ resource_user.position }}
            + {% endblock resource_user_position %} + + {% block resource_user_organization %} +
            {% trans "Organization" %}
            +
            {{ resource_user.organization }}
            + {% endblock resource_user_organization %} + + {% block resource_user_location %} +
            {% trans "Location" %}
            +
            {{ resource_user.location }}
            + {% endblock resource_user_location %} + + {% block resource_user_voice %} +
            {% trans "Voice" %}
            +
            {{ resource_user.voice }}
            + {% endblock resource_user_voice %} + + {% block resource_user_fax %} +
            {% trans "Fax" %}
            +
            {{ resource_user.fax }}
            + {% endblock resource_user_fax %} + + {% block resource_user_keyword_list %} + {% if resource_user.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in resource_user.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock resource_user_keyword_list %} +
            + {% endfor%} + {% endif %} + + {% comment %} Contact Role: Resource Provider {% endcomment %} + {% if resource.resource_provider %} + + {% trans "Resource Provider" %} +

            + + {% for resource_provider in resource.resource_provider %} +
            + {% block resource_provider_doc_file %} +
            {% trans "Name" %}
            +
            {{ resource_provider.name_long }}
            + {% endblock resource_provider_doc_file %} + + {% block resource_provider_email %} +
            {% trans "email" %}
            +
            {{ resource_provider.email }}
            + {% endblock resource_provider_email %} + + {% block resource_provider_position %} +
            {% trans "Position" %}
            +
            {{ resource_provider.position }}
            + {% endblock resource_provider_position %} + + {% block resource_provider_organization %} +
            {% trans "Organization" %}
            +
            {{ resource_provider.organization }}
            + {% endblock resource_provider_organization %} + + {% block resource_provider_location %} +
            {% trans "Location" %}
            +
            {{ resource_provider.location }}
            + {% endblock resource_provider_location %} + + {% block resource_provider_voice %} +
            {% trans "Voice" %}
            +
            {{ resource_provider.voice }}
            + {% endblock resource_provider_voice %} + + {% block resource_provider_fax %} +
            {% trans "Fax" %}
            +
            {{ resource_provider.fax }}
            + {% endblock resource_provider_fax %} + + {% block resource_provider_keyword_list %} + {% if resource_provider.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in resource_provider.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock resource_provider_keyword_list %} +
            + {% endfor%} + {% endif %} + + + {% comment %} Contact Role: Originator {% endcomment %} + {% if resource.originator %} + + {% trans "Resource Provider" %} +

            + + {% for originator in resource.originator %} +
            + {% block originator_doc_file %} +
            {% trans "Name" %}
            +
            {{ originator.name_long }}
            + {% endblock originator_doc_file %} + + {% block originator_email %} +
            {% trans "email" %}
            +
            {{ originator.email }}
            + {% endblock originator_email %} + + {% block originator_position %} +
            {% trans "Position" %}
            +
            {{ originator.position }}
            + {% endblock originator_position %} + + {% block originator_organization %} +
            {% trans "Organization" %}
            +
            {{ originator.organization }}
            + {% endblock originator_organization %} + + {% block originator_location %} +
            {% trans "Location" %}
            +
            {{ originator.location }}
            + {% endblock originator_location %} + + {% block originator_voice %} +
            {% trans "Voice" %}
            +
            {{ originator.voice }}
            + {% endblock originator_voice %} + + {% block originator_fax %} +
            {% trans "Fax" %}
            +
            {{ originator.fax }}
            + {% endblock originator_fax %} + + {% block originator_keyword_list %} + {% if originator.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in originator.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock originator_keyword_list %} +
            + {% endfor%} + {% endif %} + + + {% comment %} Contact Role: Resource Provider {% endcomment %} + {% if resource.principal_investigator %} + + {% trans "Principal Investigator" %} +

            + + {% for principal_investigator in resource.principal_investigator %} +
            + {% block principal_investigator_doc_file %} +
            {% trans "Name" %}
            +
            {{ principal_investigator.name_long }}
            + {% endblock principal_investigator_doc_file %} + + {% block principal_investigator_email %} +
            {% trans "email" %}
            +
            {{ principal_investigator.email }}
            + {% endblock principal_investigator_email %} + + {% block principal_investigator_position %} +
            {% trans "Position" %}
            +
            {{ principal_investigator.position }}
            + {% endblock principal_investigator_position %} + + {% block principal_investigator_organization %} +
            {% trans "Organization" %}
            +
            {{ principal_investigator.organization }}
            + {% endblock principal_investigator_organization %} + + {% block principal_investigator_location %} +
            {% trans "Location" %}
            +
            {{ principal_investigator.location }}
            + {% endblock principal_investigator_location %} + + {% block principal_investigator_voice %} +
            {% trans "Voice" %}
            +
            {{ principal_investigator.voice }}
            + {% endblock principal_investigator_voice %} + + {% block principal_investigator_fax %} +
            {% trans "Fax" %}
            +
            {{ principal_investigator.fax }}
            + {% endblock principal_investigator_fax %} + + {% block principal_investigator_keyword_list %} + {% if principal_investigator.keyword_list %} +
            {% trans "Keywords" %}
            +
            {% for keyword in principal_investigator.keyword_list %} + {{ keyword }} + {% endfor %}
            + {% endif %} + {% endblock principal_investigator_keyword_list %} +
            + {% endfor%} + {% endif %} - - {% endblock %} From c8c669f353cf8deb673b3eeb5032a0057a8c398f Mon Sep 17 00:00:00 2001 From: Malte Iwanicki Date: Thu, 8 Dec 2022 15:40:25 +0100 Subject: [PATCH 05/49] fixed typo --- geonode/base/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index b58ae4e610b..b026481d703 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -482,7 +482,7 @@ class ResourceBaseForm(TranslationModelForm): widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) poc = ContactRoleMultipleChoiceField( - label=_("Person of Contact"), + label=_("Point of Contact"), required=True, queryset=get_user_model().objects.exclude( username='AnonymousUser'), From 99922dd89d716dec2d9ac52e428114de322a7408 Mon Sep 17 00:00:00 2001 From: Malte Iwanicki Date: Thu, 15 Dec 2022 00:01:49 +0100 Subject: [PATCH 06/49] created a centralized enum with the roles. added contacts to geonode_metadata_full.html. refactored --- geonode/base/api/serializers.py | 6 +- geonode/base/forms.py | 25 ++++---- geonode/base/models.py | 58 +++++++++---------- .../templates/geonode_metadata_full.html | 13 +++-- geonode/catalogue/views.py | 39 +++++-------- geonode/people/__init__.py | 54 ++++++++++++++++- 6 files changed, 119 insertions(+), 76 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 9dabfbbfa99..f64627c5df0 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -360,12 +360,12 @@ def to_representation(self, instance): class ContactRoleField(DynamicComputedField): - def __init__(self, contat_type, **kwargs): - self.contat_type = contat_type + def __init__(self, contact_type, **kwargs): + self.contact_type = contact_type super().__init__(**kwargs) def get_attribute(self, instance): - return getattr(instance, self.contat_type) + return getattr(instance, self.contact_type) def to_representation(self, value): return [UserSerializer(embed=True, many=False).to_representation(v) for v in value] diff --git a/geonode/base/forms.py b/geonode/base/forms.py index b026481d703..2fa7250d21d 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -52,6 +52,7 @@ from geonode.documents.models import Document from geonode.layers.models import Dataset from geonode.base.utils import validate_extra_metadata, remove_country_from_languagecode +from geonode.people import Roles logger = logging.getLogger(__name__) @@ -424,8 +425,8 @@ class ResourceBaseForm(TranslationModelForm): widget=TinyMCE()) owner = forms.ModelChoiceField( - empty_label=_("Owner"), - label=_("Owner"), + empty_label=_(Roles.OWNER.label), + label=_(Roles.OWNER.label), required=True, queryset=get_user_model().objects.exclude(username='AnonymousUser'), widget=autocomplete.ModelSelect2(url='autocomplete_profile')) @@ -454,70 +455,70 @@ class ResourceBaseForm(TranslationModelForm): ) metadata_author = ContactRoleMultipleChoiceField( - label=_("Metadata Author"), + label=_(Roles.METADATA_AUTHOR.label), required=True, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) processor = ContactRoleMultipleChoiceField( - label=_("Processor"), + label=_(Roles.PROCESSOR.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) publisher = ContactRoleMultipleChoiceField( - label=_("Publisher"), + label=_(Roles.PUBLISHER.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) custodian = ContactRoleMultipleChoiceField( - label=_("Custodian"), + label=_(Roles.CUSTODIAN.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) poc = ContactRoleMultipleChoiceField( - label=_("Point of Contact"), + label=_(Roles.POC.label), required=True, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) distributor = ContactRoleMultipleChoiceField( - label=_("Distributor"), + label=_(Roles.DISTRIBUTOR.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) resource_user = ContactRoleMultipleChoiceField( - label=_("Resource User"), + label=_(Roles.RESOURCE_USER.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) resource_provider = ContactRoleMultipleChoiceField( - label=_("Resource Provider"), + label=_(Roles.RESOURCE_PROVIDER.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) originator = ContactRoleMultipleChoiceField( - label=_('Originator'), + label=_(Roles.ORIGINATOR.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) principal_investigator = ContactRoleMultipleChoiceField( - label=_('Principal Investigator'), + label=_(Roles.PRINCIPAL_INVESTIGATOR.label), required=False, queryset=get_user_model().objects.exclude( username='AnonymousUser'), diff --git a/geonode/base/models.py b/geonode/base/models.py index b70ccde3933..d868fe13c58 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -88,6 +88,7 @@ from geonode.notifications_helper import ( send_notification, get_notification_recipients) +from geonode.people import Roles from geonode.people.enumerations import ROLE_VALUES from urllib.parse import urlsplit, urljoin @@ -1824,18 +1825,7 @@ def get_multivalue_role_property_names() -> List[str]: _type_: List(str) _description: list of names """ - return [ - 'metadata_author', - 'processor', - 'publisher', - 'custodian', - 'poc', - 'distributor', - 'resource_user', - 'resource_provider', - 'originator', - 'principal_investigator' - ] + return [role.name for role in Roles.get_multivalue_ones()] @staticmethod def get_multivalue_required_role_property_names() -> List[str]: @@ -1845,10 +1835,14 @@ def get_multivalue_required_role_property_names() -> List[str]: _type_: List(str) _description: list of names """ - return [ - 'metadata_author', - 'poc', - ] + return ( + [ + role.name + for role in ( + set(Roles.get_multivalue_ones()) & set(Roles.get_required_ones()) + ) + ] + ) # from geonode.base.forms import ResourceBaseForm; unable due to circular ... def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: @@ -1917,50 +1911,50 @@ def _set_metadata_author(self, user_profile): return self._set_contact_role_elem @property def metadata_author_csv(self): return ','.join(p.get_full_name() or p.username for p in self.metadata_author) - def _get_processor(self): return self._get_contact_role_elements(role="processor") - def _set_processor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="processor") + def _get_processor(self): return self._get_contact_role_elements(role=Roles.PROCESSOR.name) + def _set_processor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PROCESSOR.name) processor = property(_get_processor, _set_processor) @property def processor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.processor) - def _get_publisher(self): return self._get_contact_role_elements(role="publisher") - def _set_publisher(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="publisher") + def _get_publisher(self): return self._get_contact_role_elements(role=Roles.PUBLISHER.name) + def _set_publisher(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PUBLISHER.name) publisher = property(_get_publisher, _set_publisher) @property def publisher_csv(self): return ','.join(p.get_full_name() or p.username for p in self.publisher) - def _get_custodian(self): return self._get_contact_role_elements(role="custodian") - def _set_custodian(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="custodian") + def _get_custodian(self): return self._get_contact_role_elements(role=Roles.CUSTODIAN.name) + def _set_custodian(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.CUSTODIAN.name) custodian = property(_get_custodian, _set_custodian) @property def custodian_csv(self): return ','.join(p.get_full_name() or p.username for p in self.custodian) - def _get_distributor(self): return self._get_contact_role_elements(role="distributor") - def _set_distributor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="distributor") + def _get_distributor(self): return self._get_contact_role_elements(role=Roles.DISTRIBUTOR.name) + def _set_distributor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.DISTRIBUTOR.name) distributor = property(_get_distributor, _set_distributor) @property def distributor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.distributor) - def _get_resource_user(self): return self._get_contact_role_elements(role="resource_user") - def _set_resource_user(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="resource_user") + def _get_resource_user(self): return self._get_contact_role_elements(role=Roles.RESOURCE_USER.name) + def _set_resource_user(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.RESOURCE_USER.name) resource_user = property(_get_resource_user, _set_resource_user) @property def resource_user_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_user) - def _get_resource_provider(self): return self._get_contact_role_elements(role="resource_provider") - def _set_resource_provider(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="resource_provider") + def _get_resource_provider(self): return self._get_contact_role_elements(role=Roles.RESOURCE_PROVIDER.name) + def _set_resource_provider(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.RESOURCE_PROVIDER.name) resource_provider = property(_get_resource_provider, _set_resource_provider) @property def resource_provider_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_provider) - def _get_originator(self): return self._get_contact_role_elements(role="originator") - def _set_originator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="originator") + def _get_originator(self): return self._get_contact_role_elements(role=Roles.ORIGINATOR.name) + def _set_originator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.ORIGINATOR.name) originator = property(_get_originator, _set_originator) @property def originator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.originator) - def _get_principal_investigator(self): return self._get_contact_role_elements(role="principal_investigator") - def _set_principal_investigator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="principal_investigator") + def _get_principal_investigator(self): return self._get_contact_role_elements(role=Roles.PRINCIPAL_INVESTIGATOR.name) + def _set_principal_investigator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PRINCIPAL_INVESTIGATOR.name) principal_investigator = property(_get_principal_investigator, _set_principal_investigator) @property def principal_investigator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.principal_investigator) diff --git a/geonode/catalogue/templates/geonode_metadata_full.html b/geonode/catalogue/templates/geonode_metadata_full.html index a5cd9277ce1..47413188a32 100644 --- a/geonode/catalogue/templates/geonode_metadata_full.html +++ b/geonode/catalogue/templates/geonode_metadata_full.html @@ -72,11 +72,14 @@

            {{ resource.title }}

            {% trans "Responsible" %}
            {{resource.owner}}
            -
            {% trans "Point of Contact" %}
            - {% for poc in extra_res_md.poc %} -
            {{poc.last_name}}
            -
            {{ poc.email}}
            - {% endfor %} + {% for role in extra_res_md.roles %} +
            {% trans role.label %}
            + + {% for user in role.users %} +
            {{user.last_name}}
            +
            {{ user.email}}
            + {% endfor %} + {% endfor %}
            {% trans "Purpose" %}
            {% if resource.purpose %} diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index bc91ac9b709..a17ac2541f2 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -33,7 +33,7 @@ from geonode.groups.models import GroupProfile from django.db import connection from django.core.exceptions import ObjectDoesNotExist - +from geonode.people import Roles @csrf_exempt def csw_global_dispatch(request, dataset_filter=None, config_updater=None): @@ -290,24 +290,16 @@ def csw_render_extra_format_txt(request, layeruuid, resname): @staticmethod def __append_contact_role__(content, cr_attr_name, title_in_txt): - cr = resource.__getattribute(cr_attr_name) + cr = resource.__getattribute__(cr_attr_name) if cr is not None or (isinstance(list, cr) and len(0)): content += f"{title_in_txt}{sc}" - for user in cr.contacts: + for user in cr: content += f"name{s}{fst(user.last_name)}{sc}" content += f"e-mail{s}{fst(user.email)}{sc}" return content - - content = __append_contact_role__(content, "metadata_author", "Metadata Author") - content = __append_contact_role__(content, "processor", "Processor") - content = __append_contact_role__(content, "publisher", "Publisher") - content = __append_contact_role__(content, "custodian", "Custodian") - content = __append_contact_role__(content, "poc", "Point of Contact") - content = __append_contact_role__(content, "distributor", "Distributor") - content = __append_contact_role__(content, "resource_user", "User") - content = __append_contact_role__(content, "resource_provider", "Resource Provider") - content = __append_contact_role__(content, "originator", "Originator") - content = __append_contact_role__(content, "principal_investigator", "Principal Investigator") + + for role in set(Roles).difference([Roles.OWNER]): + content = __append_contact_role__(content, role.name, role.label) logger = logging.getLogger(__name__) logger.error(content) @@ -337,12 +329,13 @@ def csw_render_extra_format_html(request, layeruuid, resname): s = f"{attr.attribute}{attr.attribute_label}{attr.description}" extra_res_md['atrributes'] += s - for role in resource.get_multivalue_role_property_names(): - cr = resource.__getattribute__(role) - for user in cr.contacts: - extra_res_md[role][user.id] = {} - extra_res_md[role][user.id]['last_name'] = user.last_name - extra_res_md[role][user.id]['email'] = user.email - - return render(request, "geonode_metadata_full.html", context={"resource": resource, - "extra_res_md": extra_res_md}) + extra_res_md["roles"] = [] + for role in Roles: + cr = resource.__getattribute__(role.name) + if not type(cr)==list: + cr = [cr] + users=[{"pk":user.id, 'last_name': user.last_name, 'email': user.email} for user in cr] + if users: + extra_res_md["roles"].append({"label":role.label, "users":users}) + + return render(request, "geonode_metadata_full.html", context={"resource": resource, "extra_res_md": extra_res_md}) diff --git a/geonode/people/__init__.py b/geonode/people/__init__.py index 0ee47361982..852ef1a6f2e 100644 --- a/geonode/people/__init__.py +++ b/geonode/people/__init__.py @@ -18,7 +18,7 @@ ######################################################################### from django.utils.translation import ugettext_noop as _ from geonode.notifications_helper import NotificationsAppConfigBase - +import enum class PeopleAppConfig(NotificationsAppConfigBase): name = 'geonode.people' @@ -34,3 +34,55 @@ def ready(self): default_app_config = 'geonode.people.PeopleAppConfig' + +class Role: + def __init__(self, label, is_required, is_multivalue): + self.label = label + self.is_required = is_required + self.is_multivalue = is_multivalue + + def __repr__(self): + return self.label + + +class Roles(enum.Enum): + """Roles with their `label`, `is_required`, and `is_multivalue`""" + + OWNER = Role("Owner", True, False) + METADATA_AUTHOR = Role("Metadata Author", True, True) + PROCESSOR = Role("Processor", False, True) + PUBLISHER = Role("Publisher", False, True) + CUSTODIAN = Role("Custodian", False, True) + POC = Role("Point of Contact", True, True) + DISTRIBUTOR = Role("Distributor", False, True) + RESOURCE_USER = Role("Resource User", False, True) + RESOURCE_PROVIDER = Role("Resource Provider", False, True) + ORIGINATOR = Role("Originator", False, True) + PRINCIPAL_INVESTIGATOR = Role("Principal Investigator", False, True) + + @property + def name(self): + return super().name.lower() + + @property + def label(self): + return self.value.label + + @property + def is_required(self): + return self.value.is_required + + @property + def is_multivalue(self): + return self.value.is_multivalue + + def __repr__(self): + return self.name + + @classmethod + def get_required_ones(cls): + return [role for role in cls if role.is_required] + + @classmethod + def get_multivalue_ones(cls): + return [role for role in cls if role.is_multivalue] \ No newline at end of file From dadea2bb72a6d39acf5fd8cc63a60b827239074c Mon Sep 17 00:00:00 2001 From: Malte Iwanicki Date: Tue, 20 Dec 2022 12:32:02 +0100 Subject: [PATCH 07/49] multiple poc are displayed in _resourcebase_info_panel --- geonode/base/templates/base/_resourcebase_info_panel.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geonode/base/templates/base/_resourcebase_info_panel.html b/geonode/base/templates/base/_resourcebase_info_panel.html index 4696ac98a90..4d9c5a80a04 100644 --- a/geonode/base/templates/base/_resourcebase_info_panel.html +++ b/geonode/base/templates/base/_resourcebase_info_panel.html @@ -108,10 +108,10 @@
            {% endif %} - {% if resource.poc.user %} - {% for poc in resource.poc %} + {% if resource.poc %}
            {% trans "Point of Contact" %}
            -
            {{ poc.user.username }}
            + {% for user in resource.poc %} +
            {{ user.username }}
            {% endfor%} {% endif %} From d0605990cffd84165c1682fc428fc24dfd5f4c8c Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 12 Jan 2023 11:26:32 +0100 Subject: [PATCH 08/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/api/serializers.py | 21 +++++++++++---------- geonode/base/forms.py | 20 ++++++++++---------- geonode/base/models.py | 13 ++++++++++++- geonode/people/__init__.py | 2 +- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index f64627c5df0..dc98ab30fa5 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -26,6 +26,7 @@ from django.forms.models import model_to_dict from django.contrib.auth import get_user_model from django.db.models.query import QuerySet +from geonode.people import Roles from rest_framework import serializers from rest_framework_gis import fields @@ -448,16 +449,16 @@ def __init__(self, *args, **kwargs): self.fields['resource_type'] = serializers.CharField(required=False) self.fields['polymorphic_ctype_id'] = serializers.CharField(read_only=True) self.fields['owner'] = DynamicRelationField(UserSerializer, embed=True, many=False, read_only=True) - self.fields['metadata_author'] = ContactRoleField('metadata_author', required=False) - self.fields['processor'] = ContactRoleField('processor', required=False) - self.fields['publisher'] = ContactRoleField('publisher', required=False) - self.fields['custodian'] = ContactRoleField('custodian', required=False) - self.fields['poc'] = ContactRoleField('poc', required=False) - self.fields['distributor'] = ContactRoleField('distributor', required=False) - self.fields['resource_user'] = ContactRoleField('resource_user', required=False) - self.fields['resource_provider'] = ContactRoleField('resource_provider', required=False) - self.fields['originator'] = ContactRoleField('originator', required=False) - self.fields['principal_investigator'] = ContactRoleField('principal_investigator', required=False) + self.fields['metadata_author'] = ContactRoleField(Roles.METADATA_AUTHOR.name, required=False) + self.fields['processor'] = ContactRoleField(Roles.PROCESSOR.name, required=False) + self.fields['publisher'] = ContactRoleField(Roles.PUBLISHER.name, required=False) + self.fields['custodian'] = ContactRoleField(Roles.CUSTODIAN.name, required=False) + self.fields['poc'] = ContactRoleField(Roles.POC.name, required=False) + self.fields['distributor'] = ContactRoleField(Roles.DISTRIBUTOR.name, required=False) + self.fields['resource_user'] = ContactRoleField(Roles.RESOURCE_USER.name, required=False) + self.fields['resource_provider'] = ContactRoleField(Roles.RESOURCE_PROVIDER.name, required=False) + self.fields['originator'] = ContactRoleField(Roles.ORIGINATOR.name, required=False) + self.fields['principal_investigator'] = ContactRoleField(Roles.PRINCIPAL_INVESTIGATOR.name, required=False) self.fields['title'] = serializers.CharField() self.fields['abstract'] = serializers.CharField(required=False) self.fields['attribution'] = serializers.CharField(required=False) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 2fa7250d21d..358089484a1 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -456,70 +456,70 @@ class ResourceBaseForm(TranslationModelForm): metadata_author = ContactRoleMultipleChoiceField( label=_(Roles.METADATA_AUTHOR.label), - required=True, + required=Roles.METADATA_AUTHOR.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) processor = ContactRoleMultipleChoiceField( label=_(Roles.PROCESSOR.label), - required=False, + required=Roles.PROCESSOR.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) publisher = ContactRoleMultipleChoiceField( label=_(Roles.PUBLISHER.label), - required=False, + required=Roles.PUBLISHER.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) custodian = ContactRoleMultipleChoiceField( label=_(Roles.CUSTODIAN.label), - required=False, + required=Roles.CUSTODIAN.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) poc = ContactRoleMultipleChoiceField( label=_(Roles.POC.label), - required=True, + required=Roles.POC.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) distributor = ContactRoleMultipleChoiceField( label=_(Roles.DISTRIBUTOR.label), - required=False, + required=Roles.DISTRIBUTOR.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) resource_user = ContactRoleMultipleChoiceField( label=_(Roles.RESOURCE_USER.label), - required=False, + required=Roles.RESOURCE_USER.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) resource_provider = ContactRoleMultipleChoiceField( label=_(Roles.RESOURCE_PROVIDER.label), - required=False, + required=Roles.RESOURCE_PROVIDER.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) originator = ContactRoleMultipleChoiceField( label=_(Roles.ORIGINATOR.label), - required=False, + required=Roles.ORIGINATOR.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) principal_investigator = ContactRoleMultipleChoiceField( label=_(Roles.PRINCIPAL_INVESTIGATOR.label), - required=False, + required=Roles.PRINCIPAL_INVESTIGATOR.is_required, queryset=get_user_model().objects.exclude( username='AnonymousUser'), widget=TaggitProfileSelect2Custom(url='autocomplete_profile')) diff --git a/geonode/base/models.py b/geonode/base/models.py index d868fe13c58..cd5e3fc68d0 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -24,7 +24,7 @@ import uuid import logging import traceback -from typing import List, Optional +from typing import List, Optional, Tuple from sequences.models import Sequence from sequences import get_next_value @@ -1807,6 +1807,8 @@ def maintenance_frequency_title(self): def language_title(self): return [v for v in enumerations.ALL_LANGUAGES if v[0] == self.language][0][1].title() + # Contact Roles + def add_missing_metadata_author_or_poc(self): """ Set metadata_author and/or point of contact (poc) to a resource when any of them is missing @@ -1959,6 +1961,15 @@ def _set_principal_investigator(self, user_profile): return self._set_contact_ro @property def principal_investigator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.principal_investigator) + def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]: + """ _summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. + + Returns: + _type_: List[Tuple[List[people object], roles_label_name]] + _description: list tuples including two elements: 1. list of people have a certain role. 2. role label + """ + return [ (self.__getattribute__(role.name),role.label) for role in Roles.get_multivalue_ones() if self.__getattribute__(role.name) ] + class LinkManager(models.Manager): """Helper class to access links grouped by type diff --git a/geonode/people/__init__.py b/geonode/people/__init__.py index 852ef1a6f2e..61f72d8ac82 100644 --- a/geonode/people/__init__.py +++ b/geonode/people/__init__.py @@ -85,4 +85,4 @@ def get_required_ones(cls): @classmethod def get_multivalue_ones(cls): - return [role for role in cls if role.is_multivalue] \ No newline at end of file + return [role for role in cls if role.is_multivalue] From 945d08af045b08a388c0be79b971030e8fa8370b Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 12 Jan 2023 11:29:36 +0100 Subject: [PATCH 09/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- .../backends/pycsw_local_mappings.py | 9 -- .../templates/catalogue/full_metadata.xml | 131 ++++-------------- 2 files changed, 29 insertions(+), 111 deletions(-) diff --git a/geonode/catalogue/backends/pycsw_local_mappings.py b/geonode/catalogue/backends/pycsw_local_mappings.py index 71cb6dafaba..90287bfbf27 100644 --- a/geonode/catalogue/backends/pycsw_local_mappings.py +++ b/geonode/catalogue/backends/pycsw_local_mappings.py @@ -78,15 +78,6 @@ 'pycsw:Publisher': 'publisher_csv', 'pycsw:Contributor': 'contributor_csv', 'pycsw:Processor': 'processor_csw', - - # 'pycsw:MetadataAuthor': 'metadata_author_csv', - # 'pycsw:Custodian': 'custodian_csv', - # 'pycsw:Distributor': 'distributor_csv', - # 'pycsw:ResourceUser': 'resource_user_csv', - # 'pycsw:ResourceProvider': 'resource_provider_csv', - # 'pycsw:Originator': 'originator_csv', - # 'pycsw:PrincipalInvestigator': 'principal_investigator_csv', - 'pycsw:Relation': 'relation', 'pycsw:Links': 'download_links', } diff --git a/geonode/catalogue/templates/catalogue/full_metadata.xml b/geonode/catalogue/templates/catalogue/full_metadata.xml index cc775fb1188..bc74760f0f7 100644 --- a/geonode/catalogue/templates/catalogue/full_metadata.xml +++ b/geonode/catalogue/templates/catalogue/full_metadata.xml @@ -12,58 +12,57 @@ dataset - - {% with layer.poc as poc %} + {% for contact_roles, label in layer.get_defined_contact_roles %} + {% for contact_role in contact_roles %} - - {% if poc.name %} {{ poc.name }} {% endif %} + {% else %}> + {{ contact_role.first_name }} {{ contact_role.last_name}}{% endif %} - - {% if poc.organization %} {{ poc.organization }} {% endif %} + + {% if contact_role.organization %} {{ contact_role.organization }} {% endif %} - - {% if poc.position %}{{ poc.position }} {% endif %} + + {% if contact_role.position %}{{ contact_role.position }} {% endif %} - - {% if poc.voice %}{{ poc.voice }}{% endif %} + + {% if contact_role.voice %}{{ contact_role.voice }}{% endif %} - - {% if poc.fax %}{{ poc.fax }} {%endif %} + + {% if contact_role.fax %}{{ contact_role.fax }} {%endif %} - - {% if poc.delivery %}{{ poc.delivery }}{% endif %} + + {% if contact_role.delivery %}{{ contact_role.delivery }}{% endif %} - - {% if poc.city %}{{ poc.city }}{% endif %} + + {% if contact_role.city %}{{ contact_role.city }}{% endif %} - - {% if poc.area %}{{ poc.area }}{% endif %} + + {% if contact_role.area %}{{ contact_role.area }}{% endif %} - - {% if poc.zipcode %}{{ poc.zipcode }}{% endif %} + + {% if contact_role.zipcode %}{{ contact_role.zipcode }}{% endif %} - - {% if poc.country %}{{ poc.country }}{% endif %} + + {% if contact_role.country %}{{ contact_role.country }}{% endif %} - - {% if poc.email %}{{ poc.email }}{% endif %} + + {% if contact_role.email %}{{ contact_role.email }}{% endif %} - {% if poc.user %} - {{ SITEURL }}{{ layer.poc.get_absolute_url }} + {{ SITEURL }}{{ contact_role.get_absolute_url }} WWW:LINK-1.0-http--link @@ -73,87 +72,15 @@ - {% endif %} - pointOfContact + {{ label }} - {% endwith %} - - {% with layer.metadata_author as metadata_author %} - - - - {% if metadata_author.name %} {{ metadata_author.name }} {% endif %} - - - {% if metadata_author.organization %} {{ metadata_author.organization }} {% endif %} - - - {% if metadata_author.position %}{{ metadata_author.position }} {% endif %} - - - - - - - {% if metadata_author.voice %}{{ metadata_author.voice }}{% endif %} - - - {% if metadata_author.fax %}{{ metadata_author.fax }} {%endif %} - - - - - - - {% if metadata_author.delivery %}{{ metadata_author.delivery }}{% endif %} - - - {% if metadata_author.city %}{{ metadata_author.city }}{% endif %} - - - {% if metadata_author.area %}{{ metadata_author.area }}{% endif %} - - - {% if metadata_author.zipcode %}{{ metadata_author.zipcode }}{% endif %} - - - {% if metadata_author.country %}{{ metadata_author.country }}{% endif %} - - - {% if metadata_author.email %}{{ metadata_author.email }}{% endif %} - - - - {% if metadata_author.user %} - - - - {{ SITEURL }}{{ layer.metadata_author.get_absolute_url }} - - - WWW:LINK-1.0-http--link - - - GeoNode profile page - - - - {% endif %} - - - - author - - - - {% endwith %} - - + {% endfor %} + {% endfor %} {{layer.csw_insert_date|date:"Y-m-d\TH:i:s\Z"}} @@ -295,7 +222,7 @@ - originator + owner From 5d10ab4f39c0fdf73d7483c0bbbf7c65d9fda6c6 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 12 Jan 2023 16:18:58 +0100 Subject: [PATCH 10/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- .../templates/layouts/doc_panels.html | 114 +++++++++++++++--- geonode/documents/views.py | 2 +- .../geoapps/templates/apps/app_metadata.html | 2 +- .../geoapps/templates/layouts/app_panels.html | 100 +++++++++++++-- geonode/geoapps/views.py | 76 +++--------- geonode/layers/templates/layouts/panels.html | 5 +- .../maps/templates/layouts/map_panels.html | 114 +++++++++++++++--- geonode/maps/views.py | 2 +- 8 files changed, 299 insertions(+), 116 deletions(-) diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html index 3f48b433da7..751d5ce49bb 100644 --- a/geonode/documents/templates/layouts/doc_panels.html +++ b/geonode/documents/templates/layouts/doc_panels.html @@ -553,32 +553,110 @@ {% endblock doc_extra_metadata %}
            -
            -
            -
            {% trans "Responsible Parties" %}
            -
            - - {{ document_form.poc }} -
            + +
            +
            +
            {% trans "Responsible Parties" %}
            + {% block document_poc %} +
            + + {{ document_form.poc }} +
            + {% endblock document_poc %}
            -
            {% trans "Responsible and Permissions" %}
            -
            -
            - - - {{ document_form.owner }} +
            {% trans "Responsible and Permissions" %}
            +
            + {% block document_owner %} +
            + + {{ document_form.owner }} +
            + {% endblock document_owner %} +
            +
            + {% trans "toggle more Contact Roles" %} +
            +
            {% trans "more metadata contact roles" %}
            +
            + {% block document_metadata_author %} +
            + + {{ document_form.metadata_author }} +
            + {% endblock document_metadata_author %} +
            +
            + {% block document_processor %} +
            + + {{ document_form.processor }} +
            + {% endblock document_processor %}
            -
            - - - {{ document_form.metadata_author }} +
            + {% block document_publisher %} +
            + + {{ document_form.publisher }} +
            + {% endblock document_publisher %} +
            +
            + {% block document_custodian %} +
            + + {{ document_form.custodian }} +
            + {% endblock document_custodian %} +
            +
            + {% block document_distributor %} +
            + + {{ document_form.distributor }} +
            + {% endblock document_distributor %} +
            +
            + {% block document_resource_user %} +
            + + {{ document_form.resource_user }} +
            + {% endblock document_resource_user %} +
            +
            + {% block document_resource_provider %} +
            + + {{ document_form.resource_provider }} +
            + {% endblock document_resource_provider %}
            +
            + {% block document_originator %} +
            + + {{ document_form.originator }} +
            + {% endblock document_originator %} +
            +
            + {% block document_principal_investigator %} +
            + + {{ document_form.principal_investigator }} +
            + {% endblock document_principal_investigator %} +
            +
            +
            -
            + {% endblock ownership %} diff --git a/geonode/documents/views.py b/geonode/documents/views.py index 2441a8b0537..120f24d9ab0 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -492,7 +492,7 @@ def document_metadata( # define contact role forms contact_role_forms_context = {} for role in document.get_multivalue_role_property_names(): - document.fields[role].initial = [p.username for p in document.__getattribute__(role)] + document_form.fields[role].initial = [p.username for p in document.__getattribute__(role)] role_form = ProfileForm(prefix=role) role_form.hidden = True contact_role_forms_context[f"{role}_form"] = role_form diff --git a/geonode/geoapps/templates/apps/app_metadata.html b/geonode/geoapps/templates/apps/app_metadata.html index 9e92fad7720..1ce1fd240fd 100644 --- a/geonode/geoapps/templates/apps/app_metadata.html +++ b/geonode/geoapps/templates/apps/app_metadata.html @@ -68,7 +68,7 @@

            {% trans "Point of Contact" %}

            diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html index f4b617bf99d..d974ca959dd 100644 --- a/geonode/geoapps/templates/layouts/app_panels.html +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -483,32 +483,110 @@ {% endblock geoapp_extra_metadata %} -
            -
            + +
            +
            {% trans "Responsible Parties" %}
            -
            - - {{ geoapp_form.poc }} -
            + {% block geoapp_poc %} +
            + + {{ geoapp_form.poc }} +
            + {% endblock geoapp_poc %}
            {% trans "Responsible and Permissions" %}
            + {% block geoapp_owner %}
            - - - {{ geoapp_form.owner }} + + {{ geoapp_form.owner }}
            + {% endblock geoapp_owner %} +
            +
            + {% trans "toggle more Contact Roles" %} +
            +
            {% trans "more metadata contact roles" %}
            +
            + {% block geoapp_metadata_author %}
            - {{ geoapp_form.metadata_author }}
            + {% endblock geoapp_metadata_author %} +
            +
            + {% block geoapp_processor %} +
            + + {{ geoapp_form.processor }} +
            + {% endblock geoapp_processor %} +
            +
            + {% block geoapp_publisher %} +
            + + {{ geoapp_form.publisher }} +
            + {% endblock geoapp_publisher %} +
            +
            + {% block geoapp_custodian %} +
            + + {{ geoapp_form.custodian }} +
            + {% endblock geoapp_custodian %} +
            +
            + {% block geoapp_distributor %} +
            + + {{ geoapp_form.distributor }} +
            + {% endblock geoapp_distributor %} +
            +
            + {% block geoapp_resource_user %} +
            + + {{ geoapp_form.resource_user }} +
            + {% endblock geoapp_resource_user %} +
            +
            + {% block geoapp_resource_provider %} +
            + + {{ geoapp_form.resource_provider }} +
            + {% endblock geoapp_resource_provider %} +
            +
            + {% block geoapp_originator %} +
            + + {{ geoapp_form.originator }} +
            + {% endblock geoapp_originator %} +
            +
            + {% block geoapp_principal_investigator %} +
            + + {{ geoapp_form.principal_investigator }} +
            + {% endblock geoapp_principal_investigator %} +
            +
            + - +
            diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index 652bf314d7d..18e4bcb7896 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -227,8 +227,6 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T # Add metadata_author or poc if missing geoapp_obj.add_missing_metadata_author_or_poc() resource_type = geoapp_obj.resource_type - poc = geoapp_obj.poc - metadata_author = geoapp_obj.metadata_author topic_category = geoapp_obj.category current_keywords = [keyword.name for keyword in geoapp_obj.keywords.all()] @@ -289,10 +287,7 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T values = [keyword.id for keyword in topic_thesaurus if int(tid) == keyword.thesaurus.id] tkeywords_form.fields[tid].initial = values - if request.method == "POST" and geoapp_form.is_valid( - ) and category_form.is_valid() and tkeywords_form.is_valid(): - new_poc = geoapp_form.cleaned_data.pop('poc') - new_author = geoapp_form.cleaned_data.pop('metadata_author') + if request.method == "POST" and geoapp_form.is_valid() and category_form.is_valid() and tkeywords_form.is_valid(): new_keywords = current_keywords if request.keyword_readonly else geoapp_form.cleaned_data.pop('keywords') new_regions = geoapp_form.cleaned_data.pop('regions') @@ -301,50 +296,13 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T category_form.cleaned_data['category_choice_field']: new_category = TopicCategory.objects.get( id=int(category_form.cleaned_data['category_choice_field'])) - - if new_poc is None: - if poc is None: - poc_form = ProfileForm( - request.POST, - prefix="poc", - instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.is_valid(): - if len(poc_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = poc_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set a point of contact for this resource')) - poc = None - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", - instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.is_valid(): - if len(author_form.cleaned_data['profile']) == 0: - # FIXME use form.add_error in django > 1.7 - errors = author_form._errors.setdefault( - 'profile', ErrorList()) - errors.append( - _('You must set an author for this resource')) - metadata_author = None - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() - geoapp_form.cleaned_data.pop('ptype') - additional_vals = dict( - poc=new_poc or geoapp_obj.poc, - metadata_author=new_author or geoapp_obj.metadata_author, - category=new_category - ) + # update contact roles + geoapp_obj.set_contact_roles_from_metadata_edit(geoapp_form) + geoapp_obj.save() + + additional_vals = dict(category=new_category) geoapp_form.cleaned_data.pop('metadata') extra_metadata = geoapp_form.cleaned_data.pop('extra_metadata') @@ -359,7 +317,6 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T instance=geoapp_obj, keywords=new_keywords, regions=new_regions, - vals=_vals, notify=True, extra_metadata=json.loads(extra_metadata) ) @@ -419,16 +376,13 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T status=400) # - POST Request Ends here - - # Request.GET - if poc is not None: - geoapp_form.fields['poc'].initial = poc.id - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - - if metadata_author is not None: - geoapp_form.fields['metadata_author'].initial = metadata_author.id - author_form = ProfileForm(prefix="author") - author_form.hidden = True + # define contact role forms + contact_role_forms_context = {} + for role in geoapp_obj.get_multivalue_role_property_names(): + geoapp_form.fields[role].initial = [p.username for p in geoapp_obj.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) @@ -442,8 +396,6 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T "resource": geoapp_obj, "geoapp": geoapp_obj, "geoapp_form": geoapp_form, - "poc_form": poc_form, - "author_form": author_form, "category_form": category_form, "tkeywords_form": tkeywords_form, "metadata_author_groups": metadata_author_groups, @@ -454,7 +406,7 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T | set(getattr(settings, 'UI_REQUIRED_FIELDS', [])) ) - }) + } | contact_role_forms_context) @login_required diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index 613eaa427be..7ff23269280 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -556,6 +556,7 @@ {% endblock layer_extra_metadata %}
            +
            {% trans "Responsible Parties" %}
            @@ -577,10 +578,9 @@ {% endblock dataset_owner %}
            - {% trans "toggle more Contact Roles" %}
            -
            {% trans "more Metadata Contact Roles" %}
            +
            {% trans "more metadata contact roles" %}
            {% block dataset_metadata_author %}
            @@ -589,7 +589,6 @@
            {% endblock dataset_metadata_author %}
            -
            {% block dataset_processor %}
            diff --git a/geonode/maps/templates/layouts/map_panels.html b/geonode/maps/templates/layouts/map_panels.html index 4ce88a199d8..abf335f3055 100644 --- a/geonode/maps/templates/layouts/map_panels.html +++ b/geonode/maps/templates/layouts/map_panels.html @@ -463,9 +463,7 @@ {% endblock map_constraints_other %}
            -
            - -
            +
            @@ -541,33 +539,111 @@ {% endblock map_extra_metadata %} -
            -
            -
            {% trans "Responsible Parties" %}
            -
            + +
            +
            +
            {% trans "Responsible Parties" %}
            + {% block map_poc %} +
            {{ map_form.poc }}
            + {% endblock map_poc %}
            -
            -
            {% trans "Responsible and Permissions" %}
            -
            -
            +
            +
            {% trans "Responsible and Permissions" %}
            +
            + {% block map_owner %} +
            - {{ map_form.owner }} -
            -
            - - - {{ map_form.metadata_author }} -
            +
            + {% endblock map_owner %} +
            +
            + {% trans "toggle more Contact Roles" %} +
            +
            {% trans "more metadata contact roles" %}
            +
            + {% block map_metadata_author %} +
            + + {{ map_form.metadata_author }} +
            + {% endblock map_metadata_author %} +
            +
            + {% block map_processor %} +
            + + {{ map_form.processor }} +
            + {% endblock map_processor %} +
            +
            + {% block map_publisher %} +
            + + {{ map_form.publisher }} +
            + {% endblock map_publisher %} +
            +
            + {% block map_custodian %} +
            + + {{ map_form.custodian }} +
            + {% endblock map_custodian %}
            +
            + {% block map_distributor %} +
            + + {{ map_form.distributor }} +
            + {% endblock map_distributor %} +
            +
            + {% block map_resource_user %} +
            + + {{ map_form.resource_user }} +
            + {% endblock map_resource_user %} +
            +
            + {% block map_resource_provider %} +
            + + {{ map_form.resource_provider }} +
            + {% endblock map_resource_provider %} +
            +
            + {% block map_originator %} +
            + + {{ map_form.originator }} +
            + {% endblock map_originator %} +
            +
            + {% block map_principal_investigator %} +
            + + {{ map_form.principal_investigator }} +
            + {% endblock map_principal_investigator %} +
            +
            +
            -
            + +
            diff --git a/geonode/maps/views.py b/geonode/maps/views.py index ec416d4f00a..f8366d482dc 100644 --- a/geonode/maps/views.py +++ b/geonode/maps/views.py @@ -168,7 +168,7 @@ def map_metadata(request, mapid, template="maps/map_metadata.html", ajax=True): id=int(category_form.cleaned_data['category_choice_field'])) # update contact roles - map_obj.store_contact_roles_from_metadata_edit(request) + map_obj.set_contact_roles_from_metadata_edit(request) map_obj.save() map_obj.set_contact_roles_from_metadata_edit(map_form) From 8768cb5dd6975cce598d877784932b4fb143787c Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 13 Jan 2023 11:38:33 +0100 Subject: [PATCH 11/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 14 +++++++------- geonode/catalogue/views.py | 11 ++++++----- geonode/documents/views.py | 2 -- .../templates/apps/app_metadata_advanced.html | 2 +- geonode/geoapps/views.py | 1 - geonode/people/__init__.py | 4 +++- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index cd5e3fc68d0..bfd0484cefb 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1867,7 +1867,7 @@ def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: """ generell getter of for all contact roles except owner - param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested + param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested return List(ContactRole): returns the requested contact role from the database """ try: @@ -1962,13 +1962,13 @@ def _set_principal_investigator(self, user_profile): return self._set_contact_ro def principal_investigator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.principal_investigator) def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]: - """ _summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. + """ _summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. - Returns: - _type_: List[Tuple[List[people object], roles_label_name]] - _description: list tuples including two elements: 1. list of people have a certain role. 2. role label - """ - return [ (self.__getattribute__(role.name),role.label) for role in Roles.get_multivalue_ones() if self.__getattribute__(role.name) ] + Returns: + _type_: List[Tuple[List[people object], roles_label_name]] + _description: list tuples including two elements: 1. list of people have a certain role. 2. role label + """ + return [(self.__getattribute__(role.name), role.label) for role in Roles.get_multivalue_ones() if self.__getattribute__(role.name)] class LinkManager(models.Manager): diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index 4b0534914a8..0df15934788 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -36,6 +36,7 @@ from django.core.exceptions import ObjectDoesNotExist from geonode.people import Roles + @csrf_exempt def csw_global_dispatch(request, dataset_filter=None, config_updater=None): """pycsw wrapper""" @@ -298,7 +299,7 @@ def __append_contact_role__(content, cr_attr_name, title_in_txt): content += f"name{s}{fst(user.last_name)}{sc}" content += f"e-mail{s}{fst(user.email)}{sc}" return content - + for role in set(Roles).difference([Roles.OWNER]): content = __append_contact_role__(content, role.name, role.label) @@ -333,12 +334,12 @@ def csw_render_extra_format_html(request, layeruuid, resname): extra_res_md["roles"] = [] for role in Roles: cr = resource.__getattribute__(role.name) - if not type(cr)==list: + if not type(cr) == list: cr = [cr] - users=[{"pk":user.id, 'last_name': user.last_name, 'email': user.email} for user in cr] + users = [{"pk": user.id, 'last_name': user.last_name, 'email': user.email} for user in cr] if users: - extra_res_md["roles"].append({"label":role.label, "users":users}) - + extra_res_md["roles"].append({"label": role.label, "users": users}) + return render(request, "geonode_metadata_full.html", context={"resource": resource, "extra_res_md": extra_res_md}) diff --git a/geonode/documents/views.py b/geonode/documents/views.py index 120f24d9ab0..3cb45a8fb95 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -23,12 +23,10 @@ import warnings import traceback - from django.urls import reverse from django.conf import settings from django.contrib import messages from django.shortcuts import render, get_object_or_404 -from django.forms.utils import ErrorList from django.utils.translation import ugettext as _ from django.contrib.auth.decorators import login_required from django.template import loader diff --git a/geonode/geoapps/templates/apps/app_metadata_advanced.html b/geonode/geoapps/templates/apps/app_metadata_advanced.html index 21d45962dc8..ee0440f4a53 100644 --- a/geonode/geoapps/templates/apps/app_metadata_advanced.html +++ b/geonode/geoapps/templates/apps/app_metadata_advanced.html @@ -130,7 +130,7 @@

            {% trans "Point of Contact" %}

            diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index 18e4bcb7896..7cf78c61ccf 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -24,7 +24,6 @@ from django.conf import settings from django.shortcuts import render -from django.forms.utils import ErrorList from django.utils.translation import ugettext as _ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect, Http404 diff --git a/geonode/people/__init__.py b/geonode/people/__init__.py index 61f72d8ac82..d0e12fce29e 100644 --- a/geonode/people/__init__.py +++ b/geonode/people/__init__.py @@ -20,6 +20,7 @@ from geonode.notifications_helper import NotificationsAppConfigBase import enum + class PeopleAppConfig(NotificationsAppConfigBase): name = 'geonode.people' NOTIFICATIONS = (("user_follow", _("User following you"), _("Another user has started following you"),), @@ -35,6 +36,7 @@ def ready(self): default_app_config = 'geonode.people.PeopleAppConfig' + class Role: def __init__(self, label, is_required, is_multivalue): self.label = label @@ -63,7 +65,7 @@ class Roles(enum.Enum): @property def name(self): return super().name.lower() - + @property def label(self): return self.value.label From de273d600140f637ee6db6983f890d90905ed643 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 13 Jan 2023 14:26:31 +0100 Subject: [PATCH 12/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 2 +- geonode/catalogue/views.py | 2 +- geonode/locale/de/LC_MESSAGES/django.mo | Bin 156640 -> 156832 bytes geonode/locale/de/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/en/LC_MESSAGES/django.mo | Bin 143913 -> 144085 bytes geonode/locale/en/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/fr/LC_MESSAGES/django.mo | Bin 161109 -> 161262 bytes geonode/locale/fr/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/it/LC_MESSAGES/django.mo | Bin 155297 -> 155440 bytes geonode/locale/it/LC_MESSAGES/django.po | 6 ++++++ 10 files changed, 26 insertions(+), 2 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index bfd0484cefb..79d6ead9081 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1858,7 +1858,7 @@ def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: for role in self.get_multivalue_role_property_names(): try: self.__setattr__(role, resource_base_form.cleaned_data[role]) - except: + except AttributeError: logger.warning(f"unable to set contact role {role} for {self} ...") failed = True return failed diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index 0df15934788..4935e036526 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -29,7 +29,7 @@ from geonode.base.models import ResourceBase from geonode.layers.models import Dataset from geonode.base.auth import get_or_create_token -from geonode.base.models import ContactRole, SpatialRepresentationType +from geonode.base.models import SpatialRepresentationType from geonode.groups.models import GroupProfile from geonode.utils import resolve_object from django.db import connection diff --git a/geonode/locale/de/LC_MESSAGES/django.mo b/geonode/locale/de/LC_MESSAGES/django.mo index 2034684e2c89f29e0300b7aa74d4415f38757a97..95e04cff54c2ac0d3874ac73c5d7f354f7ee4379 100644 GIT binary patch delta 32923 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 IC&K6d0Ykkrvj6}9 diff --git a/geonode/locale/de/LC_MESSAGES/django.po b/geonode/locale/de/LC_MESSAGES/django.po index 854efdbe448..dd8cfaf6914 100644 --- a/geonode/locale/de/LC_MESSAGES/django.po +++ b/geonode/locale/de/LC_MESSAGES/django.po @@ -1231,6 +1231,12 @@ msgstr "Andere, optionale, Metadaten" msgid "Responsible Parties" msgstr "Verantworliche Person" +msgid "toggle more Contact Roles" +msgstr "weitere Rollen anzeigen" + +msgid "more metadata contact roles" +msgstr "weitere Kontakt Rollen für die Metadaten" + msgid "Responsible and Permissions" msgstr "Verantwortliche und Berechtigungen" diff --git a/geonode/locale/en/LC_MESSAGES/django.mo b/geonode/locale/en/LC_MESSAGES/django.mo index f8663d06708af49835ebb15452c0cb68aec04c63..f8139b550fcd67f01b9c200874e1f8afb2a6c370 100644 GIT binary patch delta 32061 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 ZEw@G>w{DK$vq4)j)Cs!($Nz6D{|_{!KyUy6 delta 31933 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 diff --git a/geonode/locale/en/LC_MESSAGES/django.po b/geonode/locale/en/LC_MESSAGES/django.po index ae445ae92c7..70cd37a772c 100644 --- a/geonode/locale/en/LC_MESSAGES/django.po +++ b/geonode/locale/en/LC_MESSAGES/django.po @@ -1215,6 +1215,12 @@ msgstr "Other, Optional, Metadata" msgid "Responsible Parties" msgstr "Responsible Parties" +msgid "toggle more Contact Roles" +msgstr "toggle more Contact Roles" + +msgid "more metadata contact roles" +msgstr "more metadata contact roles" + msgid "Responsible and Permissions" msgstr "Responsible and Permissions" diff --git a/geonode/locale/fr/LC_MESSAGES/django.mo b/geonode/locale/fr/LC_MESSAGES/django.mo index 1103341e8455f922d9b813bfdc46aa9d53af6328..7c80f5526614605724deda336f4a9f4385b791cb 100644 GIT binary patch delta 25409 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 delta 25258 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 diff --git a/geonode/locale/fr/LC_MESSAGES/django.po b/geonode/locale/fr/LC_MESSAGES/django.po index d470d6f59aa..2dd120c69d2 100644 --- a/geonode/locale/fr/LC_MESSAGES/django.po +++ b/geonode/locale/fr/LC_MESSAGES/django.po @@ -1252,6 +1252,12 @@ msgstr "Autre, facultatif, métadonnées" msgid "Responsible Parties" msgstr "Parties responsables" +msgid "toggle more Contact Roles" +msgstr "afficher plus de rôles" + +msgid "more metadata contact roles" +msgstr "plus de rôles de contact de métadonnées" + msgid "Responsible and Permissions" msgstr "Responsabilité et autorisations" diff --git a/geonode/locale/it/LC_MESSAGES/django.mo b/geonode/locale/it/LC_MESSAGES/django.mo index a093d7f2986572518ff6c3196a2195cdfea49d9e..5d360014fa9aa9b518d459abc4f5d9876b7f20a4 100644 GIT binary patch delta 25686 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 Date: Fri, 13 Jan 2023 18:48:59 +0100 Subject: [PATCH 13/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 98 +++++++++++++++++++++------------------ geonode/base/tests.py | 103 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 47 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index 79d6ead9081..3bbe609a177 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -24,7 +24,7 @@ import uuid import logging import traceback -from typing import List, Optional, Tuple +from typing import List, Optional, Union, Tuple from sequences.models import Sequence from sequences import get_next_value @@ -1845,14 +1845,16 @@ def get_multivalue_required_role_property_names() -> List[str]: ) ] ) - # from geonode.base.forms import ResourceBaseForm; unable due to circular ... + # typing not possible due to: from geonode.base.forms import ResourceBaseForm; unable due to circular ... def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: - """ gets a ResourceBaseForm and extracts the Contact Role elements from it + """_summary_: gets a ResourceBaseForm and extracts the Contact Role elements from it Args: resource_base_form (ResourceBaseForm): ResourceBaseForm with contact roles set - return (bool): returns true if all contact roles could be set, else false + + Returns: + bool: true if all contact roles could be set, else false """ failed = False for role in self.get_multivalue_role_property_names(): @@ -1863,12 +1865,13 @@ def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: failed = True return failed - def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: - """ - generell getter of for all contact roles except owner + def __get_contact_role_elements__(self, role: str) -> Optional[List[settings.AUTH_USER_MODEL]]: + """_summary_: general getter of for all contact roles except owner - param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested - return List(ContactRole): returns the requested contact role from the database + Args: + role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested + Returns: + Optional[List[settings.AUTH_USER_MODEL]]: returns the requested contact role from the database """ try: contact_role = ContactRole.objects.filter( @@ -1878,15 +1881,18 @@ def _get_contact_role_elements(self, role: str) -> List[Optional[ContactRole]]: contacts = None return contacts - def _set_contact_role_element(self, user_profile, role: str): - """ - general setter for all contact roles except owner in resource base + # types allowed as input for Contact role properties + CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES = Union[settings.AUTH_USER_MODEL, QuerySet, List[settings.AUTH_USER_MODEL]] + + def __set_contact_role_element__(self, user_profile: CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES, role: str): + """_summary_: general setter for all contact roles except owner in resource base - param contact_role (ContactRole): - param role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set + Args: + user_profile (CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES): _description_ + role (str): tring coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set """ - def __create_role__(resource, role: str, user_profile): + def __create_role__(resource, role: str, user_profile: settings.AUTH_USER_MODEL) -> List[settings.AUTH_USER_MODEL]: return ContactRole.objects.create( role=role, resource=resource, @@ -1898,78 +1904,80 @@ def __create_role__(resource, role: str, user_profile): elif isinstance(user_profile, get_user_model()): ContactRole.objects.filter(role=role, resource=self).delete() return [__create_role__(self, role, user_profile)] + elif isinstance(user_profile, list) and all(isinstance(x, get_user_model()) for x in user_profile): + return [__create_role__(self, role, user_profile)] else: logger.error(f"Bad profile format for role: {role} ...") - def _get_poc(self): return self._get_contact_role_elements(role="pointOfContact") - def _set_poc(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="pointOfContact") - poc = property(_get_poc, _set_poc) + def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]: + """ _summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. + + Returns: + _type_: List[Tuple[List[people object], roles_label_name]] + _description: list tuples including two elements: 1. list of people have a certain role. 2. role label + """ + return {role.label: self.__getattribute__(role.name) for role in Roles.get_multivalue_ones() if self.__getattribute__(role.name)} + + def __get_poc__(self) -> List[settings.AUTH_USER_MODEL]: return self.__get_contact_role_elements__(role="pointOfContact") + def __set_poc__(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role="pointOfContact") + poc = property(__get_poc__, __set_poc__) @property def poc_csv(self): return ','.join(p.get_full_name() or p.username for p in self.poc) - def _get_metadata_author(self): return self._get_contact_role_elements(role="author") - def _set_metadata_author(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role="author") + def _get_metadata_author(self): return self.__get_contact_role_elements__(role="author") + def _set_metadata_author(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role="author") metadata_author = property(_get_metadata_author, _set_metadata_author) @property def metadata_author_csv(self): return ','.join(p.get_full_name() or p.username for p in self.metadata_author) - def _get_processor(self): return self._get_contact_role_elements(role=Roles.PROCESSOR.name) - def _set_processor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PROCESSOR.name) + def _get_processor(self): return self.__get_contact_role_elements__(role=Roles.PROCESSOR.name) + def _set_processor(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PROCESSOR.name) processor = property(_get_processor, _set_processor) @property def processor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.processor) - def _get_publisher(self): return self._get_contact_role_elements(role=Roles.PUBLISHER.name) - def _set_publisher(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PUBLISHER.name) + def _get_publisher(self): return self.__get_contact_role_elements__(role=Roles.PUBLISHER.name) + def _set_publisher(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PUBLISHER.name) publisher = property(_get_publisher, _set_publisher) @property def publisher_csv(self): return ','.join(p.get_full_name() or p.username for p in self.publisher) - def _get_custodian(self): return self._get_contact_role_elements(role=Roles.CUSTODIAN.name) - def _set_custodian(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.CUSTODIAN.name) + def _get_custodian(self): return self.__get_contact_role_elements__(role=Roles.CUSTODIAN.name) + def _set_custodian(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.CUSTODIAN.name) custodian = property(_get_custodian, _set_custodian) @property def custodian_csv(self): return ','.join(p.get_full_name() or p.username for p in self.custodian) - def _get_distributor(self): return self._get_contact_role_elements(role=Roles.DISTRIBUTOR.name) - def _set_distributor(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.DISTRIBUTOR.name) + def _get_distributor(self): return self.__get_contact_role_elements__(role=Roles.DISTRIBUTOR.name) + def _set_distributor(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.DISTRIBUTOR.name) distributor = property(_get_distributor, _set_distributor) @property def distributor_csv(self): return ','.join(p.get_full_name() or p.username for p in self.distributor) - def _get_resource_user(self): return self._get_contact_role_elements(role=Roles.RESOURCE_USER.name) - def _set_resource_user(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.RESOURCE_USER.name) + def _get_resource_user(self): return self.__get_contact_role_elements__(role=Roles.RESOURCE_USER.name) + def _set_resource_user(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.RESOURCE_USER.name) resource_user = property(_get_resource_user, _set_resource_user) @property def resource_user_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_user) - def _get_resource_provider(self): return self._get_contact_role_elements(role=Roles.RESOURCE_PROVIDER.name) - def _set_resource_provider(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.RESOURCE_PROVIDER.name) + def _get_resource_provider(self): return self.__get_contact_role_elements__(role=Roles.RESOURCE_PROVIDER.name) + def _set_resource_provider(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.RESOURCE_PROVIDER.name) resource_provider = property(_get_resource_provider, _set_resource_provider) @property def resource_provider_csv(self): return ','.join(p.get_full_name() or p.username for p in self.resource_provider) - def _get_originator(self): return self._get_contact_role_elements(role=Roles.ORIGINATOR.name) - def _set_originator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.ORIGINATOR.name) + def _get_originator(self): return self.__get_contact_role_elements__(role=Roles.ORIGINATOR.name) + def _set_originator(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.ORIGINATOR.name) originator = property(_get_originator, _set_originator) @property def originator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.originator) - def _get_principal_investigator(self): return self._get_contact_role_elements(role=Roles.PRINCIPAL_INVESTIGATOR.name) - def _set_principal_investigator(self, user_profile): return self._set_contact_role_element(user_profile=user_profile, role=Roles.PRINCIPAL_INVESTIGATOR.name) + def _get_principal_investigator(self): return self.__get_contact_role_elements__(role=Roles.PRINCIPAL_INVESTIGATOR.name) + def _set_principal_investigator(self, user_profile): return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PRINCIPAL_INVESTIGATOR.name) principal_investigator = property(_get_principal_investigator, _set_principal_investigator) @property def principal_investigator_csv(self): return ','.join(p.get_full_name() or p.username for p in self.principal_investigator) - def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]: - """ _summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. - - Returns: - _type_: List[Tuple[List[people object], roles_label_name]] - _description: list tuples including two elements: 1. list of people have a certain role. 2. role label - """ - return [(self.__getattribute__(role.name), role.label) for role in Roles.get_multivalue_ones() if self.__getattribute__(role.name)] - class LinkManager(models.Manager): """Helper class to access links grouped by type diff --git a/geonode/base/tests.py b/geonode/base/tests.py index 7be78f2ea6a..d4ccebfd771 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -158,8 +158,107 @@ def test_add_missing_metadata_author_or_poc(self): user = get_user_model().objects.create(username='zlatan_i') self.rb.owner = user self.rb.add_missing_metadata_author_or_poc() - self.assertEqual(self.rb.metadata_author.username, 'zlatan_i') - self.assertEqual(self.rb.poc.username, 'zlatan_i') + self.assertTrue('zlatan_i' in [author.username for author in self.rb.metadata_author]) + self.assertTrue('zlatan_i' in [author.username for author in self.rb.poc]) + + +class TestCreationOfContactRolesByDifferentInputTypes(ThumbnailTests): + + """ + Test that contact roles can be set as people profile + """ + + def test_set_contact_role_as_people_profile(self): + user = get_user_model().objects.create(username='zlatan_i') + + self.rb.owner = user + self.rb.metadata_author = user + self.rb.poc = user + self.rb.publisher = user + self.rb.custodian = user + self.rb.distributor = user + self.rb.resource_user = user + self.rb.resource_provider = user + self.rb.originator = user + self.rb.principal_investigator = user + self.rb.processor = user + + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.metadata_author]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.poc]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.publisher]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.custodian]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.distributor]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.resource_user]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.resource_provider]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.originator]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.principal_investigator]) + self.assertTrue('zlatan_i' in [cr.username for cr in self.rb.processor]) + + """ + Test that contact roles can be set as list of people profiles + """ + + def test_set_contact_role_as_querysert(self): + user = get_user_model().objects.create(username='zlatan_i') + user2 = get_user_model().objects.create(username='zlatan_i') + + profile_list = [user, user2] + + self.rb.owner = user + self.rb.metadata_author = profile_list + self.rb.poc = profile_list + self.rb.publisher = profile_list + self.rb.custodian = profile_list + self.rb.distributor = profile_list + self.rb.resource_user = profile_list + self.rb.resource_provider = profile_list + self.rb.originator = profile_list + self.rb.principal_investigator = profile_list + self.rb.processor = profile_list + + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.metadata_author]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.poc]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.publisher]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.custodian]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.distributor]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.resource_user]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.resource_provider]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.originator]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.principal_investigator]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.processor]) + + """ + Test that contact roles can be set as queryset + """ + + def test_set_contact_role_as_querysert(self): + user = get_user_model().objects.create(username='zlatan_i') + user2 = get_user_model().objects.create(username='zlatan_i') + + query = get_user_model().objects.filter(username__in=["zlatan_i", "sven_z"]) + + self.rb.owner = user + self.rb.metadata_author = query + self.rb.poc = query + self.rb.publisher = query + self.rb.custodian = query + self.rb.distributor = query + self.rb.resource_user = query + self.rb.resource_provider = query + self.rb.originator = query + self.rb.principal_investigator = query + self.rb.processor = query + + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.metadata_author]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.poc]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.publisher]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.custodian]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.distributor]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.resource_user]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.resource_provider]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.originator]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.principal_investigator]) + self.assertTrue('zlatan_i' and "sven_z" in [cr.username for cr in self.rb.processor]) class RenderMenuTagTest(GeoNodeBaseTestSupport): From 22f735f985cab54b5905dfd60b570a752edf8df3 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 13 Jan 2023 19:13:51 +0100 Subject: [PATCH 14/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geonode/base/tests.py b/geonode/base/tests.py index d4ccebfd771..0d13263a376 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -198,7 +198,7 @@ def test_set_contact_role_as_people_profile(self): Test that contact roles can be set as list of people profiles """ - def test_set_contact_role_as_querysert(self): + def test_set_contact_role_as_list_of_people(self): user = get_user_model().objects.create(username='zlatan_i') user2 = get_user_model().objects.create(username='zlatan_i') @@ -231,9 +231,9 @@ def test_set_contact_role_as_querysert(self): Test that contact roles can be set as queryset """ - def test_set_contact_role_as_querysert(self): + def test_set_contact_role_as_queryset(self): user = get_user_model().objects.create(username='zlatan_i') - user2 = get_user_model().objects.create(username='zlatan_i') + get_user_model().objects.create(username='zlatan_i') query = get_user_model().objects.filter(username__in=["zlatan_i", "sven_z"]) From d7e0c94f91031dad80ffdd1637a1542181e54323 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 16 Jan 2023 11:34:25 +0100 Subject: [PATCH 15/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/forms.py | 14 ++++++-------- geonode/base/widgets.py | 4 ++-- geonode/layers/tests.py | 43 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 358089484a1..6c8532f0940 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -371,14 +371,12 @@ def _get_thesauro_title_label(item, lang): class ContactRoleMultipleChoiceField(forms.ModelMultipleChoiceField): - # TODO ERROR HANDLING def clean(self, value): - # try: - users = get_user_model().objects.filter(username__in=value) - # except: - # raise forms.ValidationError(_("Something went wrong in finding the profiles")) - # if len(users) < len(value): - # raise forms.ValidationError(_("not alle given profiles are found, maybe a typo?")) + try: + users = get_user_model().objects.filter(username__in=value) + except TypeError: + # value of not supported type ... + raise forms.ValidationError(_("Something went wrong in finding the profile(s) in a contact role form ...")) return users @@ -570,7 +568,7 @@ def __init__(self, *args, **kwargs): 'data-container': 'body', 'data-html': 'true'}) - if field in ['poc', 'owner'] and not self.can_change_perms: + if field in ['owner'] and not self.can_change_perms: self.fields[field].disabled = True def disable_keywords_widget_for_non_superuser(self, user): diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py index 5e7b9597d86..535d69a7ef1 100644 --- a/geonode/base/widgets.py +++ b/geonode/base/widgets.py @@ -32,7 +32,7 @@ def value_from_datadict(self, data, files, name): returns list of selected elements """ try: - ret_list = data.getlist(name) + ret_list = data[name] return ret_list - except TypeError: + except KeyError: return [] diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py index 794a0ceae6f..942d874781a 100644 --- a/geonode/layers/tests.py +++ b/geonode/layers/tests.py @@ -57,6 +57,7 @@ from geonode.layers.views import _resolve_dataset from geonode import GeoNodeException, geoserver from geonode.people.utils import get_valid_user +from geonode.people import Roles from guardian.shortcuts import get_anonymous_user from geonode.tests.base import GeoNodeBaseTestSupport from geonode.resource.manager import resource_manager @@ -1861,6 +1862,8 @@ def test_give_single_file_should_return_False(self): class TestDatasetForm(GeoNodeBaseTestSupport): def setUp(self) -> None: self.user = get_user_model().objects.get(username='admin') + self.user2 = get_user_model().objects.get_or_create(username='svenzwei') + self.dataset = create_single_dataset("my_single_layer", owner=self.user) self.sut = DatasetForm self.time_form = DatasetTimeSerieForm @@ -2020,6 +2023,36 @@ def test_dataset_time_form_should_raise_error_if_invalid_payload(self): self.assertTrue('presentation' in form.errors) self.assertEqual("Select a valid choice. INVALID_PRESENTATION_VALUE is not one of the available choices.", form.errors['presentation'][0]) + def test_resource_form_is_valid_single_user_contact_role(self): + """ test if passing a single user to a contact role form is working + """ + for cr in Roles.get_multivalue_ones(): + form = self.sut(instance=self.dataset, data={ + "owner": self.dataset.owner.id, + cr.name: self.user.username, + "title": "layer_title", + "date": "2022-01-24 16:38 pm", + "date_type": "creation", + "language": "eng", + "extra_metadata": '[{"id": 1, "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]' + }) + self.assertTrue(form.is_valid()) + + def test_resource_form_is_valid_multiple_user_contact_role_as_queryset(self): + """ test if passing a single user to a contact role form is working + """ + for cr in Roles.get_multivalue_ones(): + form = self.sut(instance=self.dataset, data={ + "owner": self.dataset.owner.id, + "processor": get_user_model().objects.filter(username__in=["svenzwei", "admin"]), + "title": "layer_title", + "date": "2022-01-24 16:38 pm", + "date_type": "creation", + "language": "eng", + "extra_metadata": '[{"id": 1, "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]' + }) + self.assertTrue(form.is_valid()) + class SetLayersPermissionsCommand(GeoNodeBaseTestSupport): ''' @@ -2154,11 +2187,11 @@ def _create_arguments(self, perms_type, mode='set'): args = [] username = get_user_model().objects.exclude(username='admin').exclude(username='AnonymousUser').first().username opts = { - "permission": perms_type, - "users": [username], - "resources": str(dataset.id), - "delete": True if mode == 'unset' else False - } + "permission": perms_type, + "users": [username], + "resources": str(dataset.id), + "delete": True if mode == 'unset' else False + } return dataset, args, username, opts From 9f707ae82d2862fa6c5fac05c351ee3558c423f9 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 16 Jan 2023 12:40:44 +0100 Subject: [PATCH 16/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/tests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/geonode/base/tests.py b/geonode/base/tests.py index 0d13263a376..6c2f6a59659 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -155,7 +155,7 @@ def test_add_missing_metadata_author_or_poc(self): Test that calling add_missing_metadata_author_or_poc resource method sets a missing metadata_author and/or point of contact (poc) to resource owner """ - user = get_user_model().objects.create(username='zlatan_i') + user = get_user_model().objects.get_or_create(username='zlatan_i') self.rb.owner = user self.rb.add_missing_metadata_author_or_poc() self.assertTrue('zlatan_i' in [author.username for author in self.rb.metadata_author]) @@ -169,7 +169,7 @@ class TestCreationOfContactRolesByDifferentInputTypes(ThumbnailTests): """ def test_set_contact_role_as_people_profile(self): - user = get_user_model().objects.create(username='zlatan_i') + user = get_user_model().objects.get_or_create(username='zlatan_i') self.rb.owner = user self.rb.metadata_author = user @@ -199,8 +199,8 @@ def test_set_contact_role_as_people_profile(self): """ def test_set_contact_role_as_list_of_people(self): - user = get_user_model().objects.create(username='zlatan_i') - user2 = get_user_model().objects.create(username='zlatan_i') + user = get_user_model().objects.get_or_create(username='zlatan_i') + user2 = get_user_model().objects.get_or_create(username='zlatan_i') profile_list = [user, user2] @@ -232,8 +232,8 @@ def test_set_contact_role_as_list_of_people(self): """ def test_set_contact_role_as_queryset(self): - user = get_user_model().objects.create(username='zlatan_i') - get_user_model().objects.create(username='zlatan_i') + user = get_user_model().objects.get_or_create(username='zlatan_i') + get_user_model().objects.get_or_create(username='zlatan_i') query = get_user_model().objects.filter(username__in=["zlatan_i", "sven_z"]) From eb29c859d6d70672601e7d86a787bbc550bf390d Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 16 Jan 2023 14:40:09 +0100 Subject: [PATCH 17/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 5 +++-- geonode/base/tests.py | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index 3bbe609a177..e6dcf48bef1 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1903,9 +1903,10 @@ def __create_role__(resource, role: str, user_profile: settings.AUTH_USER_MODEL) return [__create_role__(self, role, user) for user in user_profile] elif isinstance(user_profile, get_user_model()): ContactRole.objects.filter(role=role, resource=self).delete() - return [__create_role__(self, role, user_profile)] + return __create_role__(self, role, user_profile) elif isinstance(user_profile, list) and all(isinstance(x, get_user_model()) for x in user_profile): - return [__create_role__(self, role, user_profile)] + ContactRole.objects.filter(role=role, resource=self).delete() + return [__create_role__(self, role, profile) for profile in user_profile] else: logger.error(f"Bad profile format for role: {role} ...") diff --git a/geonode/base/tests.py b/geonode/base/tests.py index 6c2f6a59659..2af4e3aa43b 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -155,7 +155,8 @@ def test_add_missing_metadata_author_or_poc(self): Test that calling add_missing_metadata_author_or_poc resource method sets a missing metadata_author and/or point of contact (poc) to resource owner """ - user = get_user_model().objects.get_or_create(username='zlatan_i') + user, _ = get_user_model().objects.get_or_create(username='zlatan_i') + self.rb.owner = user self.rb.add_missing_metadata_author_or_poc() self.assertTrue('zlatan_i' in [author.username for author in self.rb.metadata_author]) @@ -169,7 +170,7 @@ class TestCreationOfContactRolesByDifferentInputTypes(ThumbnailTests): """ def test_set_contact_role_as_people_profile(self): - user = get_user_model().objects.get_or_create(username='zlatan_i') + user, _ = get_user_model().objects.get_or_create(username='zlatan_i') self.rb.owner = user self.rb.metadata_author = user @@ -199,8 +200,8 @@ def test_set_contact_role_as_people_profile(self): """ def test_set_contact_role_as_list_of_people(self): - user = get_user_model().objects.get_or_create(username='zlatan_i') - user2 = get_user_model().objects.get_or_create(username='zlatan_i') + user, _ = get_user_model().objects.get_or_create(username='zlatan_i') + user2, _ = get_user_model().objects.get_or_create(username='sven_z') profile_list = [user, user2] @@ -232,8 +233,8 @@ def test_set_contact_role_as_list_of_people(self): """ def test_set_contact_role_as_queryset(self): - user = get_user_model().objects.get_or_create(username='zlatan_i') - get_user_model().objects.get_or_create(username='zlatan_i') + user, _ = get_user_model().objects.get_or_create(username='zlatan_i') + user2, _ = get_user_model().objects.get_or_create(username='sven_z') query = get_user_model().objects.filter(username__in=["zlatan_i", "sven_z"]) From 8d968b0bfd74aa64ddf9d957a6479616c3292f4b Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Tue, 17 Jan 2023 09:38:43 +0100 Subject: [PATCH 18/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/geoapps/views.py | 1 + geonode/resource/utils.py | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index 7cf78c61ccf..203384e9b52 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -316,6 +316,7 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T instance=geoapp_obj, keywords=new_keywords, regions=new_regions, + vals=_vals, notify=True, extra_metadata=json.loads(extra_metadata) ) diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index d589be93ef3..e3e32837fb2 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -54,6 +54,7 @@ DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP) from ..people.utils import get_valid_user +from geonode.people import Roles from ..layers.utils import resolve_regions from ..layers.metadata import convert_keyword @@ -136,7 +137,6 @@ def _set_tkeyword(self, tkeyword): def update_resource(instance: ResourceBase, xml_file: str = None, regions: list = [], keywords: list = [], vals: dict = {}, extra_metadata: list = []): - if xml_file: instance.metadata_xml = open(xml_file).read() @@ -177,8 +177,8 @@ def update_resource(instance: ResourceBase, xml_file: str = None, regions: list else: defaults[key] = value - poc = defaults.pop('poc', None) - metadata_author = defaults.pop('metadata_author', None) + # get contact roles from instance defaults object + contact_roles = {contact_role.name: defaults.pop(contact_role.name, None) for contact_role in Roles.get_multivalue_ones()} to_update = {} for _key in ('name', ): @@ -238,6 +238,9 @@ def update_resource(instance: ResourceBase, xml_file: str = None, regions: list _default_ows_url = urljoin(ogc_settings.PUBLIC_LOCATION, 'ows') to_update['ows_url'] = defaults.pop('ows_url', getattr(instance, 'ows_url', None)) or _default_ows_url + # update contact roles in instance + [instance.__setattr__(contact_role_name, contact_role_value) for contact_role_name, contact_role_value in contact_roles.items()] + to_update.update(defaults) try: ResourceBase.objects.filter(id=instance.resourcebase_ptr.id).update(**defaults) @@ -269,10 +272,6 @@ def update_resource(instance: ResourceBase, xml_file: str = None, regions: list # Refresh from DB instance.refresh_from_db() - if poc: - instance.poc = poc - if metadata_author: - instance.metadata_author = metadata_author if extra_metadata: instance.metadata.all().delete() From a51210eb2ad4f96afb8bab72d3b1d5480d6d1088 Mon Sep 17 00:00:00 2001 From: ahmdthr Date: Thu, 19 Jan 2023 01:43:58 +0100 Subject: [PATCH 19/49] Updates for PATCH for multiple contacts along with tests for each role. --- geonode/base/api/serializers.py | 3 + geonode/layers/api/tests.py | 160 ++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index dc98ab30fa5..460d24a56c0 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -371,6 +371,9 @@ def get_attribute(self, instance): def to_representation(self, value): return [UserSerializer(embed=True, many=False).to_representation(v) for v in value] + def to_internal_value(self, value): + return get_user_model().objects.filter(pk__in=[val.get('id') for val in value]) + class DataBlobField(DynamicRelationField): diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index e17661c1de1..e610941b849 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -312,3 +312,163 @@ def test_layer_replace_should_work(self, _validate_input_source): layer.refresh_from_db() # evaluate that the number of available layer is not changed self.assertEqual(Dataset.objects.count(), cnt) + + def test_patch_point_of_contact(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'poc': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.poc), len(user_ids)) + self.assertTrue(all(poc.pk in user_ids for poc in layer.poc)) + + def test_patch_metadata_author(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'metadata_author': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.metadata_author), len(user_ids)) + self.assertTrue(all(metadata_author.pk in user_ids for metadata_author in layer.metadata_author)) + + def test_patch_processor(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'processor': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.processor), len(user_ids)) + self.assertTrue(all(processor.pk in user_ids for processor in layer.processor)) + + def test_patch_publisher(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'publisher': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.publisher), len(user_ids)) + self.assertTrue(all(publisher.pk in user_ids for publisher in layer.publisher)) + + def test_patch_custodian(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'custodian': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.custodian), len(user_ids)) + self.assertTrue(all(custodian.pk in user_ids for custodian in layer.custodian)) + + def test_patch_distributor(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'distributor': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.distributor), len(user_ids)) + self.assertTrue(all(distributor.pk in user_ids for distributor in layer.distributor)) + + def test_patch_resource_user(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'resource_user': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.resource_user), len(user_ids)) + self.assertTrue(all(resource_user.pk in user_ids for resource_user in layer.resource_user)) + + def test_patch_resource_provider(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'resource_provider': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.resource_provider), len(user_ids)) + self.assertTrue(all(resource_provider.pk in user_ids for resource_provider in layer.resource_provider)) + + def test_patch_originator(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'originator': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.originator), len(user_ids)) + self.assertTrue(all(originator.pk in user_ids for originator in layer.originator)) + + def test_patch_principal_investigator(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username='ninja') + get_user_model().objects.get_or_create(username='turtle') + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {'principal_investigator': [{'id' : uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format='json') + + self.assertEqual(200, response.status_code) + self.assertEqual(len(layer.principal_investigator), len(user_ids)) + self.assertTrue(all(principal_investigator.pk in user_ids for principal_investigator in layer.principal_investigator)) From 69c814a1dee88b4040ded480c9140c1ba743cf80 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 19 Jan 2023 09:29:57 +0100 Subject: [PATCH 20/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/layers/api/tests.py | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index 35fb8971925..09745d61196 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -325,7 +325,7 @@ def test_patch_point_of_contact(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'poc': [{'id' : uid} for uid in user_ids]} + patch_data = {'poc': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') self.assertEqual(200, response.status_code) @@ -341,9 +341,9 @@ def test_patch_metadata_author(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'metadata_author': [{'id' : uid} for uid in user_ids]} + patch_data = {'metadata_author': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.metadata_author), len(user_ids)) self.assertTrue(all(metadata_author.pk in user_ids for metadata_author in layer.metadata_author)) @@ -357,9 +357,9 @@ def test_patch_processor(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'processor': [{'id' : uid} for uid in user_ids]} + patch_data = {'processor': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.processor), len(user_ids)) self.assertTrue(all(processor.pk in user_ids for processor in layer.processor)) @@ -373,9 +373,9 @@ def test_patch_publisher(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'publisher': [{'id' : uid} for uid in user_ids]} + patch_data = {'publisher': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.publisher), len(user_ids)) self.assertTrue(all(publisher.pk in user_ids for publisher in layer.publisher)) @@ -389,9 +389,9 @@ def test_patch_custodian(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'custodian': [{'id' : uid} for uid in user_ids]} + patch_data = {'custodian': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.custodian), len(user_ids)) self.assertTrue(all(custodian.pk in user_ids for custodian in layer.custodian)) @@ -405,9 +405,9 @@ def test_patch_distributor(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'distributor': [{'id' : uid} for uid in user_ids]} + patch_data = {'distributor': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.distributor), len(user_ids)) self.assertTrue(all(distributor.pk in user_ids for distributor in layer.distributor)) @@ -421,9 +421,9 @@ def test_patch_resource_user(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'resource_user': [{'id' : uid} for uid in user_ids]} + patch_data = {'resource_user': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.resource_user), len(user_ids)) self.assertTrue(all(resource_user.pk in user_ids for resource_user in layer.resource_user)) @@ -437,9 +437,9 @@ def test_patch_resource_provider(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'resource_provider': [{'id' : uid} for uid in user_ids]} + patch_data = {'resource_provider': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.resource_provider), len(user_ids)) self.assertTrue(all(resource_provider.pk in user_ids for resource_provider in layer.resource_provider)) @@ -453,9 +453,9 @@ def test_patch_originator(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'originator': [{'id' : uid} for uid in user_ids]} + patch_data = {'originator': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.originator), len(user_ids)) self.assertTrue(all(originator.pk in user_ids for originator in layer.originator)) @@ -469,9 +469,9 @@ def test_patch_principal_investigator(self): get_user_model().objects.get_or_create(username='turtle') users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] - patch_data = {'principal_investigator': [{'id' : uid} for uid in user_ids]} + patch_data = {'principal_investigator': [{'id': uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format='json') - + self.assertEqual(200, response.status_code) self.assertEqual(len(layer.principal_investigator), len(user_ids)) self.assertTrue(all(principal_investigator.pk in user_ids for principal_investigator in layer.principal_investigator)) From 36c27ef5f7e7617fd9d98d4be9b2861c5416c1bc Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Wed, 25 Jan 2023 10:42:11 +0100 Subject: [PATCH 21/49] Fixes GeoNode#10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/resource/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 1f89c17d039..35efff19a1e 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -51,7 +51,6 @@ from ..documents.models import Document from ..documents.enumerations import DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP from ..people.utils import get_valid_user -from geonode.people import Roles from ..layers.utils import resolve_regions from ..layers.metadata import convert_keyword From 87a704b82bcaf9ff2c6e77d0c75e79eef9e232ce Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Wed, 22 Feb 2023 08:59:24 +0100 Subject: [PATCH 22/49] merged correct --- geonode/people/models.py | 9 -------- geonode/security/tests.py | 44 --------------------------------------- tasks.py | 20 ------------------ 3 files changed, 73 deletions(-) diff --git a/geonode/people/models.py b/geonode/people/models.py index 8f9b43f30df..37d4c21631c 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -74,15 +74,6 @@ class Profile(AbstractUser): null=True, help_text=_("role or position of the responsible person"), ) -<<<<<<< HEAD - # orcid = models.CharField( - # _('orcid'), - # max_length=20, - # blank=True, - # null=True, - # help_text=_('Uniquely identifies an individual or legal entity, according to various schemes. (e.g. 0000-0001-5000-0007)')) -======= ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f voice = models.CharField( _("Voice"), max_length=255, diff --git a/geonode/security/tests.py b/geonode/security/tests.py index f7a5fecf8a8..a73d360a889 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -351,8 +351,6 @@ def test_set_bulk_permissions(self): """Test that after restrict view permissions on two layers bobby is unable to see them""" -<<<<<<< HEAD -======= rules_count = 0 if check_ogc_backend(geoserver.BACKEND_PACKAGE): delete_all_geofence_rules() @@ -360,7 +358,6 @@ def test_set_bulk_permissions(self): rules_count = geofence.get_rules_count() self.assertEqual(rules_count, 0) ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f layers = Dataset.objects.all()[:2].values_list("id", flat=True) layers_id = [str(x) for x in layers] test_perm_dataset = Dataset.objects.get(id=layers[0]) @@ -389,18 +386,12 @@ def test_set_bulk_permissions(self): resp = self.client.get(self.list_url) self.assertGreaterEqual(len(self.deserialize(resp)["objects"]), 6) -<<<<<<< HEAD - perms = get_users_with_perms(test_perm_dataset) - _log(f"3. perms: {perms} ") - sync_geofence_with_guardian(test_perm_dataset, perms, user="bobby") -======= # perms = get_users_with_perms(test_perm_dataset) # _log(f"3. perms: {perms} ") # batch = AutoPriorityBatch(get_first_available_priority(), f'test batch for {test_perm_dataset}') # for u, p in perms.items(): # create_geofence_rules(test_perm_dataset, p, user=u, batch=batch) # geofence.run_batch(batch) ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f # Check GeoFence Rules have been correctly created rules_count = geofence.get_rules_count() @@ -416,14 +407,9 @@ def test_set_bulk_permissions(self): user = settings.OGC_SERVER["default"]["USER"] passwd = settings.OGC_SERVER["default"]["PASSWORD"] -<<<<<<< HEAD - r = requests.get(f"{url}gwc/rest/seed/{test_perm_dataset.alternate}.json", auth=HTTPBasicAuth(user, passwd)) - self.assertEqual(r.status_code, 400) -======= test_url = f"{url}gwc/rest/seed/{test_perm_dataset.alternate}.json" r = requests.get(test_url, auth=HTTPBasicAuth(user, passwd)) self.assertEqual(r.status_code, 400, f"GWC error for user: {user} URL: {test_url}\n{r.text}") ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f rules_count = 0 if check_ogc_backend(geoserver.BACKEND_PACKAGE): @@ -608,14 +594,8 @@ def test_perm_specs_synchronization(self): layer = Dataset.objects.first() # grab bobby bobby = get_user_model().objects.get(username="bobby") -<<<<<<< HEAD - gf_services = _get_gf_services(layer, layer.get_all_level_info()) - _, _, _disable_dataset_cache, _, _, _ = get_user_geolimits(layer, None, None, gf_services) - filters, formats = _get_gwc_filters_and_formats([_disable_dataset_cache]) -======= _disable_dataset_cache = has_geolimits(layer, None, None) filters, formats = _get_gwc_filters_and_formats(_disable_dataset_cache) ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertListEqual(filters, [{"styleParameterFilter": {"STYLES": ""}}]) self.assertListEqual( formats, @@ -647,22 +627,14 @@ def test_perm_specs_synchronization(self): rules_count = geofence.get_rules_count() self.assertEqual(rules_count, 8) -<<<<<<< HEAD - rules_objs = get_geofence_rules(entries=8) -======= rules_objs = geofence.get_rules() ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertEqual(len(rules_objs["rules"]), 8) # Order is important _limit_rule_position = -1 for cnt, rule in enumerate(rules_objs["rules"]): if rule["service"] is None and rule["userName"] == "bobby": self.assertEqual(rule["userName"], "bobby") -<<<<<<< HEAD - self.assertEqual(rule["workspace"], "CA") -======= self.assertEqual(rule["workspace"], "geonode") ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertEqual(rule["layer"], "CA") self.assertEqual(rule["access"], "LIMIT") @@ -695,11 +667,7 @@ def test_perm_specs_synchronization(self): rules_count = geofence.get_rules_count() self.assertEqual(rules_count, 6) -<<<<<<< HEAD - rules_objs = get_geofence_rules(entries=6) -======= rules_objs = geofence.get_rules() ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertEqual(len(rules_objs["rules"]), 6) # Order is important _limit_rule_position = -1 @@ -707,11 +675,7 @@ def test_perm_specs_synchronization(self): if rule["roleName"] == "ROLE_BAR": if rule["service"] is None: self.assertEqual(rule["userName"], None) -<<<<<<< HEAD - self.assertEqual(rule["workspace"], "CA") -======= self.assertEqual(rule["workspace"], "geonode") ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertEqual(rule["layer"], "CA") self.assertEqual(rule["access"], "LIMIT") @@ -747,11 +711,7 @@ def test_perm_specs_synchronization(self): if rule["service"] is None: self.assertEqual(rule["service"], None) self.assertEqual(rule["userName"], None) -<<<<<<< HEAD - self.assertEqual(rule["workspace"], "CA") -======= self.assertEqual(rule["workspace"], "geonode") ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f self.assertEqual(rule["layer"], "CA") self.assertEqual(rule["access"], "LIMIT") @@ -1311,11 +1271,7 @@ def test_perms_info(self): # 7. change_dataset_style def test_not_superuser_permissions(self): -<<<<<<< HEAD - geofence_rules_count = 0 -======= rules_count = 0 ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f if check_ogc_backend(geoserver.BACKEND_PACKAGE): delete_all_geofence_rules() # Reset GeoFence Rules diff --git a/tasks.py b/tasks.py index 40a621b6e5c..64cdcf3e6ec 100755 --- a/tasks.py +++ b/tasks.py @@ -129,23 +129,6 @@ def update(ctx): allowed_hosts = [str(c) for c in current_allowed] + ['"geonode"', '"django"'] ctx.run( -<<<<<<< HEAD - "echo export GEONODE_INSPIRE_URL=\ -{inspire_download_url} >> {override_fn}".format( - **envs - ), - pty=True, - ) - ctx.run( - "echo export GEONODE_AGROVOC_URL=\ -{agrovoc_download_url} >> {override_fn}".format( - **envs - ), - pty=True, - ) - ctx.run( -======= ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f "echo export DJANGO_SETTINGS_MODULE=\ {local_settings} >> {override_fn}".format( **envs @@ -485,7 +468,6 @@ def collectmetrics(ctx): def initialized(ctx): print("**************************init file********************************") ctx.run("date > /mnt/volumes/statics/geonode_init.lock") -<<<<<<< HEAD @task @@ -514,8 +496,6 @@ def initzalf(ctx): --settings={_localsettings()}", pty=True, ) -======= ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f def _docker_host_ip(): From 2cc81ef3752e1c12f0fbe0553d9a08783a9c0367 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Wed, 22 Feb 2023 09:14:59 +0100 Subject: [PATCH 23/49] merged correct --- geonode/security/tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/geonode/security/tests.py b/geonode/security/tests.py index a73d360a889..4fea502ffd8 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -71,12 +71,9 @@ delete_all_geofence_rules, sync_resources_with_guardian, _get_gwc_filters_and_formats, -<<<<<<< HEAD -======= has_geolimits, create_geofence_rules, delete_geofence_rules_for_layer, ->>>>>>> 0e89afe806f359a1a0b700c233f292999249af5f ) From 00355cfa78d8f7421c99285754106965953f2999 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Thu, 30 Mar 2023 14:33:20 +0200 Subject: [PATCH 24/49] WIP bonares metadata schema --- geonode/base/api/fields.py | 96 ++ geonode/base/api/serializers.py | 216 +++-- geonode/base/api/urls.py | 4 + geonode/base/api/views.py | 70 +- geonode/base/fixtures/initial_data.json | 923 +++++++++++++++++-- geonode/base/fixtures/zalfinit.json | 65 -- geonode/base/forms.py | 38 +- geonode/base/models.py | 169 ++-- geonode/layers/templates/layouts/panels.html | 23 +- geonode/settings.py | 81 +- 10 files changed, 1247 insertions(+), 438 deletions(-) create mode 100644 geonode/base/api/fields.py delete mode 100644 geonode/base/fixtures/zalfinit.json diff --git a/geonode/base/api/fields.py b/geonode/base/api/fields.py new file mode 100644 index 00000000000..1339d0bc9f7 --- /dev/null +++ b/geonode/base/api/fields.py @@ -0,0 +1,96 @@ +######################################################################### +# +# Copyright (C) 2016 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +import json + +from django.core.exceptions import ValidationError + +from rest_framework.exceptions import ParseError +from dynamic_rest.fields.fields import DynamicRelationField + +from geonode.base.models import ( + RelatedIdentifierType, + RelationType, + RelatedIdentifier, + FundingReference, + Funder + ) + +class RelatedIdentifierDynamicRelationField(DynamicRelationField): + + def to_internal_value_single(self, data, serializer): + try: + rit = RelatedIdentifierType.objects.get(**data['related_identifier_type']) + rt = RelationType.objects.get(**data['relation_type']) + RelatedIdentifier.objects.get_or_create(related_identifier=data['related_identifier'], + related_identifier_type=rit, + relation_type=rt)[0].save() + r = RelatedIdentifier.objects.get(related_identifier=data['related_identifier'], related_identifier_type=rit, relation_type=rt) + except TypeError: + raise ParseError(detail="Could not convert related_identifier to internal object ...", code=400) + return r + +class FundersDynamicRelationField(DynamicRelationField): + + def to_internal_value_single(self, data, serializer): + try: + funding_reference = FundingReference.objects.get(**data['funding_reference']) + data['funding_reference'] = funding_reference + except TypeError: + raise ParseError(detail="Missing funding_reference object in funders ...", code=400) + try: + funder = Funder.objects.get_or_create(**data) + except TypeError: + raise ParseError(detail="Could not convert related_identifier to internal object ...", code=400) + return funder + + +class ComplexDynamicRelationField(DynamicRelationField): + + def to_internal_value_single(self, data, serializer): + """Overwrite of DynamicRelationField implementation to handle complex data structure initialization + + Args: + data (Union[str, Dict]}): serialized or deserialized data from http calls (POST, GET ...), + if content-type application/json is used, data shows up as dict + serializer (DynamicModelSerializer): Serializer for the given data + + Raises: + ValidationError: raised when requested data does not exist + + django.db.models.QuerySet: return QuerySet object of the request or set data + """ + related_model = serializer.Meta.model + try: + if isinstance(data, str): + data = json.loads(data) + except ValueError: + return super().to_internal_value_single(data, serializer) + + if isinstance(data, dict): + try: + if hasattr(serializer, "many") and serializer.many is True: + return [serializer.get_model().objects.get(**d) for d in data] + return serializer.get_model().objects.get(**data) + except related_model.DoesNotExist: + raise ValidationError( + "Invalid value for '%s': %s object with ID=%s not found" + % (self.field_name, related_model.__name__, data) + ) + else: + return super().to_internal_value_single(data, serializer) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 8c96cbe03aa..6d80dc9aaf2 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -49,15 +49,15 @@ ThesaurusKeyword, ThesaurusKeywordLabel, ExtraMetadata, - # ZALF EXTRAS - AlternateType, - DescriptionType, - FundingReference, + RelatedIdentifierType, + RelationType, RelatedIdentifier, - ## + FundingReference, + RelatedProject, + Funder, ) from geonode.groups.models import GroupCategory, GroupProfile - +from geonode.base.api.fields import ComplexDynamicRelationField, RelatedIdentifierDynamicRelationField, FundersDynamicRelationField from geonode.utils import build_absolute_uri from geonode.security.utils import get_resources_with_perms from geonode.resource.models import ExecutionRequest @@ -66,7 +66,6 @@ logger = logging.getLogger(__name__) - class BaseDynamicModelSerializer(DynamicModelSerializer): def to_representation(self, instance): data = super().to_representation(instance) @@ -160,6 +159,7 @@ def to_representation(self, value): return {"name": value.name, "slug": value.slug} + class _ThesaurusKeywordSerializerMixIn: def to_representation(self, value): _i18n = {} @@ -183,7 +183,7 @@ class Meta: model = ThesaurusKeyword name = "ThesaurusKeyword" fields = ("alt_label",) - + class SimpleRegionSerializer(DynamicModelSerializer): class Meta: @@ -192,6 +192,53 @@ class Meta: fields = ("code", "name") +class SimpleRelatedIdentifierType(DynamicModelSerializer): + class Meta: + model = RelatedIdentifierType + name = "RelatedIdentifierType" + fields = ("label", "description") + + +class SimpleRelationType(DynamicModelSerializer): + class Meta: + model = RelationType + name = "RelationType" + fields = ("label", "description") + + +class SimpleRelatedIdentifierSerializer(DynamicModelSerializer): + class Meta: + model = RelatedIdentifier + name = "RelatedIdentifier" + fields = ("related_identifier", "related_identifier_type", "relation_type") + + related_identifier_type = DynamicRelationField(SimpleRelatedIdentifierType, embed=True, many=False) + relation_type = DynamicRelationField(SimpleRelationType, embed=True, many=False) + + +class FundingReferenceSerializer(DynamicModelSerializer): + class Meta: + model = FundingReference + name = "FundingReference" + fields = ("funder_name", "funder_identifier", "funder_identifier_type") + + +class SimpleFunderSerializer(DynamicModelSerializer): + class Meta: + model = Funder + name = "Funder" + fields = ("award_title", "award_uri", "funding_reference", "award_number") + + funding_reference = DynamicRelationField(FundingReferenceSerializer, embed=True, many=False) + + +class SimpleRelatedProjectSerializer(DynamicModelSerializer): + class Meta: + model = RelatedProject + name = "RelatedProject" + fields = ("label","display_name") + + class SimpleTopicCategorySerializer(DynamicModelSerializer): class Meta: model = TopicCategory @@ -414,47 +461,6 @@ def to_representation(self, instance): return data -############################# -# ZALF ADDITIONS SERIALIZER # -############################# -class AlternateTypeSerializer(DynamicModelSerializer): - class Meta: - model = AlternateType - name = "AlternateType" - fields = ("alternate_type",) - - -class DescriptionTypeSerializer(DynamicModelSerializer): - class Meta: - model = DescriptionType - name = "DescriptionType" - fields = ("description_type",) - - -class FundingReferenceSerializer(DynamicModelSerializer): - class Meta: - model = FundingReference - name = "FundingReference" - fields = ( - "funder_name", - "funder_identifier", - "funder_identifier_type", - "award_number", - "award_uri", - "award_title", - ) - - -class RelatedIdentifierSerializer(DynamicModelSerializer): - class Meta: - model = RelatedIdentifier - name = "RelatedIdentifier" - fields = ("related_identifier",) - - -## - - class ResourceBaseSerializer( ResourceBaseToRepresentationSerializerMixin, BaseDynamicModelSerializer, @@ -470,10 +476,31 @@ def __init__(self, *args, **kwargs): self.fields["owner"] = DynamicRelationField( UserSerializer, embed=True, many=False, read_only=True, required=False ) + self.fields["poc"] = ContactRoleField("poc", read_only=True) self.fields["metadata_author"] = ContactRoleField("metadata_author", read_only=True) self.fields["title"] = serializers.CharField() + self.fields["title_translated"] = serializers.CharField() + self.fields["abstract"] = serializers.CharField(required=False) + self.fields["abstract_translated"] = serializers.CharField(required=False) + + self.fields["subtitle"] = serializers.CharField(required=False) + self.fields["method_description"] = serializers.CharField(required=False) + self.fields["series_information"] = serializers.CharField(required=False) + self.fields["table_of_content"] = serializers.CharField(required=False) + self.fields["technical_info"] = serializers.CharField(required=False) + self.fields["other_description"] = serializers.CharField(required=False) + + self.fields["related_identifier"] = RelatedIdentifierDynamicRelationField( + SimpleRelatedIdentifierSerializer, embed=True, many=True) + self.fields["funders"] = FundersDynamicRelationField( + SimpleFunderSerializer, embed=True, many=True) + self.fields["related_projects"] = ComplexDynamicRelationField( + SimpleRelatedProjectSerializer, embed=True, many=True) + + + self.fields["attribution"] = serializers.CharField(required=False) self.fields["doi"] = serializers.CharField(required=False) self.fields["alternate"] = serializers.CharField(read_only=True) @@ -513,16 +540,19 @@ def __init__(self, *args, **kwargs): self.fields["embed_url"] = EmbedUrlField(required=False) self.fields["thumbnail_url"] = ThumbnailUrlField(read_only=True) - self.fields["keywords"] = DynamicRelationField(SimpleHierarchicalKeywordSerializer, embed=False, many=True) - self.fields["tkeywords"] = DynamicRelationField(SimpleThesaurusKeywordSerializer, embed=False, many=True) + self.fields["keywords"] = ComplexDynamicRelationField(SimpleHierarchicalKeywordSerializer, embed=False, many=True) + self.fields["tkeywords"] = ComplexDynamicRelationField(SimpleThesaurusKeywordSerializer, embed=False, many=True) self.fields["regions"] = DynamicRelationField(SimpleRegionSerializer, embed=True, many=True, read_only=True) - self.fields["category"] = DynamicRelationField(SimpleTopicCategorySerializer, embed=True, many=False) - self.fields["restriction_code_type"] = DynamicRelationField( - RestrictionCodeTypeSerializer, embed=True, many=False + self.fields["category"] = ComplexDynamicRelationField(SimpleTopicCategorySerializer, embed=True, many=False) + self.fields["restriction_code_type"] = ComplexDynamicRelationField( + RestrictionCodeTypeSerializer, embed=True, many=True ) - self.fields["license"] = DynamicRelationField(LicenseSerializer, embed=True, many=False) - self.fields["spatial_representation_type"] = DynamicRelationField( - SpatialRepresentationTypeSerializer, embed=True, many=False + self.fields["use_constrains"] = ComplexDynamicRelationField( + RestrictionCodeTypeSerializer, embed=True, many=True + ) + self.fields["license"] = ComplexDynamicRelationField(LicenseSerializer, embed=True, many=False) + self.fields["spatial_representation_type"] = ComplexDynamicRelationField( + SpatialRepresentationTypeSerializer, embed=True, many=False ) self.fields["blob"] = serializers.JSONField(required=False, write_only=True) self.fields["is_copyable"] = serializers.BooleanField(read_only=True) @@ -531,42 +561,13 @@ def __init__(self, *args, **kwargs): self.fields["favorite"] = FavoriteField(read_only=True) - ################## - # ZALF ADDITIONS # - ################## - - self.fields["title_de"] = serializers.CharField(required=False) - self.fields["abstract_de"] = serializers.CharField(required=False) - self.fields["alternate_type"] = AlternateTypeSerializer(required=False) - self.fields["description_type"] = DescriptionTypeSerializer(required=False) - self.fields["funding_reference"] = FundingReferenceSerializer(required=False) - self.fields["related_identifier"] = RelatedIdentifierSerializer(required=False) - self.fields["use_contraints"] = serializers.CharField(read_only=True) - self.fields["parent_ressource"] = DynamicRelationField( - UserSerializer, embed=True, many=False, read_only=True, required=False - ) - - ## - - metadata = DynamicRelationField(ExtraMetadataSerializer, embed=False, many=True, deferred=True) + metadata = ComplexDynamicRelationField(ExtraMetadataSerializer, embed=False, many=True, deferred=True) class Meta: model = ResourceBase name = "resource" view_name = "base-resources-list" fields = ( - ################## - # ZALF ADDITIONS # - ################## - "title_de", - "abstract_de", - "alternate_type", - "description_type", - "funding_reference", - "related_identifier", - "use_contraints", - "parent_ressource", - # "pk", "uuid", "resource_type", @@ -580,9 +581,19 @@ class Meta: "regions", "category", "title", + "title_translated", "abstract", + "abstract_translated", "attribution", "alternate", + "subtitle", + "method_description", + "series_information", + "table_of_content", + "technical_info", + "other_description", + "related_identifier", + "funders", "doi", "bbox_polygon", "ll_bbox_polygon", @@ -593,6 +604,7 @@ class Meta: "purpose", "maintenance_frequency", "restriction_code_type", + "use_constrains", "constraints_other", "license", "language", @@ -770,7 +782,37 @@ class Meta: count_type = "category" view_name = "categories-list" fields = "__all__" + + +class RelationTypeSerializer(DynamicModelSerializer): + class Meta: + name = "relationtypes" + model = RelationType + count_type = "relationtype" + fields = "__all__" + +class RelatedIdentifierTypeSerializer(DynamicModelSerializer): + class Meta: + name = "relatedidentifiertypes" + model = RelatedIdentifierType + count_type = "relatedidentifiertype" + fields = "__all__" + + +class FundingReferenceSerializer(DynamicModelSerializer): + class Meta: + name = "fundingreferences" + model = FundingReference + count_type = "fundingreferences" + fields = "__all__" + +class RelatedProjectSerializer(DynamicModelSerializer): + class Meta: + name = "relatedprojects" + model = RelatedProject + count_type = "relatedprojects" + fields = "__all__" class OwnerSerializer(BaseResourceCountSerializer): class Meta: diff --git a/geonode/base/api/urls.py b/geonode/base/api/urls.py index b29044ef6fc..150eca56a32 100644 --- a/geonode/base/api/urls.py +++ b/geonode/base/api/urls.py @@ -25,6 +25,10 @@ router.register(r"resources", views.ResourceBaseViewSet, "base-resources") router.register(r"owners", views.OwnerViewSet, "owners") router.register(r"categories", views.TopicCategoryViewSet, "categories") +router.register(r"relationtypes", views.RelationTypeViewSet, "relationtypes") +router.register(r"relatedidentifiertypes", views.RelatedIdentifierTypeViewSet, "relatedidentifiertypes") +router.register(r"fundingreferences", views.FundingReferenceViewSet, "fundingreferences") +router.register(r"relatedprojects", views.RelatedProjectViewSet, "relatedprojects") router.register(r"keywords", views.HierarchicalKeywordViewSet, "keywords") router.register(r"tkeywords", views.ThesaurusKeywordViewSet, "tkeywords") router.register(r"regions", views.RegionViewSet, "regions") diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index f86933ec49b..15b9f13df92 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -65,7 +65,17 @@ from geonode.thumbs.thumbnails import create_thumbnail from geonode.thumbs.utils import _decode_base64, BASE64_PATTERN from geonode.groups.conf import settings as groups_settings -from geonode.base.models import HierarchicalKeyword, Region, ResourceBase, TopicCategory, ThesaurusKeyword +from geonode.base.models import ( + HierarchicalKeyword, + Region, + ResourceBase, + TopicCategory, + ThesaurusKeyword, + RelationType, + RelatedIdentifierType, + FundingReference, + RelatedProject +) from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter, FacetVisibleResourceFilter, FavoriteFilter from geonode.groups.models import GroupProfile, GroupMember from geonode.people.utils import get_available_users @@ -95,6 +105,10 @@ OwnerSerializer, HierarchicalKeywordSerializer, TopicCategorySerializer, + RelationTypeSerializer, + RelatedIdentifierTypeSerializer, + FundingReferenceSerializer, + RelatedProjectSerializer, RegionSerializer, ThesaurusKeywordSerializer, ExtraMetadataSerializer, @@ -302,6 +316,60 @@ class TopicCategoryViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveMode pagination_class = GeoNodeApiPagination +class RelationTypeViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + API endpoint that lists relationtype. + """ + + permission_classes = [ + AllowAny, + ] + filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter] + queryset = RelationType.objects.all() + serializer_class = RelationTypeSerializer + pagination_class = GeoNodeApiPagination + +class RelatedIdentifierTypeViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + API endpoint that lists relatedidentifiertypes. + """ + + permission_classes = [ + AllowAny, + ] + filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter] + queryset = RelatedIdentifierType.objects.all() + serializer_class = RelatedIdentifierTypeSerializer + pagination_class = GeoNodeApiPagination + +class FundingReferenceViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + API endpoint that lists fundingreference. + """ + + permission_classes = [ + AllowAny, + ] + filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter] + queryset = FundingReference.objects.all() + serializer_class = FundingReferenceSerializer + pagination_class = GeoNodeApiPagination + +class RelatedProjectViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + API endpoint that lists relatedprojects. + """ + + permission_classes = [ + AllowAny, + ] + filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter] + queryset = RelatedProject.objects.all() + serializer_class = RelatedProjectSerializer + pagination_class = GeoNodeApiPagination + + +RelatedProjectSerializer class OwnerViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): """ API endpoint that lists all possible owners. diff --git a/geonode/base/fixtures/initial_data.json b/geonode/base/fixtures/initial_data.json index f025e795601..7bb6869febb 100644 --- a/geonode/base/fixtures/initial_data.json +++ b/geonode/base/fixtures/initial_data.json @@ -278,91 +278,165 @@ "identifier": "video", "description": "scene from a video recording" } - }, - { - "pk": 1, - "model": "base.license", - "fields": { - "identifier": "not_specified", - "name":"Not Specified", - "abbreviation":"", - "description":"The original author did not specify a license.", - "url":"", - "license_text":"Not applicable" - } - }, -{ - "pk": 2, - "model": "base.license", - "fields": { - "identifier":"varied_original", - "name":"Varied / Original", - "abbreviation":"", - "description":"This item is either licensed under multiple licenses. See the item's abstract for more information or contact the distributor.", - "url":"", - "license_text":"Not applicable" - } - }, - { - "pk": 3, + }, + + { + "pk": 1, + "model": "base.license", + "fields": { + "identifier": "not_specified", + "name":"Not Specified", + "abbreviation":"", + "description":"The original author did not specify a license.", + "url":"", + "license_text":"Not applicable" + } + }, + { + "pk": 2, + "model": "base.license", + "fields": { + "identifier":"varied_original", + "name":"Varied / Original", + "abbreviation":"", + "description":"This item is either licensed under multiple licenses. See the item's abstract for more information or contact the distributor.", + "url":"", + "license_text":"Not applicable" + } + }, + { + "pk": 3, + "model": "base.license", + "fields": { + "identifier":"varied_derived", + "name":"Varied / Derived", + "abbreviation":"", + "description":"The constituent parts of this item have different licenses. Go to each part to see license information.", + "url":"", + "license_text":"Not applicable" + } + }, + { + "pk": 4, + "model": "base.license", + "fields": { + "identifier":"public_domain", + "name":"Public Domain", + "abbreviation":"PD", + "description":"Works in the public domain may be used freely without the permission of the former copyright owner.", + "url":"http://www.copyright.gov/help/faq/faq-definitions.html", + "license_text":"The public domain is not a place. A work of authorship is in the “public domain” if it is no longer under copyright protection or if it failed to meet the requirements for copyright protection. Works in the public domain may be used freely without the permission of the former copyright owner." + } + }, + { + "pk": 5, + "model": "base.license", + "fields": { + "identifier":"public_domain_usg", + "name":"Public Domain / USG", + "abbreviation":"PD/USG", + "description":"This project constitutes a work of the United States Government and is not subject to domestic copyright protection under 17 USC § 105.", + "url":"https://raw.githubusercontent.com/state-hiu/cybergis-licenses/master/licenses/PUBLICDOMAIN-LICENSE-RAW.txt", + "license_text":"This project constitutes a work of the United States Government and is not subject to domestic copyright protection under 17 USC § 105." + } + }, + { + "pk": 6, + "model": "base.license", + "fields": { + "identifier":"odbl", + "name":"Open Data Commons Open Database License / OSM", + "abbreviation":"ODbL/OSM", + "description":"You are free to copy, distribute, transmit and adapt our data, as long as you credit OpenStreetMap and its contributors\nIf you alter or build upon our data, you may distribute the result only under the same licence.", + "url":"http://www.openstreetmap.org/copyright", + "license_text":"" + } + }, + { + "pk": 7, + "model": "base.license", + "fields": { + "identifier":"nextview", + "name":"NextView", + "abbreviation":"NV", + "description":"This data is licensed for use by the US Government (USG) under the NextView (NV) license and copyrighted by Digital Globe or GeoEye. The NV license allows the USG to share the imagery and Literal Imagery Derived Products (LIDP) with entities outside the USG when that entity is working directly with the USG, for the USG, or in a manner that is directly beneficial to the USG. The party receiving the data can only use the imagery or LIDP for the original purpose or only as otherwise agreed to by the USG. The party receiving the data cannot share the imagery or LIDP with a third party without express permission from the USG. At no time should this imagery or LIDP be used for other than USG-related purposes and must not be used for commercial gain. The copyright information should be maintained at all times. Your acceptance of these license terms is implied by your use.\nIn other words, you may only use NextView imagery linked from this site for digitizing OpenStreetMap data for humanitarian purposes.", + "url":"https://raw.githubusercontent.com/state-hiu/cybergis-licenses/master/licenses/NEXTVIEW-LICENSE-RAW.txt", + "license_text":"" + } + }, + { + "pk": 8, + "model": "base.license", + "fields": { + "identifier":"CC-0", + "name":"CC-0", + "abbreviation":"CC-0", + "description":"https://creativecommons.org/share-your-work/public-domain/cc0/", + "url":"https://creativecommons.org/publicdomain/zero/1.0/legalcode", + "license_text":"" + } + }, + { + "pk": 9, "model": "base.license", "fields": { - "identifier":"varied_derived", - "name":"Varied / Derived", - "abbreviation":"", - "description":"The constituent parts of this item have different licenses. Go to each part to see license information.", + "identifier":"CC-SNA", + "name":"CC-SNA", + "abbreviation":"CC-SNA", + "description":"https://creativecommons.org/", "url":"", - "license_text":"Not applicable" + "license_text":"" } - }, - { - "pk": 4, + }, + { + "pk": 10, "model": "base.license", "fields": { - "identifier":"public_domain", - "name":"Public Domain", - "abbreviation":"PD", - "description":"Works in the public domain may be used freely without the permission of the former copyright owner.", - "url":"http://www.copyright.gov/help/faq/faq-definitions.html", - "license_text":"The public domain is not a place. A work of authorship is in the “public domain” if it is no longer under copyright protection or if it failed to meet the requirements for copyright protection. Works in the public domain may be used freely without the permission of the former copyright owner." + "identifier":"CC-NC", + "name":"CC-NC", + "abbreviation":"CC-NC", + "description":"https://creativecommons.org/licenses/by-nc/2.0/", + "url":"https://creativecommons.org/licenses/by-nc/2.0/legalcode", + "license_text":"" } - }, - { - "pk": 5, + }, + { + "pk": 11, "model": "base.license", "fields": { - "identifier":"public_domain_usg", - "name":"Public Domain / USG", - "abbreviation":"PD/USG", - "description":"This project constitutes a work of the United States Government and is not subject to domestic copyright protection under 17 USC § 105.", - "url":"https://raw.githubusercontent.com/state-hiu/cybergis-licenses/master/licenses/PUBLICDOMAIN-LICENSE-RAW.txt", - "license_text":"This project constitutes a work of the United States Government and is not subject to domestic copyright protection under 17 USC § 105." + "identifier":"CC-ND", + "name":"CC-ND", + "abbreviation":"CC-ND", + "description":"https://creativecommons.org/licenses/by-nd/2.0/", + "url":"https://creativecommons.org/licenses/by-nd/2.0/legalcode", + "license_text":"" } - }, - { - "pk": 6, + }, + { + "pk": 12, "model": "base.license", "fields": { - "identifier":"odbl", - "name":"Open Data Commons Open Database License / OSM", - "abbreviation":"ODbL/OSM", - "description":"You are free to copy, distribute, transmit and adapt our data, as long as you credit OpenStreetMap and its contributors\nIf you alter or build upon our data, you may distribute the result only under the same licence.", - "url":"http://www.openstreetmap.org/copyright", + "identifier":"CC-NC-SA", + "name":"CC-NC-SA", + "abbreviation":"CC-NC-SA", + "description":"https://creativecommons.org/licenses/by-nc-sa/2.0/", + "url":"https://creativecommons.org/licenses/by-nc-sa/2.0/legalcode", "license_text":"" } - }, - { - "pk": 7, + }, + { + "pk": 12, "model": "base.license", "fields": { - "identifier":"nextview", - "name":"NextView", - "abbreviation":"NV", - "description":"This data is licensed for use by the US Government (USG) under the NextView (NV) license and copyrighted by Digital Globe or GeoEye. The NV license allows the USG to share the imagery and Literal Imagery Derived Products (LIDP) with entities outside the USG when that entity is working directly with the USG, for the USG, or in a manner that is directly beneficial to the USG. The party receiving the data can only use the imagery or LIDP for the original purpose or only as otherwise agreed to by the USG. The party receiving the data cannot share the imagery or LIDP with a third party without express permission from the USG. At no time should this imagery or LIDP be used for other than USG-related purposes and must not be used for commercial gain. The copyright information should be maintained at all times. Your acceptance of these license terms is implied by your use.\nIn other words, you may only use NextView imagery linked from this site for digitizing OpenStreetMap data for humanitarian purposes.", - "url":"https://raw.githubusercontent.com/state-hiu/cybergis-licenses/master/licenses/NEXTVIEW-LICENSE-RAW.txt", + "identifier":"CC-NC-SA", + "name":"CC-NC-SA", + "abbreviation":"CC-NC-SA", + "description":"https://creativecommons.org/licenses/by-nc-sa/2.0/", + "url":"https://creativecommons.org/licenses/by-nc-sa/2.0/legalcode", "license_text":"" } - }, + }, + { "pk": 1, "model": "base.restrictioncodetype", @@ -443,6 +517,7 @@ "description": "a name, symbol, or other device identifying a product, officially registered and legally restricted to the use of the owner or manufacturer" } }, + { "pk": 1, "model": "base.region", @@ -4873,5 +4948,717 @@ "fields": { "name": "CARDS_MENU" } + }, + + + { + "model": "base.relatedidentifiertype", + "pk": 1, + "fields": { + "label": "ARK", + "description": "ARK" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 2, + "fields": { + "label": "DOI", + "description": "DOI" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 3, + "fields": { + "label": "EAN13", + "description": "EAN13" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 4, + "fields": { + "label": "EISSN", + "description": "EISSN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 5, + "fields": { + "label": "Handle", + "description": "Handle" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 6, + "fields": { + "label": "IGSN", + "description": "IGSN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 7, + "fields": { + "label": "IBSN", + "description": "IBSN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 8, + "fields": { + "label": "ISSN", + "description": "ISSN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 9, + "fields": { + "label": "ISTC", + "description": "ISTC" + } + }, + + { + "model": "base.relatedidentifiertype", + "pk": 10, + "fields": { + "label": "LISSN", + "description": "LISSN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 11, + "fields": { + "label": "LSID", + "description": "LSID" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 12, + "fields": { + "label": "PMID", + "description": "PMID" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 13, + "fields": { + "label": "PURL", + "description": "PURL" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 14, + "fields": { + "label": "UPC", + "description": "UPC" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 15, + "fields": { + "label": "URL", + "description": "URL" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 16, + "fields": { + "label": "URN", + "description": "URN" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 17, + "fields": { + "label": "arXiv", + "description": "arXiv" + } + }, + { + "model": "base.relatedidentifiertype", + "pk": 18, + "fields": { + "label": "bibcode", + "description": "bibcode" + } + }, + + + { + "model": "base.relationtype", + "pk": 1, + "fields": { + "label": "Cites", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 2, + "fields": { + "label": "Compiles", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 3, + "fields": { + "label": "Continues", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 4, + "fields": { + "label": "Documents", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 5, + "fields": { + "label": "HasMetadata", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 6, + "fields": { + "label": "HasPart", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 7, + "fields": { + "label": "IsCitedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 8, + "fields": { + "label": "IsCompiledBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 9, + "fields": { + "label": "IsContinuedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 10, + "fields": { + "label": "IsDerivedFrom", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 11, + "fields": { + "label": "IsDocumentedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 12, + "fields": { + "label": "IsIdenticalTo", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 13, + "fields": { + "label": "IsMetadataFor", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 14, + "fields": { + "label": "IsNewVersionOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 15, + "fields": { + "label": "IsOriginalFormOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 16, + "fields": { + "label": "IsPartOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 17, + "fields": { + "label": "IsPreviousVersionOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 18, + "fields": { + "label": "IsReferencedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 19, + "fields": { + "label": "IsReviewedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 20, + "fields": { + "label": "IsSourceOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 21, + "fields": { + "label": "IsSupplementTo", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 22, + "fields": { + "label": "IsSupplementedBy", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 23, + "fields": { + "label": "IsVariantFormOf", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 24, + "fields": { + "label": "References", + "description": "" + } + }, + { + "model": "base.relationtype", + "pk": 25, + "fields": { + "label": "Reviews", + "description": "" + } + }, + + { + "model": "base.fundingreference", + "pk": 1, + "fields": { + "funder_name": "Federal Ministry for Education and Research (BMBF)", + "funder_identifier": "10.13039/501100002347", + "funder_identifier_type": "BMBF" + } + }, + { + "model": "base.fundingreference", + "pk": 2, + "fields": { + "funder_name": "European agricultural fund for rural development (EAFRD)", + "funder_identifier": "10.13039/501100014141", + "funder_identifier_type": "EAFRD" + } + }, + { + "model": "base.fundingreference", + "pk": 3, + "fields": { + "funder_name": "European Union's Horizon 2020 research and innovation programme (Horizon2020)", + "funder_identifier": "", + "funder_identifier_type": "Horizon2020" + } + }, + { + "model": "base.fundingreference", + "pk": 4, + "fields": { + "funder_name": "Federal Ministry of Food and Agriculture (BMEL)", + "funder_identifier": "", + "funder_identifier_type": "BMEL" + } + }, + { + "model": "base.fundingreference", + "pk": 5, + "fields": { + "funder_name": "Forschungsring e.V.", + "funder_identifier": "", + "funder_identifier_type": "Forschungsring" + } + }, + { + "model": "base.fundingreference", + "pk": 6, + "fields": { + "funder_name": "German Academic Exchange Service (DAAD)", + "funder_identifier": "", + "funder_identifier_type": "DAAD" + } + }, + + { + "model": "base.fundingreference", + "pk": 7, + "fields": { + "funder_name": "Research and Culture of the State of Brandenburg (MWFK)", + "funder_identifier": "", + "funder_identifier_type": "MWFK" + } + }, + { + "model": "base.fundingreference", + "pk": 8, + "fields": { + "funder_name": "Leibniz Association", + "funder_identifier": "", + "funder_identifier_type": "Leibniz" + } + }, + { + "model": "base.fundingreference", + "pk": 9, + "fields": { + "funder_name": "Leibniz Center for Agricultural Landscape Research (ZALF e.V.)", + "funder_identifier": "", + "funder_identifier_type": "ZALF" + } + }, + + { + "model": "base.relatedproject", + "pk": 1, + "fields": { + "display_name": "BonaRes - CATCHY", + "label": "catchy" + } + }, + { + "model": "base.relatedproject", + "pk": 2, + "fields": { + "display_name": "BonaRes - DiControl", + "label": "dicontrol" + } + }, + { + "model": "base.relatedproject", + "pk": 3, + "fields": { + "display_name": "BonaRes - InnoSoilPhos", + "label": "innosoilphos" + } + }, + { + "model": "base.relatedproject", + "pk": 4, + "fields": { + "display_name": "BonaRes - Inplamint", + "label": "inplamint" + } + }, + { + "model": "base.relatedproject", + "pk": 5, + "fields": { + "display_name": "BonaRes - I4S", + "label": "i4s" + } + }, + { + "model": "base.relatedproject", + "pk": 6, + "fields": { + "display_name": "BonaRes - ORDIAmur", + "label": "ordiamur" + } + }, + { + "model": "base.relatedproject", + "pk": 7, + "fields": { + "display_name": "BonaRes - SOILAssist", + "label": "soilassist" + } + }, + { + "model": "base.relatedproject", + "pk": 8, + "fields": { + "display_name": "BonaRes - Soil3", + "label": "soil3" + } + }, + { + "model": "base.relatedproject", + "pk": 9, + "fields": { + "display_name": "BonaRes - SUSALPS", + "label": "susalps" + } + }, + { + "model": "base.relatedproject", + "pk": 10, + "fields": { + "display_name": "BonaRes - SIGNAL", + "label": "signal" + } + }, + { + "model": "base.relatedproject", + "pk": 11, + "fields": { + "display_name": "BonaRes Centre", + "label": "bonares_centre" + } + }, + { + "model": "base.relatedproject", + "pk": 12, + "fields": { + "display_name": "LTFE", + "label": "ltfe" + } + }, + { + "model": "base.relatedproject", + "pk": 13, + "fields": { + "display_name": "LTFE V140 Müncheberg", + "label": "ltfe_v140_muencheberg" + } + }, + { + "model": "base.relatedproject", + "pk": 14, + "fields": { + "display_name": "LTFE Dauerdüngungsversuch Dikopshof", + "label": "ltfe_ddv_dikopshof" + } + }, + { + "model": "base.relatedproject", + "pk": 15, + "fields": { + "display_name": "LTFE Phosphordüngungsstrategien Rostock", + "label": "ltfe_pds_rostock" + } + }, + { + "model": "base.relatedproject", + "pk": 16, + "fields": { + "display_name": "LTFE Langzeitdüngungsversuch Darmstadt (Feld A)", + "label": "ltfe_ddv_darmstadt" + } + }, + { + "model": "base.relatedproject", + "pk": 17, + "fields": { + "display_name": "LTFE Hohes Feld Göttingen", + "label": "ltfe_goettingen_hohes_feld" + } + }, + { + "model": "base.relatedproject", + "pk": 18, + "fields": { + "display_name": "LTFE Garte-Süd Göttingen", + "label": "ltfe_goettingen_garte_sued" + } + }, + { + "model": "base.relatedproject", + "pk": 19, + "fields": { + "display_name": "LTFE Dauerdüngungsversuch IOSDV Dülmen", + "label": "ltfe_duelmen" + } + }, + { + "model": "base.relatedproject", + "pk": 20, + "fields": { + "display_name": "Other", + "label": "extern" + } + }, + { + "model": "base.relatedproject", + "pk": 21, + "fields": { + "display_name": "LTFE Langzeitdüngungsversuch Darmstadt (Feld E)", + "label": "ltfe_darmstadt_feld_e" + } + }, + { + "model": "base.relatedproject", + "pk": 22, + "fields": { + "display_name": "ZALF", + "label": "zalf" + } + }, + { + "model": "base.relatedproject", + "pk": 23, + "fields": { + "display_name": "ZALF - patchCROP", + "label": "zalf-patchcrop" + } + }, + { + "model": "base.relatedproject", + "pk": 24, + "fields": { + "display_name": "Rhizo4Bio - RootWayS", + "label": "rhizo_rootways" + } + }, + { + "model": "base.relatedproject", + "pk": 25, + "fields": { + "display_name": "Rhizo4Bio - CROP", + "label": "rhizo_crop" + } + }, + { + "model": "base.relatedproject", + "pk": 26, + "fields": { + "display_name": "Rhizo4Bio - rhizotraits", + "label": "rhizo_traits" + } + }, + { + "model": "base.relatedproject", + "pk": 27, + "fields": { + "display_name": "Rhizo4Bio - RhizoWheat", + "label": "rhizo_wheat" + } + }, + { + "model": "base.relatedproject", + "pk": 28, + "fields": { + "display_name": "Rhizo4Bio - bread and beer", + "label": "rhizo_bread_and_beer" + } + }, + { + "model": "base.relatedproject", + "pk": 29, + "fields": { + "display_name": "Rhizo4Bio - µPlastic", + "label": "rhizo_plastic" + } + }, + { + "model": "base.relatedproject", + "pk": 30, + "fields": { + "display_name": "LTFE Rauischholzhausen", + "label": "ltfe_rauischholzhausen" + } + }, + { + "model": "base.relatedproject", + "pk": 31, + "fields": { + "display_name": "LTFE Dürnast", + "label": "ltfe_duernast" + } + }, + { + "model": "base.relatedproject", + "pk": 32, + "fields": { + "display_name": "LTE Dahlem_D42", + "label": "ltfe_dahlem_d42" + } + }, + { + "model": "base.relatedproject", + "pk": 33, + "fields": { + "display_name": "LTE Thyrow_D5", + "label": "ltfe_thyrow_d5" + } + }, + { + "model": "base.relatedproject", + "pk": 34, + "fields": { + "display_name": "LTFE Bad Lauchstädt", + "label": "ltfe_bad_lauchstaedt" + } + }, { + "model": "base.relatedproject", + "pk": 35, + "fields": { + "display_name": "LTE Seehausen", + "label": "lte_seehausen" + } } -] + ] diff --git a/geonode/base/fixtures/zalfinit.json b/geonode/base/fixtures/zalfinit.json deleted file mode 100644 index 09bd39054c7..00000000000 --- a/geonode/base/fixtures/zalfinit.json +++ /dev/null @@ -1,65 +0,0 @@ -[ - { - "pk": 1, - "model": "base.DescriptionType", - "fields": { - "description_type": "Methods" - } - }, - { - "pk": 2, - "model": "base.DescriptionType", - "fields": { - "description_type": "SeriesInformation" - } - }, - { - "pk": 3, - "model": "base.DescriptionType", - "fields": { - "description_type": "TableOfContents" - } - }, - { - "pk": 4, - "model": "base.DescriptionType", - "fields": { - "description_type": "TechnicalInfo" - } - }, - { - "pk": 5, - "model": "base.DescriptionType", - "fields": { - "description_type": "TechnicotheralInfo" - } - }, - { - "pk": 1, - "model": "base.AlternateType", - "fields": { - "alternate_type": "Alternative" - } - }, - { - "pk": 2, - "model": "base.AlternateType", - "fields": { - "alternate_type": "Subtitle" - } - }, - { - "pk": 3, - "model": "base.AlternateType", - "fields": { - "alternate_type": "Translated" - } - }, - { - "pk": 4, - "model": "base.AlternateType", - "fields": { - "alternate_type": "Other" - } - } -] \ No newline at end of file diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 5a760c6176d..9c88afa3544 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -53,10 +53,6 @@ ThesaurusKeywordLabel, ThesaurusLabel, TopicCategory, - AlternateType, - DescriptionType, - FundingReference, - RelatedIdentifier, ) from geonode.base.widgets import TaggitSelect2Custom from geonode.base.fields import MultiThesauriField @@ -364,7 +360,15 @@ class ResourceBaseForm(TranslationModelForm): """Base form for metadata, should be inherited by childres classes of ResourceBase""" abstract = forms.CharField(label=_("Abstract"), required=False, widget=TinyMCE()) - + abstract_translated = forms.CharField(label=_("translated abstract"), help_text=ResourceBase.abstract_translated_help_text ,required=False, widget=TinyMCE()) + + subtitle = forms.CharField(required=False, help_text=ResourceBase.subtitle_help_text ,widget=TinyMCE()) + method_description = forms.CharField(required=False, help_text=ResourceBase.method_description_help_text, widget=TinyMCE()) + series_information = forms.CharField(required=False, help_text=ResourceBase.series_information_help_text, widget=TinyMCE()) + table_of_content = forms.CharField(required=False, help_text=ResourceBase.table_of_content_help_text, widget=TinyMCE()) + technical_info = forms.CharField(required=False, help_text=ResourceBase.technical_info_help_text, widget=TinyMCE()) + other_description = forms.CharField(required=False, help_text=ResourceBase.other_description_help_text, widget=TinyMCE()) + purpose = forms.CharField(label=_("Purpose"), required=False, widget=TinyMCE()) constraints_other = forms.CharField(label=_("Other constraints"), required=False, widget=TinyMCE()) @@ -376,25 +380,6 @@ class ResourceBaseForm(TranslationModelForm): data_quality_statement = forms.CharField(label=_("Data quality statement"), required=False, widget=TinyMCE()) - ################## - # ZALF ADDITIONS # - ################## - - abstract_de = forms.CharField(label=_("Abstract German"), required=False, widget=TinyMCE()) - - # Alternate = forms.CharField( - # label=_("Alternate"), - # required=False, - # widget=TinyMCE()) - - alternate_type = forms.ModelChoiceField( - label=_("Alternate Type"), queryset=AlternateType.objects.all(), required=False - ) - - description_type = forms.ModelChoiceField( - label=_("Description Type"), queryset=DescriptionType.objects.all(), required=False - ) - ## owner = forms.ModelChoiceField( empty_label=_("Owner"), label=_("Owner"), @@ -593,11 +578,6 @@ class BatchEditForm(forms.Form): owner = forms.ModelChoiceField(label=_("Owner"), queryset=get_user_model().objects.all(), required=False) category = forms.ModelChoiceField(label=_("Category"), queryset=TopicCategory.objects.all(), required=False) license = forms.ModelChoiceField(label=_("License"), queryset=License.objects.all(), required=False) - ################## - # ZALF ADDITIONS # - ################## - - ## regions = forms.ModelChoiceField(label=_("Regions"), queryset=Region.objects.all(), required=False) date = forms.DateTimeField(label=_("Date"), required=False) language = forms.ChoiceField( diff --git a/geonode/base/models.py b/geonode/base/models.py index 0339a8d07d2..896bfe072cf 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -633,47 +633,31 @@ def cleanup_uploaded_files(resource_id): upload.delete() -######################## -# ZALF MODEL ADDITIONS # -######################## +class RelatedIdentifierType(models.Model): + label = models.CharField(max_length=255, help_text=_("related identifier, available identifier types"),unique=True, primary_key=True) + description = models.CharField(max_length=255, help_text=_("label description")) + def __str__(self): + return f"{self.label}" -class AlternateType(models.Model): - """Available Alternate Types""" - - ALTERNATE_TYPE = ( - ("Alternative", ""), - ("Subtitle", ""), - ("Translated", ""), - ("Other", ""), - ) - alternate_type = models.CharField(max_length=255, choices=ALTERNATE_TYPE, help_text=_("Alternate title type")) +class RelationType(models.Model): + label = models.CharField(max_length=255, help_text=_("Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory"),unique=True, primary_key=True) + description = models.CharField(max_length=255, help_text=_("label description")) def __str__(self): - return f"{self.alternate_type}" - + return f"{self.label}" -class DescriptionType(models.Model): - """Descripion Type of abstract""" - - DESCRIPTION_TYPES = ( - ("Methods", "The methodology employed for the study or research"), - ("SeriesInformation", "Information about a repeating series, such as volumne, issue, number"), - ("TableOfContents", "A listing of the Table of Contents"), - ( - "TechnicalInfo", - "Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system", - ), - ("other", "Other description information that does not fit into an existing category"), - ) - description_type = models.CharField( - max_length=255, choices=DESCRIPTION_TYPES, help_text=_("abstract description type") +class RelatedIdentifier(models.Model): + related_identifier = models.CharField( + max_length=255, help_text=_("Identifiers of related resources. These must be globally unique identifiers.") ) - + related_identifier_type = models.ForeignKey(RelatedIdentifierType, on_delete=models.CASCADE) + relation_type = models.ForeignKey(RelationType, on_delete=models.CASCADE) + def __str__(self): - return f"{self.description_type}" + return f"Related Identifier: {self.related_identifier}({self.relation_type}: {self.related_identifier_type})" class FundingReference(models.Model): @@ -689,9 +673,16 @@ class FundingReference(models.Model): ), ) funder_identifier_type = models.CharField(max_length=255, help_text=_("The type of the Identifier. (e.g. BMBF)")) + + def __str__(self): + return f"{self.funder_name}" + + +class Funder(models.Model): + funding_reference = models.ForeignKey(FundingReference, null=False, blank=False, on_delete=models.CASCADE) award_number = models.CharField( max_length=255, help_text=_("The code assigned by the funder to a sponsored award (grant). (e.g. 282625)") - ) + ) award_uri = models.CharField( max_length=255, help_text=_( @@ -705,27 +696,15 @@ class FundingReference(models.Model): ), ) +class RelatedProject(models.Model): + label = models.CharField(max_length=255, + help_text=_("label of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)"), + unique=True + ) -class RelatedIdentifier(models.Model): - related_identifier = models.CharField( - max_length=255, help_text=_("Identifiers of related resources. These must be globally unique identifiers.") - ) - related_identifier_type = models.CharField( - max_length=255, - help_text=_( - "The type of the Related identifier. If Related identifier is used Identifier type is mandatory. (e.g. bibcode)" - ), - ) - relation_type = models.CharField( - max_length=500, - help_text=_( - "Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory." - ), - ) - - -############################################ - + display_name = models.CharField(max_length=255, + help_text=_("Name of the hierarchy levels for which the metadata is provided. (e.g. signal)") + ) class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): """ @@ -769,6 +748,7 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): "(space or comma-separated)" ) regions_help_text = _("keyword identifies a location") + use_constrains_help_text = _("This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)") restriction_code_type_help_text = _("limitation(s) placed upon the access or use of the data.") constraints_other_help_text = _( "other restrictions and legal prerequisites for accessing and using the resource or" " metadata" @@ -788,10 +768,38 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): extra_metadata_help_text = _( 'Additional metadata, must be in format [ {"metadata_key": "metadata_value"}, {"metadata_key": "metadata_value"} ]' ) + # BONARES METADATA EXTENSIONS + # Bonares Description + title_translated_help_text = ("german name by which the cited resource is known") + + subtitle_help_text = ("subtitle of the dataset") + abstract_translated_help_text = _("brief german narrative summary of the content of the resource(s)") + + method_description_help_text = _( "The methodology employed for the study or research") + series_information_help_text = _("Information about a repeating series, such as volumne, issue, number") + table_of_content_help_text = _("A listing of the Table of Contents") + technical_info_help_text = _("Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system") + other_description_help_text = _("Other description information that does not fit into an existing category") + related_identifer_help_text = _("Identifiers of related resources. These must be globally unique identifiers.") + funders_help_text = _("List of funders, funded dataset creators") + related_projects_help_text = _("Name of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)") + # internal fields uuid = models.CharField(max_length=36, unique=True, default=uuid.uuid4) title = models.CharField(_("title"), max_length=255, help_text=_("name by which the cited resource is known")) - abstract = models.TextField(_("abstract"), max_length=2000, blank=True, help_text=abstract_help_text) + title_translated = models.CharField(_("title_translated"), max_length=255, help_text=title_translated_help_text) + + abstract = models.TextField(_("abstract"), max_length=2000, help_text=abstract_help_text) + abstract_translated = models.TextField(_("abstract_translated"), max_length=2000, help_text=abstract_translated_help_text) + + # description type elements + subtitle = models.TextField(_("subtitle"), max_length=400, blank=True, help_text=subtitle_help_text) + method_description = models.TextField(_("method_description"), max_length=2000, blank=True, help_text=method_description_help_text) + series_information = models.TextField(_("series_information"), max_length=2000, blank=True, help_text=series_information_help_text) + table_of_content = models.TextField(_("table_of_content"), max_length=2000, blank=True, help_text=table_of_content_help_text) + technical_info = models.TextField(_("technical_info"), max_length=2000, blank=True, help_text=technical_info_help_text) + other_description = models.TextField(_("other_description"), max_length=2000, blank=True, help_text=other_description_help_text) + purpose = models.TextField(_("purpose"), max_length=500, null=True, blank=True, help_text=purpose_help_text) owner = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="owned_resource", verbose_name=_("Owner"), on_delete=models.PROTECT @@ -828,13 +836,22 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): regions = models.ManyToManyField( Region, verbose_name=_("keywords region"), null=True, blank=True, help_text=regions_help_text ) - restriction_code_type = models.ForeignKey( + use_constrains = models.ManyToManyField( + RestrictionCodeType, + verbose_name=_("use_constrains"), + help_text=use_constrains_help_text, + null=True, + blank=True, + related_name='use_constrains', + limit_choices_to=Q(is_choice=True) + ) + restriction_code_type = models.ManyToManyField( RestrictionCodeType, verbose_name=_("restrictions"), help_text=restriction_code_type_help_text, null=True, blank=True, - on_delete=models.SET_NULL, + related_name='restriction_code_type', limit_choices_to=Q(is_choice=True), ) constraints_other = models.TextField( @@ -994,37 +1011,11 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): metadata = models.ManyToManyField( "ExtraMetadata", verbose_name=_("Extra Metadata"), null=True, blank=True, help_text=extra_metadata_help_text ) - - ############################### - # ZALF added GeoNode Metadata # - ############################### - - title_de = models.CharField( - _("title_de"), max_length=255, default="", help_text=_("german name by which the cited resource is known") - ) - - abstract_de = models.TextField( - _("abstract_de"), - max_length=2000, - blank=True, - help_text=_("brief german narrative summary of the content of the resource(s)"), - ) - - alternate_type = models.ForeignKey( - AlternateType, null=True, blank=False, on_delete=models.SET_NULL, help_text=_("Type of the alternate field") - ) - - description_type = models.ForeignKey( - DescriptionType, null=True, blank=False, on_delete=models.SET_NULL, help_text=_("Descripion Type of abstract.") - ) - - # project_leader = models.ForeignKey( - # settings.AUTH_USER_MODEL, - # on_delete=models.PROTECT) - - funding_reference = models.ForeignKey(FundingReference, null=True, blank=True, on_delete=models.SET_NULL) - - related_identifier = models.ForeignKey(RelatedIdentifier, null=True, blank=True, on_delete=models.SET_NULL) + + # Bonares + related_identifier = models.ManyToManyField(RelatedIdentifier, verbose_name=_("Related Identifier"), null=True, blank=True, help_text=related_identifer_help_text) + funders = models.ManyToManyField(Funder, verbose_name=_("Funder names"), null=True, blank=True, help_text=funders_help_text) + related_projects = models.ManyToManyField(RelatedProject, verbose_name=_("related project"), null=True, blank=True, help_text=related_projects_help_text) use_contraints = models.TextField( _("use_constraints"), @@ -1035,10 +1026,6 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): ), ) - parent_ressource = models.ForeignKey( - "self", null=True, blank=True, help_text=_("Parent Dataset, this dataset belongs to"), on_delete=models.SET_NULL - ) - objects = ResourceBaseManager() class Meta: diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index 9d0f75062eb..c10634646f3 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -322,7 +322,7 @@
            - {% endblock thumbnail %} + {% endblock thumbnail %}
            {% block dataset_title %}
            @@ -331,13 +331,6 @@ {{ dataset_form.title }}
            {% endblock dataset_title %} - -
            - - - - {{ dataset_form.title_de }} -
            {% block dataset_abstract %}
            @@ -345,18 +338,6 @@ {{ dataset_form.abstract }}
            {% endblock dataset_abstract %} -
            - - - - {{ dataset_form.abstract_de }} -
            -
            - - - - {{ dataset_form.description_type }} -
            {% block dataset_date_type %} @@ -364,7 +345,7 @@ {{ dataset_form.date_type }} -
            + {% endblock dataset_date_type %} {% block dataset_date %}
            diff --git a/geonode/settings.py b/geonode/settings.py index 7d669218739..6c69583b75b 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -37,12 +37,6 @@ from kombu import Queue, Exchange from kombu.serialization import register -# add these import lines to the top of your geonode settings file -from django_auth_ldap import config as ldap_config -from geonode_ldap.config import GeonodeNestedGroupOfNamesType -import ldap -import sentry_sdk - from . import serializer SILENCED_SYSTEM_CHECKS = [ @@ -56,30 +50,6 @@ # GeoNode Version VERSION = get_version() -BUILD_NUMBER = os.environ.get("BUILD_NUMBER", "0") - - -# ZALF SENTRY ADDITIONS -SENTRY_ENABLED = ast.literal_eval(os.getenv("SENTRY_ENABLED", "False")) -if SENTRY_ENABLED: - import sentry_sdk - - print("sentry enabled ...") - SENTRY_DSN = os.getenv("SENTRY_DSN") - print(SENTRY_DSN) - sentry_sdk.init( - dsn=SENTRY_DSN, - release="geonodex@{}.{}".format(VERSION, BUILD_NUMBER), - environment=os.getenv("SENTRY_ENVIRONMENT", "development"), - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0, - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True, - ) - DEFAULT_CHARSET = "utf-8" @@ -869,52 +839,12 @@ # 'oauth2_provider.backends.OAuth2Backend', "django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend", - # 'allauth.account.auth_backends.AuthenticationBackend', + "allauth.account.auth_backends.AuthenticationBackend", ) if "announcements" in INSTALLED_APPS: AUTHENTICATION_BACKENDS += ("announcements.auth_backends.AnnouncementPermissionsBackend",) -LDAP_ENABLED = strtobool(os.getenv("LDAP_ENABLED", "False")) -if LDAP_ENABLED: - # add both standard ModelBackend auth and geonode.contrib.ldap auth - AUTHENTICATION_BACKENDS += ("geonode_ldap.backend.GeonodeLdapBackend",) - -# django_auth_ldap configuration -AUTH_LDAP_SERVER_URI = os.getenv("LDAP_SERVER_URL") -AUTH_LDAP_BIND_DN = os.getenv("LDAP_BIND_DN") -AUTH_LDAP_BIND_PASSWORD = os.getenv("LDAP_BIND_PASSWORD") -AUTH_LDAP_USER_SEARCH = ldap_config.LDAPSearch( - os.getenv("LDAP_USER_SEARCH_DN"), ldap.SCOPE_SUBTREE, os.getenv("LDAP_USER_SEARCH_FILTERSTR") -) - -# should LDAP groups be used to spawn groups in GeoNode? -AUTH_LDAP_MIRROR_GROUPS = True -AUTH_LDAP_GROUP_SEARCH = ldap_config.LDAPSearch( - os.getenv("LDAP_GROUP_SEARCH_DN"), ldap.SCOPE_SUBTREE, os.getenv("LDAP_GROUP_SEARCH_FILTERSTR") -) - -AUTH_LDAP_GROUP_TYPE = GeonodeNestedGroupOfNamesType() -AUTH_LDAP_USER_ATTR_MAP_FIRST_NAME = os.getenv("LDAP_USER_ATTR_MAP_FIRST_NAME", "givenName") -AUTH_LDAP_USER_ATTR_MAP_LAST_NAME = os.getenv("LDAP_USER_ATTR_MAP_LAST_NAME", "sn") -AUTH_LDAP_USER_ATTR_MAP_EMAIL_ADDR = os.getenv("LDAP_USER_ATTR_MAP_EMAIL_ADDR", "mailPrimaryAddress") -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": AUTH_LDAP_USER_ATTR_MAP_FIRST_NAME, - "last_name": AUTH_LDAP_USER_ATTR_MAP_LAST_NAME, - "email": AUTH_LDAP_USER_ATTR_MAP_EMAIL_ADDR, -} - -AUTH_LDAP_FIND_GROUP_PERMS = True -AUTH_LDAP_ALWAYS_UPDATE_USER = strtobool(os.getenv("LDAP_ALWAYS_UPDATE_USER", "True")) -AUTH_LDAP_FIND_GROUP_PERMS = True -AUTH_LDAP_CACHE_TIMEOUT = 3600 - -# these are not needed by django_auth_ldap - we use them to find and match -# GroupProfiles and GroupCategories -# GEONODE_LDAP_GROUP_NAME_ATTRIBUTE = os.getenv("LDAP_GROUP_NAME_ATTRIBUTE", default="cn") -# GEONODE_LDAP_GROUP_PROFILE_FILTERSTR = os.getenv("LDAP_GROUP_SEARCH_FILTERSTR", default='(ou=research group)') -# GEONODE_LDAP_GROUP_PROFILE_MEMBER_ATTR = os.getenv("LDAP_GROUP_PROFILE_MEMBER_ATTR", default='member') - OAUTH2_PROVIDER = { "SCOPES": { "openid": "Default to OpenID", @@ -1001,6 +931,7 @@ # # Settings for third party apps # + # Pinax Ratings PINAX_RATINGS_CATEGORY_CHOICES = { "maps.Map": {"map": "How good is this map?"}, @@ -1406,9 +1337,6 @@ ] -SLACK_ENALBED = ast.literal_eval(os.getenv("SLACK_ENABLED", "True")) -SLACK_WEBHOOK_URLS = eval(os.getenv("SLACK_WEBHOOK_URL", "[]")) - DISPLAY_ORIGINAL_DATASET_LINK = ast.literal_eval(os.getenv("DISPLAY_ORIGINAL_DATASET_LINK", "True")) ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE = ast.literal_eval(os.getenv("ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE", "False")) @@ -1533,7 +1461,7 @@ def get_geonode_catalogue_service(): } } return pycsw_catalogue - return None + return None GEONODE_CATALOGUE_SERVICE = get_geonode_catalogue_service() @@ -1978,7 +1906,6 @@ def get_geonode_catalogue_service(): # Required: (boolean, optional, default false) mandatory while editing metadata (not implemented yet) # Filter: (boolean, optional, default false) a filter option on that thesaurus will appear in the main search page # THESAURUS = {'name': 'inspire_themes', 'required': True, 'filter': True} -THESAURUS_DEFAULT_LANG = os.environ.get("THESAURUS_DEFAULT_LANG", "en") # ######################################################## # # Advanced Resource Publishing Worklow Settings - START # @@ -2273,11 +2200,13 @@ def get_geonode_catalogue_service(): Define the URLs patterns used by the SizeRestrictedFileUploadHandler to evaluate if the file is greater than the limit size defined """ + SIZE_RESTRICTED_FILE_UPLOAD_ELEGIBLE_URL_NAMES = ( "data_upload", "uploads-upload", "document_upload", ) + SUPPORTED_DATASET_FILE_TYPES = [ { "id": "shp", From f3985fd0facd4b6ea6969bd029532078db501a4c Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 31 Mar 2023 12:47:54 +0200 Subject: [PATCH 25/49] models WIP --- geonode/base/models.py | 187 ++++++++++++++++++++++++++++++++--------- 1 file changed, 147 insertions(+), 40 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index 896bfe072cf..4ed65e7c99e 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -24,6 +24,7 @@ import uuid import logging import traceback +import datetime from sequences.models import Sequence from sequences import get_next_value @@ -634,19 +635,28 @@ def cleanup_uploaded_files(resource_id): class RelatedIdentifierType(models.Model): - label = models.CharField(max_length=255, help_text=_("related identifier, available identifier types"),unique=True, primary_key=True) + label = models.CharField( + max_length=255, help_text=_("related identifier, available identifier types"), unique=True, primary_key=True + ) description = models.CharField(max_length=255, help_text=_("label description")) def __str__(self): - return f"{self.label}" + return f"{self.label}" class RelationType(models.Model): - label = models.CharField(max_length=255, help_text=_("Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory"),unique=True, primary_key=True) - description = models.CharField(max_length=255, help_text=_("label description")) + label = models.CharField( + max_length=255, + help_text=_( + "Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory" + ), + unique=True, + primary_key=True, + ) + description = models.CharField(max_length=255, help_text=_("label description")) def __str__(self): - return f"{self.label}" + return f"{self.label}" class RelatedIdentifier(models.Model): @@ -655,9 +665,9 @@ class RelatedIdentifier(models.Model): ) related_identifier_type = models.ForeignKey(RelatedIdentifierType, on_delete=models.CASCADE) relation_type = models.ForeignKey(RelationType, on_delete=models.CASCADE) - + def __str__(self): - return f"Related Identifier: {self.related_identifier}({self.relation_type}: {self.related_identifier_type})" + return f"Related Identifier: {self.related_identifier}({self.relation_type}: {self.related_identifier_type})" class FundingReference(models.Model): @@ -673,16 +683,16 @@ class FundingReference(models.Model): ), ) funder_identifier_type = models.CharField(max_length=255, help_text=_("The type of the Identifier. (e.g. BMBF)")) - + def __str__(self): - return f"{self.funder_name}" + return f"{self.funder_name}" class Funder(models.Model): funding_reference = models.ForeignKey(FundingReference, null=False, blank=False, on_delete=models.CASCADE) award_number = models.CharField( max_length=255, help_text=_("The code assigned by the funder to a sponsored award (grant). (e.g. 282625)") - ) + ) award_uri = models.CharField( max_length=255, help_text=_( @@ -696,15 +706,18 @@ class Funder(models.Model): ), ) + class RelatedProject(models.Model): - label = models.CharField(max_length=255, - help_text=_("label of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)"), - unique=True - ) + label = models.CharField( + max_length=255, + help_text=_("label of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)"), + unique=True, + ) + + display_name = models.CharField( + max_length=255, help_text=_("Name of the hierarchy levels for which the metadata is provided. (e.g. signal)") + ) - display_name = models.CharField(max_length=255, - help_text=_("Name of the hierarchy levels for which the metadata is provided. (e.g. signal)") - ) class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): """ @@ -725,7 +738,8 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): PERMISSIONS = {} - VALID_DATE_TYPES = [(x.lower(), _(x)) for x in ["Publication"]] + VALID_DATE_TYPES = [(x.lower(), _(x)) for x in ["Creation", "Publication", "Revision"]] + VALID_CONFORMITY_RESULTS = [(x, _(x)) for x in ["Passed", "Not Passed", "Unknown"]] abstract_help_text = _("brief narrative summary of the content of the resource(s)") date_help_text = _("reference date for the cited resource") @@ -748,7 +762,9 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): "(space or comma-separated)" ) regions_help_text = _("keyword identifies a location") - use_constrains_help_text = _("This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)") + use_constrains_help_text = _( + "This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)" + ) restriction_code_type_help_text = _("limitation(s) placed upon the access or use of the data.") constraints_other_help_text = _( "other restrictions and legal prerequisites for accessing and using the resource or" " metadata" @@ -770,36 +786,108 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): ) # BONARES METADATA EXTENSIONS # Bonares Description - title_translated_help_text = ("german name by which the cited resource is known") - - subtitle_help_text = ("subtitle of the dataset") + title_translated_help_text = "german name by which the cited resource is known" + + subtitle_help_text = "subtitle of the dataset" abstract_translated_help_text = _("brief german narrative summary of the content of the resource(s)") - method_description_help_text = _( "The methodology employed for the study or research") + method_description_help_text = _("The methodology employed for the study or research") series_information_help_text = _("Information about a repeating series, such as volumne, issue, number") table_of_content_help_text = _("A listing of the Table of Contents") - technical_info_help_text = _("Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system") + technical_info_help_text = _( + "Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system" + ) other_description_help_text = _("Other description information that does not fit into an existing category") related_identifer_help_text = _("Identifiers of related resources. These must be globally unique identifiers.") funders_help_text = _("List of funders, funded dataset creators") related_projects_help_text = _("Name of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)") - + conformity_results_help_text = _( + "This is the degree of conformity of the dataset to the implementing rules the BonaRes Schema." + ) + conformity_explanation_help_text = _( + "Give an Explanation about the conformity check. (e.g. See the referenced specification." + ) + parent_identifier_help_text = _( + "A file identifier of the metadata to which this metadata is a subset (child). (e.g. 73c0f49f-1502-48ee-b038-052563f36527)" + ) + + date_accepted_help_text = _("The date that the publisher accepted the resource into their system.") + date_available_help_text = _( + "The date the resource is made publicly available. To indicate the end of an embargo period." + ) + date_collected_help_text = _("The date or date range in which the dataset content was collected") + date_copyrighted_help_text = _( + "The specific, documented date at which the dataset receives a copyrighted status, if applicable." + ) + date_created_help_text = _( + "The date the resource itself was put together; a single date for a final component, e.g. the finalised file with all of the data." + ) + date_issued_help_text = _("The date that the resource is published or distributed e.g. to a data centre.") + date_submitted_help_text = _( + "The date the creator submits the resource to the publisher. This could be different from Accepted if the publisher the applies a selection process. To indicate the start of an embargo period. " + ) + date_updated_help_text = _( + "The date of the last update (last revision) to the dataset, when the dataset is being added to." + ) + date_valid_help_text = _("The date or date range during which the dataset or resource is accurate.") + # internal fields uuid = models.CharField(max_length=36, unique=True, default=uuid.uuid4) title = models.CharField(_("title"), max_length=255, help_text=_("name by which the cited resource is known")) title_translated = models.CharField(_("title_translated"), max_length=255, help_text=title_translated_help_text) abstract = models.TextField(_("abstract"), max_length=2000, help_text=abstract_help_text) - abstract_translated = models.TextField(_("abstract_translated"), max_length=2000, help_text=abstract_translated_help_text) + abstract_translated = models.TextField( + _("abstract_translated"), max_length=2000, help_text=abstract_translated_help_text + ) # description type elements subtitle = models.TextField(_("subtitle"), max_length=400, blank=True, help_text=subtitle_help_text) - method_description = models.TextField(_("method_description"), max_length=2000, blank=True, help_text=method_description_help_text) - series_information = models.TextField(_("series_information"), max_length=2000, blank=True, help_text=series_information_help_text) - table_of_content = models.TextField(_("table_of_content"), max_length=2000, blank=True, help_text=table_of_content_help_text) - technical_info = models.TextField(_("technical_info"), max_length=2000, blank=True, help_text=technical_info_help_text) - other_description = models.TextField(_("other_description"), max_length=2000, blank=True, help_text=other_description_help_text) - + method_description = models.TextField( + _("method_description"), max_length=2000, blank=True, help_text=method_description_help_text + ) + series_information = models.TextField( + _("series_information"), max_length=2000, blank=True, help_text=series_information_help_text + ) + table_of_content = models.TextField( + _("table_of_content"), max_length=2000, blank=True, help_text=table_of_content_help_text + ) + technical_info = models.TextField( + _("technical_info"), max_length=2000, blank=True, help_text=technical_info_help_text + ) + other_description = models.TextField( + _("other_description"), max_length=2000, blank=True, help_text=other_description_help_text + ) + + conformity_results = models.CharField( + _("conformity result"), + max_length=40, + choices=VALID_CONFORMITY_RESULTS, + default="Unknown", + help_text=conformity_results_help_text, + ) + conformity_explanation = models.CharField( + _("conformity explanation"), max_length=2000, blank=True, help_text=conformity_explanation_help_text + ) + parent_identifier = models.ForeignKey( + "self", null=True, blank=True, help_text=parent_identifier_help_text, on_delete=models.SET_NULL + ) + date_available = models.DateField( + _("date available"), default=datetime.date.today, help_text=date_available_help_text + ) + date_created = models.DateField(_("date created"), default=datetime.date.today, help_text=date_created_help_text) + date_issued = models.DateField(_("date issued"), default=datetime.date.today, help_text=date_issued_help_text) + date_updated = models.DateField(_("date updated"), default=datetime.date.today, help_text=date_updated_help_text) + + date_accepted = models.DateField(_("date accepted"), blank=True, null=True, help_text=date_accepted_help_text) + date_collected = models.DateField(_("date collected"), blank=True, null=True, help_text=date_collected_help_text) + date_copyrighted = models.DateField( + _("date copyrighted"), blank=True, null=True, help_text=date_copyrighted_help_text + ) + date_submitted = models.DateField(_("date submitted"), blank=True, null=True, help_text=date_submitted_help_text) + date_valid = models.DateField(_("date valid"), blank=True, null=True, help_text=date_valid_help_text) + date_issued = models.DateField(_("date issued"), blank=True, null=True, help_text=date_issued_help_text) + purpose = models.TextField(_("purpose"), max_length=500, null=True, blank=True, help_text=purpose_help_text) owner = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="owned_resource", verbose_name=_("Owner"), on_delete=models.PROTECT @@ -842,8 +930,8 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): help_text=use_constrains_help_text, null=True, blank=True, - related_name='use_constrains', - limit_choices_to=Q(is_choice=True) + related_name="use_constrains", + limit_choices_to=Q(is_choice=True), ) restriction_code_type = models.ManyToManyField( RestrictionCodeType, @@ -851,7 +939,7 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): help_text=restriction_code_type_help_text, null=True, blank=True, - related_name='restriction_code_type', + related_name="restriction_code_type", limit_choices_to=Q(is_choice=True), ) constraints_other = models.TextField( @@ -904,6 +992,7 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): data_quality_statement = models.TextField( _("data quality statement"), max_length=2000, blank=True, null=True, help_text=data_quality_statement_help_text ) + group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) # Section 9 @@ -1011,11 +1100,21 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): metadata = models.ManyToManyField( "ExtraMetadata", verbose_name=_("Extra Metadata"), null=True, blank=True, help_text=extra_metadata_help_text ) - - # Bonares - related_identifier = models.ManyToManyField(RelatedIdentifier, verbose_name=_("Related Identifier"), null=True, blank=True, help_text=related_identifer_help_text) - funders = models.ManyToManyField(Funder, verbose_name=_("Funder names"), null=True, blank=True, help_text=funders_help_text) - related_projects = models.ManyToManyField(RelatedProject, verbose_name=_("related project"), null=True, blank=True, help_text=related_projects_help_text) + + # Bonares + related_identifier = models.ManyToManyField( + RelatedIdentifier, + verbose_name=_("Related Identifier"), + null=True, + blank=True, + help_text=related_identifer_help_text, + ) + funders = models.ManyToManyField( + Funder, verbose_name=_("Funder names"), null=True, blank=True, help_text=funders_help_text + ) + related_projects = models.ManyToManyField( + RelatedProject, verbose_name=_("related project"), null=True, blank=True, help_text=related_projects_help_text + ) use_contraints = models.TextField( _("use_constraints"), @@ -1084,6 +1183,14 @@ def compact_permission_labels(cls): "owner": _("Owner"), } + # Bonares + @property + def metadata_standard_name(self): + return "BonaRes Metadata Schema (https://doi.org/10.20387/BonaRes-5PGG-8YRP)" + + def metadata_standard_version(self): + return "Version 1.0" + @property def raw_abstract(self): return self._remove_html_tags(self.abstract) From 80d2b47b3bde5b9a2ceedfd8d8f7e27509c628c9 Mon Sep 17 00:00:00 2001 From: ahmdthr Date: Mon, 3 Apr 2023 07:36:08 +0200 Subject: [PATCH 26/49] Fixed default contact roles for new resource and added tests --- geonode/base/api/serializers.py | 4 +- geonode/base/models.py | 16 +- geonode/base/widgets.py | 2 +- geonode/documents/api/tests.py | 301 ++++++++++++++++++++++++++++++++ geonode/layers/api/tests.py | 212 ++++++++++++++++++---- geonode/resource/utils.py | 6 +- 6 files changed, 490 insertions(+), 51 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 1e6e5d60f86..cacd4d118e6 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -331,8 +331,8 @@ def to_representation(self, instance): class ContactRoleField(DynamicComputedField): - def __init__(self, contat_type, **kwargs): - self.contat_type = contat_type + def __init__(self, contact_type, **kwargs): + self.contact_type = contact_type super().__init__(**kwargs) def get_attribute(self, instance): diff --git a/geonode/base/models.py b/geonode/base/models.py index 93e56e82967..28da1b32f42 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1585,10 +1585,10 @@ def set_missing_info(self): pass if user: - if self.poc is None: - self.poc = user - if self.metadata_author is None: - self.metadata_author = user + if len(self.poc) == 0: + self.poc = [user] + if len(self.metadata_author) == 0: + self.metadata_author = [user] from guardian.models import UserObjectPermission @@ -1613,10 +1613,10 @@ def add_missing_metadata_author_or_poc(self): """ Set metadata_author and/or point of contact (poc) to a resource when any of them is missing """ - if not self.metadata_author: - self.metadata_author = self.owner - if not self.poc: - self.poc = self.owner + if len(self.metadata_author) == 0: + self.metadata_author = [self.owner] + if len(self.poc) == 0: + self.poc = [self.owner] @staticmethod def get_multivalue_role_property_names() -> List[str]: diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py index 39db137d047..af21da26c39 100644 --- a/geonode/base/widgets.py +++ b/geonode/base/widgets.py @@ -31,7 +31,7 @@ def value_from_datadict(self, data, files, name): returns list of selected elements """ try: - ret_list = data[name] + ret_list = data.getlist(name) return ret_list except KeyError: return [] diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index 2ba5e294050..67c13cbff4f 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -136,3 +136,304 @@ def test_creation_should_create_the_doc(self): if cloned_path: os.remove(cloned_path) + + def test_patch_point_of_contact(self): + document = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{document.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"poc": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [poc.get("pk") for poc in response.json().get("document").get("poc")] for user_id in user_ids + ) + ) + # Resetting all point of contact + response = self.client.patch(url, data={"poc": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [poc.get("pk") for poc in response.json().get("document").get("poc")] + for user_id in user_ids + ) + ) + + def test_patch_metadata_author(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"metadata_author": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + in [ + metadata_author.get("pk") + for metadata_author in response.json().get("document").get("metadata_author") + ] + for user_id in user_ids + ) + ) + # Resetting all metadata authors + response = self.client.patch(url, data={"metadata_author": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + metadata_author.get("pk") + for metadata_author in response.json().get("document").get("metadata_author") + ] + for user_id in user_ids + ) + ) + + def test_patch_processor(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"processor": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [processor.get("pk") for processor in response.json().get("document").get("processor")] + for user_id in user_ids + ) + ) + # Resetting all processors + response = self.client.patch(url, data={"processor": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [processor.get("pk") for processor in response.json().get("document").get("processor")] + for user_id in user_ids + ) + ) + + def test_patch_publisher(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"publisher": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [publisher.get("pk") for publisher in response.json().get("document").get("publisher")] + for user_id in user_ids + ) + ) + # Resetting all publishers + response = self.client.patch(url, data={"publisher": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [publisher.get("pk") for publisher in response.json().get("document").get("publisher")] + for user_id in user_ids + ) + ) + + def test_patch_custodian(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"custodian": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [custodian.get("pk") for custodian in response.json().get("document").get("custodian")] + for user_id in user_ids + ) + ) + # Resetting all custodians + response = self.client.patch(url, data={"custodian": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [custodian.get("pk") for custodian in response.json().get("document").get("custodian")] + for user_id in user_ids + ) + ) + + def test_patch_distributor(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"distributor": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [distributor.get("pk") for distributor in response.json().get("document").get("distributor")] + for user_id in user_ids + ) + ) + # Resetting all distributers + response = self.client.patch(url, data={"distributor": []}, format="json") + + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [distributor.get("pk") for distributor in response.json().get("document").get("distributor")] + for user_id in user_ids + ) + ) + + def test_patch_resource_user(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"resource_user": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + in [resource_user.get("pk") for resource_user in response.json().get("document").get("resource_user")] + for user_id in user_ids + ) + ) + # Resetting all resource users + response = self.client.patch(url, data={"resource_user": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + resource_user.get("pk") for resource_user in response.json().get("document").get("resource_user") + ] + for user_id in user_ids + ) + ) + + def test_patch_resource_provider(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"resource_provider": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + in [ + resource_provider.get("pk") + for resource_provider in response.json().get("document").get("resource_provider") + ] + for user_id in user_ids + ) + ) + # Resetting all principal investigator + response = self.client.patch(url, data={"resource_provider": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + resource_provider.get("pk") + for resource_provider in response.json().get("document").get("resource_provider") + ] + for user_id in user_ids + ) + ) + + def test_patch_originator(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"originator": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [originator.get("pk") for originator in response.json().get("document").get("originator")] + for user_id in user_ids + ) + ) + # Resetting all originators + response = self.client.patch(url, data={"originator": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [originator.get("pk") for originator in response.json().get("document").get("originator")] + for user_id in user_ids + ) + ) + + def test_patch_principal_investigator(self): + layer = Document.objects.first() + url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}") + self.client.login(username="admin", password="admin") + get_user_model().objects.get_or_create(username="ninja") + get_user_model().objects.get_or_create(username="turtle") + users = get_user_model().objects.exclude(pk=-1) + user_ids = [user.pk for user in users] + patch_data = {"principal_investigator": [{"id": uid} for uid in user_ids]} + response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + in [ + principal_investigator.get("pk") + for principal_investigator in response.json().get("document").get("principal_investigator") + ] + for user_id in user_ids + ) + ) + # Resetting all principal investigator + response = self.client.patch(url, data={"principal_investigator": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + principal_investigator.get("pk") + for principal_investigator in response.json().get("document").get("principal_investigator") + ] + for user_id in user_ids + ) + ) \ No newline at end of file diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index 5f8343ad1de..b673d697222 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -318,129 +318,236 @@ def test_patch_point_of_contact(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"poc": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.poc), len(user_ids)) - self.assertTrue(all(poc.pk in user_ids for poc in layer.poc)) + self.assertTrue( + all(user_id in [poc.get("pk") for poc in response.json().get("dataset").get("poc")] for user_id in user_ids) + ) + # Resetting all point of contact + response = self.client.patch(url, data={"poc": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [poc.get("pk") for poc in response.json().get("dataset").get("poc")] + for user_id in user_ids + ) + ) def test_patch_metadata_author(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"metadata_author": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.metadata_author), len(user_ids)) - self.assertTrue(all(metadata_author.pk in user_ids for metadata_author in layer.metadata_author)) + self.assertTrue( + all( + user_id + in [ + metadata_author.get("pk") + for metadata_author in response.json().get("dataset").get("metadata_author") + ] + for user_id in user_ids + ) + ) + # Resetting all metadata authors + response = self.client.patch(url, data={"metadata_author": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + metadata_author.get("pk") + for metadata_author in response.json().get("dataset").get("metadata_author") + ] + for user_id in user_ids + ) + ) def test_patch_processor(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"processor": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.processor), len(user_ids)) - self.assertTrue(all(processor.pk in user_ids for processor in layer.processor)) + self.assertTrue( + all( + user_id in [processor.get("pk") for processor in response.json().get("dataset").get("processor")] + for user_id in user_ids + ) + ) + # Resetting all processors + response = self.client.patch(url, data={"processor": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [processor.get("pk") for processor in response.json().get("dataset").get("processor")] + for user_id in user_ids + ) + ) def test_patch_publisher(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"publisher": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.publisher), len(user_ids)) - self.assertTrue(all(publisher.pk in user_ids for publisher in layer.publisher)) + self.assertTrue( + all( + user_id in [publisher.get("pk") for publisher in response.json().get("dataset").get("publisher")] + for user_id in user_ids + ) + ) + # Resetting all publishers + response = self.client.patch(url, data={"publisher": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [publisher.get("pk") for publisher in response.json().get("dataset").get("publisher")] + for user_id in user_ids + ) + ) def test_patch_custodian(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"custodian": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.custodian), len(user_ids)) - self.assertTrue(all(custodian.pk in user_ids for custodian in layer.custodian)) + self.assertTrue( + all( + user_id in [custodian.get("pk") for custodian in response.json().get("dataset").get("custodian")] + for user_id in user_ids + ) + ) + # Resetting all custodians + response = self.client.patch(url, data={"custodian": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [custodian.get("pk") for custodian in response.json().get("dataset").get("custodian")] + for user_id in user_ids + ) + ) def test_patch_distributor(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"distributor": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id in [distributor.get("pk") for distributor in response.json().get("dataset").get("distributor")] + for user_id in user_ids + ) + ) + # Resetting all distributers + response = self.client.patch(url, data={"distributor": []}, format="json") self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.distributor), len(user_ids)) - self.assertTrue(all(distributor.pk in user_ids for distributor in layer.distributor)) + self.assertTrue( + all( + user_id + not in [distributor.get("pk") for distributor in response.json().get("dataset").get("distributor")] + for user_id in user_ids + ) + ) def test_patch_resource_user(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"resource_user": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.resource_user), len(user_ids)) - self.assertTrue(all(resource_user.pk in user_ids for resource_user in layer.resource_user)) + self.assertTrue( + all( + user_id + in [resource_user.get("pk") for resource_user in response.json().get("dataset").get("resource_user")] + for user_id in user_ids + ) + ) + # Resetting all resource users + response = self.client.patch(url, data={"resource_user": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + resource_user.get("pk") for resource_user in response.json().get("dataset").get("resource_user") + ] + for user_id in user_ids + ) + ) def test_patch_resource_provider(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"resource_provider": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.resource_provider), len(user_ids)) - self.assertTrue(all(resource_provider.pk in user_ids for resource_provider in layer.resource_provider)) + self.assertTrue( + all( + user_id + in [ + resource_provider.get("pk") + for resource_provider in response.json().get("dataset").get("resource_provider") + ] + for user_id in user_ids + ) + ) + # Resetting all principal investigator + response = self.client.patch(url, data={"resource_provider": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + resource_provider.get("pk") + for resource_provider in response.json().get("dataset").get("resource_provider") + ] + for user_id in user_ids + ) + ) def test_patch_originator(self): layer = Dataset.objects.first() @@ -453,27 +560,56 @@ def test_patch_originator(self): user_ids = [user.pk for user in users] patch_data = {"originator": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.originator), len(user_ids)) - self.assertTrue(all(originator.pk in user_ids for originator in layer.originator)) + self.assertTrue( + all( + user_id in [originator.get("pk") for originator in response.json().get("dataset").get("originator")] + for user_id in user_ids + ) + ) + # Resetting all originators + response = self.client.patch(url, data={"originator": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id not in [originator.get("pk") for originator in response.json().get("dataset").get("originator")] + for user_id in user_ids + ) + ) def test_patch_principal_investigator(self): layer = Dataset.objects.first() url = urljoin(f"{reverse('datasets-list')}/", f"{layer.id}") self.client.login(username="admin", password="admin") - get_user_model().objects.get_or_create(username="ninja") get_user_model().objects.get_or_create(username="turtle") users = get_user_model().objects.exclude(pk=-1) user_ids = [user.pk for user in users] patch_data = {"principal_investigator": [{"id": uid} for uid in user_ids]} response = self.client.patch(url, data=patch_data, format="json") - self.assertEqual(200, response.status_code) - self.assertEqual(len(layer.principal_investigator), len(user_ids)) self.assertTrue( - all(principal_investigator.pk in user_ids for principal_investigator in layer.principal_investigator) + all( + user_id + in [ + principal_investigator.get("pk") + for principal_investigator in response.json().get("dataset").get("principal_investigator") + ] + for user_id in user_ids + ) + ) + # Resetting all principal investigator + response = self.client.patch(url, data={"principal_investigator": []}, format="json") + self.assertEqual(200, response.status_code) + self.assertTrue( + all( + user_id + not in [ + principal_investigator.get("pk") + for principal_investigator in response.json().get("dataset").get("principal_investigator") + ] + for user_id in user_ids + ) ) def test_metadata_update_for_not_supported_method(self): diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 35efff19a1e..2b630da51f4 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -51,6 +51,7 @@ from ..documents.models import Document from ..documents.enumerations import DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP from ..people.utils import get_valid_user +from geonode.people import Roles from ..layers.utils import resolve_regions from ..layers.metadata import convert_keyword @@ -181,8 +182,9 @@ def update_resource( else: defaults[key] = value - poc = defaults.pop("poc", None) - metadata_author = defaults.pop("metadata_author", None) + contact_roles = { + contact_role.name: defaults.pop(contact_role.name, None) for contact_role in Roles.get_multivalue_ones() + } to_update = {} for _key in ("name",): From b95a2a84c1a5f9fac0a0cfef73ffccb4f8ae2264 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 3 Apr 2023 13:40:49 +0200 Subject: [PATCH 27/49] black --- geonode/documents/api/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index c95f3371c85..e0f3d8cd233 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -447,4 +447,4 @@ def test_patch_principal_investigator(self): ] for user_id in user_ids ) - ) \ No newline at end of file + ) From 315cbd9f67f45f42f46bf6ee56f0ee9e7c1d7022 Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Mon, 3 Apr 2023 14:51:57 +0200 Subject: [PATCH 28/49] black --- geonode/layers/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 4d85c63ea1b..cb02bf9afcf 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -524,7 +524,6 @@ def dataset_metadata( and tkeywords_form.is_valid() and timeseries_form.is_valid() ): - new_category = None if ( category_form From 0dfa9cc34b72128ca2036681bd535b759d939d98 Mon Sep 17 00:00:00 2001 From: ahmdthr Date: Mon, 3 Apr 2023 15:20:14 +0200 Subject: [PATCH 29/49] Fixed AttributeError with TaggitProfileSelect2Custom --- geonode/base/widgets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py index af21da26c39..b5d138b5df8 100644 --- a/geonode/base/widgets.py +++ b/geonode/base/widgets.py @@ -31,7 +31,10 @@ def value_from_datadict(self, data, files, name): returns list of selected elements """ try: - ret_list = data.getlist(name) + try: + ret_list = data.getlist(name) + except AttributeError: + ret_list = data[name] return ret_list except KeyError: return [] From 14820c10a05ff5c651f6dca5135bdffbe091811a Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Fri, 23 Jun 2023 15:07:05 +0200 Subject: [PATCH 30/49] WIP bonares metadata schema implementation --- .devcontainer/.env | 3 +- entrypoint.sh | 3 +- examples/create_funder.py | 19 +++ examples/create_related_identifier.py | 16 +++ examples/test_rest.py | 18 +++ geonode/base/api/fields.py | 39 +++--- geonode/base/api/serializers.py | 81 +++++++++--- geonode/base/api/views.py | 22 ++-- geonode/base/forms.py | 90 ++++++++++++-- .../migrations/0085_auto_20221114_1000.py | 94 -------------- .../migrations/0086_auto_20230324_1219.py | 116 ++++++++++++++++++ .../migrations/0087_auto_20230329_1513.py | 26 ++++ .../migrations/0088_auto_20230330_0640.py | 39 ++++++ .../0089_alter_resourcebase_use_constrains.py | 19 +++ .../migrations/0090_auto_20230330_0718.py | 31 +++++ .../migrations/0091_auto_20230331_0757.py | 28 +++++ ...ter_resourcebase_conformity_explanation.py | 19 +++ .../0093_resourcebase_parent_identifier.py | 19 +++ .../migrations/0094_auto_20230331_0918.py | 59 +++++++++ .../migrations/0095_auto_20230331_0923.py | 43 +++++++ .../0037_alter_document_abstract_en.py | 18 +++ geonode/layers/api/urls.py | 2 +- .../0045_alter_dataset_abstract_en.py | 18 +++ geonode/layers/models.py | 20 +++ .../migrations/0043_alter_map_abstract_en.py | 18 +++ .../0037_profile_orcid_identifier.py | 18 +++ geonode/people/models.py | 15 +++ 27 files changed, 738 insertions(+), 155 deletions(-) create mode 100644 examples/create_funder.py create mode 100644 examples/create_related_identifier.py create mode 100644 examples/test_rest.py delete mode 100644 geonode/base/migrations/0085_auto_20221114_1000.py create mode 100644 geonode/base/migrations/0086_auto_20230324_1219.py create mode 100644 geonode/base/migrations/0087_auto_20230329_1513.py create mode 100644 geonode/base/migrations/0088_auto_20230330_0640.py create mode 100644 geonode/base/migrations/0089_alter_resourcebase_use_constrains.py create mode 100644 geonode/base/migrations/0090_auto_20230330_0718.py create mode 100644 geonode/base/migrations/0091_auto_20230331_0757.py create mode 100644 geonode/base/migrations/0092_alter_resourcebase_conformity_explanation.py create mode 100644 geonode/base/migrations/0093_resourcebase_parent_identifier.py create mode 100644 geonode/base/migrations/0094_auto_20230331_0918.py create mode 100644 geonode/base/migrations/0095_auto_20230331_0923.py create mode 100644 geonode/documents/migrations/0037_alter_document_abstract_en.py create mode 100644 geonode/layers/migrations/0045_alter_dataset_abstract_en.py create mode 100644 geonode/maps/migrations/0043_alter_map_abstract_en.py create mode 100644 geonode/people/migrations/0037_profile_orcid_identifier.py diff --git a/.devcontainer/.env b/.devcontainer/.env index bc5837e55d1..2713c3db517 100644 --- a/.devcontainer/.env +++ b/.devcontainer/.env @@ -43,7 +43,6 @@ ASYNC_SIGNALS=True SITEURL=http://localhost:8000/ ALLOWED_HOSTS="['django', '*']" -PORXY_URL="" # Data Uploader DEFAULT_BACKEND_UPLOADER=geonode.importer @@ -165,7 +164,7 @@ SECRET_KEY='myv-y4#7j-d*p-__@j#*3z@!y24fz8%^z2v6atuy4bo9vqr1_a' # GEOIP_PATH=/mnt/volumes/statics/geoip.db CACHE_BUSTING_STATIC_ENABLED=False -PROXY_URL="" + MEMCACHED_ENABLED=False MEMCACHED_BACKEND=django.core.cache.backends.memcached.MemcachedCache MEMCACHED_LOCATION=127.0.0.1:11211 diff --git a/entrypoint.sh b/entrypoint.sh index b32aaaefd80..30e506ccec5 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -16,7 +16,6 @@ invoke () { # Start cron && memcached services service cron restart -service memcached restart echo $"\n\n\n" echo "-----------------------------------------------------" @@ -64,8 +63,8 @@ else invoke statics invoke waitforgeoserver invoke geoserverfixture + echo "Executing UWSGI server $cmd for Production" - #invoke initzalf fi echo "-----------------------------------------------------" diff --git a/examples/create_funder.py b/examples/create_funder.py new file mode 100644 index 00000000000..0a19d4e7984 --- /dev/null +++ b/examples/create_funder.py @@ -0,0 +1,19 @@ +# create funder example + +import sys, os, django +sys.path.append("/") #here store is root folder(means parent). +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "store.settings") +django.setup() + +from geonode.base.models import FundingReference, Funder, ResourceBase + +fr = FundingReference.objects.all()[0] +f = Funder( funding_reference=fr, + award_number="25132", + award_uri="http://cordis.europa.eu/project/rcn/100180_en.html", + award_title="The human readable title of the award (grant). (e.g. MOTivational strength of ecosystem service)") + +r = ResourceBase.objects.all()[0] +r.funders.add(f) +r.save() + diff --git a/examples/create_related_identifier.py b/examples/create_related_identifier.py new file mode 100644 index 00000000000..f85611de34c --- /dev/null +++ b/examples/create_related_identifier.py @@ -0,0 +1,16 @@ + +import sys, os, django +sys.path.append("/") #here store is root folder(means parent). +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "store.settings") +django.setup() + +from geonode.base.models import RelationType, RelatedIdentifier, ResourceBase, RelatedIdentifierType + +rt = RelationType.objects.all()[0] +rit = RelatedIdentifierType.objects.all()[0] +ri = RelatedIdentifier.objects.create(related_identifier="test", + related_identifier_type=rit, + relation_type=rt + ) +rb = ResourceBase.objects.all()[0] +rb.related_identifier.add(ri) \ No newline at end of file diff --git a/examples/test_rest.py b/examples/test_rest.py new file mode 100644 index 00000000000..8cfa3feba7a --- /dev/null +++ b/examples/test_rest.py @@ -0,0 +1,18 @@ +import sys, os, django +sys.path.append("/") #here store is root folder(means parent). +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "store.settings") +django.setup() + +import io +from rest_framework.renderers import JSONRenderer +from geonode.base.models import ResourceBase +from geonode.base.api.serializers import ResourceBaseSerializer +from rest_framework.parsers import JSONParser + +r = ResourceBase.objects.all()[0] +s = ResourceBaseSerializer(r) +json = JSONRenderer().render(s.data) +stream = io.BytesIO(json) +data = JSONParser().parse(stream) +se = ResourceBaseSerializer(data=data) +se.is_valid() diff --git a/geonode/base/api/fields.py b/geonode/base/api/fields.py index 1339d0bc9f7..84283af29f4 100644 --- a/geonode/base/api/fields.py +++ b/geonode/base/api/fields.py @@ -23,34 +23,30 @@ from rest_framework.exceptions import ParseError from dynamic_rest.fields.fields import DynamicRelationField -from geonode.base.models import ( - RelatedIdentifierType, - RelationType, - RelatedIdentifier, - FundingReference, - Funder - ) +from geonode.base.models import RelatedIdentifierType, RelationType, RelatedIdentifier, FundingReference, Funder -class RelatedIdentifierDynamicRelationField(DynamicRelationField): +class RelatedIdentifierDynamicRelationField(DynamicRelationField): def to_internal_value_single(self, data, serializer): try: - rit = RelatedIdentifierType.objects.get(**data['related_identifier_type']) - rt = RelationType.objects.get(**data['relation_type']) - RelatedIdentifier.objects.get_or_create(related_identifier=data['related_identifier'], - related_identifier_type=rit, - relation_type=rt)[0].save() - r = RelatedIdentifier.objects.get(related_identifier=data['related_identifier'], related_identifier_type=rit, relation_type=rt) + rit = RelatedIdentifierType.objects.get(**data["related_identifier_type"]) + rt = RelationType.objects.get(**data["relation_type"]) + RelatedIdentifier.objects.get_or_create( + related_identifier=data["related_identifier"], related_identifier_type=rit, relation_type=rt + )[0].save() + r = RelatedIdentifier.objects.get( + related_identifier=data["related_identifier"], related_identifier_type=rit, relation_type=rt + ) except TypeError: raise ParseError(detail="Could not convert related_identifier to internal object ...", code=400) - return r + return r -class FundersDynamicRelationField(DynamicRelationField): +class FundersDynamicRelationField(DynamicRelationField): def to_internal_value_single(self, data, serializer): try: - funding_reference = FundingReference.objects.get(**data['funding_reference']) - data['funding_reference'] = funding_reference + funding_reference = FundingReference.objects.get(**data["funding_reference"]) + data["funding_reference"] = funding_reference except TypeError: raise ParseError(detail="Missing funding_reference object in funders ...", code=400) try: @@ -58,15 +54,14 @@ def to_internal_value_single(self, data, serializer): except TypeError: raise ParseError(detail="Could not convert related_identifier to internal object ...", code=400) return funder - + class ComplexDynamicRelationField(DynamicRelationField): - def to_internal_value_single(self, data, serializer): """Overwrite of DynamicRelationField implementation to handle complex data structure initialization Args: - data (Union[str, Dict]}): serialized or deserialized data from http calls (POST, GET ...), + data (Union[str, Dict]}): serialized or deserialized data from http calls (POST, GET ...), if content-type application/json is used, data shows up as dict serializer (DynamicModelSerializer): Serializer for the given data @@ -81,7 +76,7 @@ def to_internal_value_single(self, data, serializer): data = json.loads(data) except ValueError: return super().to_internal_value_single(data, serializer) - + if isinstance(data, dict): try: if hasattr(serializer, "many") and serializer.many is True: diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 6d80dc9aaf2..765b4f429c4 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -57,7 +57,11 @@ Funder, ) from geonode.groups.models import GroupCategory, GroupProfile -from geonode.base.api.fields import ComplexDynamicRelationField, RelatedIdentifierDynamicRelationField, FundersDynamicRelationField +from geonode.base.api.fields import ( + ComplexDynamicRelationField, + RelatedIdentifierDynamicRelationField, + FundersDynamicRelationField, +) from geonode.utils import build_absolute_uri from geonode.security.utils import get_resources_with_perms from geonode.resource.models import ExecutionRequest @@ -66,6 +70,7 @@ logger = logging.getLogger(__name__) + class BaseDynamicModelSerializer(DynamicModelSerializer): def to_representation(self, instance): data = super().to_representation(instance) @@ -159,7 +164,6 @@ def to_representation(self, value): return {"name": value.name, "slug": value.slug} - class _ThesaurusKeywordSerializerMixIn: def to_representation(self, value): _i18n = {} @@ -183,7 +187,7 @@ class Meta: model = ThesaurusKeyword name = "ThesaurusKeyword" fields = ("alt_label",) - + class SimpleRegionSerializer(DynamicModelSerializer): class Meta: @@ -197,7 +201,7 @@ class Meta: model = RelatedIdentifierType name = "RelatedIdentifierType" fields = ("label", "description") - + class SimpleRelationType(DynamicModelSerializer): class Meta: @@ -214,8 +218,8 @@ class Meta: related_identifier_type = DynamicRelationField(SimpleRelatedIdentifierType, embed=True, many=False) relation_type = DynamicRelationField(SimpleRelationType, embed=True, many=False) - - + + class FundingReferenceSerializer(DynamicModelSerializer): class Meta: model = FundingReference @@ -236,7 +240,14 @@ class SimpleRelatedProjectSerializer(DynamicModelSerializer): class Meta: model = RelatedProject name = "RelatedProject" - fields = ("label","display_name") + fields = ("label", "display_name") + + +class SimpleResourceSerializer(DynamicModelSerializer): + class Meta: + model = ResourceBase + name = "resource" + fields = ("pk", "title") # TODO add UUID class SimpleTopicCategorySerializer(DynamicModelSerializer): @@ -357,7 +368,7 @@ class Meta: model = get_user_model() name = "user" view_name = "users-list" - fields = ("pk", "username", "first_name", "last_name", "avatar", "perms", "is_superuser", "is_staff") + fields = ("pk", "username", "first_name", "last_name", "avatar", "perms", "is_superuser", "is_staff", "orcid_identifier") @classmethod def setup_eager_loading(cls, queryset): @@ -483,6 +494,7 @@ def __init__(self, *args, **kwargs): self.fields["title_translated"] = serializers.CharField() self.fields["abstract"] = serializers.CharField(required=False) + # BONARES ELEMENTS self.fields["abstract_translated"] = serializers.CharField(required=False) self.fields["subtitle"] = serializers.CharField(required=False) @@ -493,13 +505,30 @@ def __init__(self, *args, **kwargs): self.fields["other_description"] = serializers.CharField(required=False) self.fields["related_identifier"] = RelatedIdentifierDynamicRelationField( - SimpleRelatedIdentifierSerializer, embed=True, many=True) - self.fields["funders"] = FundersDynamicRelationField( - SimpleFunderSerializer, embed=True, many=True) + SimpleRelatedIdentifierSerializer, embed=True, many=True + ) + self.fields["funders"] = FundersDynamicRelationField(SimpleFunderSerializer, embed=True, many=True) self.fields["related_projects"] = ComplexDynamicRelationField( - SimpleRelatedProjectSerializer, embed=True, many=True) + SimpleRelatedProjectSerializer, embed=True, many=True + ) + self.fields["conformity_results"] = serializers.CharField(required=False) + self.fields["conformity_explanation"] = serializers.CharField(required=False) + self.fields["parent_identifier"] = ComplexDynamicRelationField( + SimpleResourceSerializer, embed=True, many=False, required=False + ) + self.fields["date_available"] = serializers.DateField(required=True) + self.fields["date_updated"] = serializers.DateField(required=True) + self.fields["date_created"] = serializers.DateField(required=True) + self.fields["date_issued"] = serializers.DateField(required=True) + + self.fields["date_accepted"] = serializers.DateField(required=False) + self.fields["date_collected"] = serializers.DateField(required=False) + self.fields["date_copyrighted"] = serializers.DateField(required=False) + self.fields["date_submitted"] = serializers.DateField(required=False) + self.fields["date_valid"] = serializers.DateField(required=False) - + self.fields["metadata_standard_name"] = serializers.CharField(read_only=True) + self.fields["metadata_standard_version"] = serializers.CharField(read_only=True) self.fields["attribution"] = serializers.CharField(required=False) self.fields["doi"] = serializers.CharField(required=False) @@ -540,7 +569,9 @@ def __init__(self, *args, **kwargs): self.fields["embed_url"] = EmbedUrlField(required=False) self.fields["thumbnail_url"] = ThumbnailUrlField(read_only=True) - self.fields["keywords"] = ComplexDynamicRelationField(SimpleHierarchicalKeywordSerializer, embed=False, many=True) + self.fields["keywords"] = ComplexDynamicRelationField( + SimpleHierarchicalKeywordSerializer, embed=False, many=True + ) self.fields["tkeywords"] = ComplexDynamicRelationField(SimpleThesaurusKeywordSerializer, embed=False, many=True) self.fields["regions"] = DynamicRelationField(SimpleRegionSerializer, embed=True, many=True, read_only=True) self.fields["category"] = ComplexDynamicRelationField(SimpleTopicCategorySerializer, embed=True, many=False) @@ -552,7 +583,7 @@ def __init__(self, *args, **kwargs): ) self.fields["license"] = ComplexDynamicRelationField(LicenseSerializer, embed=True, many=False) self.fields["spatial_representation_type"] = ComplexDynamicRelationField( - SpatialRepresentationTypeSerializer, embed=True, many=False + SpatialRepresentationTypeSerializer, embed=True, many=False ) self.fields["blob"] = serializers.JSONField(required=False, write_only=True) self.fields["is_copyable"] = serializers.BooleanField(read_only=True) @@ -594,6 +625,20 @@ class Meta: "other_description", "related_identifier", "funders", + "conformity_results", + "conformity_explanation", + "parent_identifier", + "date_accepted", + "date_available", + "date_collected", + "date_copyrighted", + "date_created", + "date_issued", + "date_submitted", + "date_updated", + "date_valid", + "metadata_standard_name", + "metadata_standard_version", "doi", "bbox_polygon", "ll_bbox_polygon", @@ -782,8 +827,8 @@ class Meta: count_type = "category" view_name = "categories-list" fields = "__all__" - - + + class RelationTypeSerializer(DynamicModelSerializer): class Meta: name = "relationtypes" @@ -807,6 +852,7 @@ class Meta: count_type = "fundingreferences" fields = "__all__" + class RelatedProjectSerializer(DynamicModelSerializer): class Meta: name = "relatedprojects" @@ -814,6 +860,7 @@ class Meta: count_type = "relatedprojects" fields = "__all__" + class OwnerSerializer(BaseResourceCountSerializer): class Meta: name = "owners" diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 15b9f13df92..3706e670211 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -66,15 +66,15 @@ from geonode.thumbs.utils import _decode_base64, BASE64_PATTERN from geonode.groups.conf import settings as groups_settings from geonode.base.models import ( - HierarchicalKeyword, - Region, - ResourceBase, - TopicCategory, - ThesaurusKeyword, - RelationType, - RelatedIdentifierType, - FundingReference, - RelatedProject + HierarchicalKeyword, + Region, + ResourceBase, + TopicCategory, + ThesaurusKeyword, + RelationType, + RelatedIdentifierType, + FundingReference, + RelatedProject, ) from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter, FacetVisibleResourceFilter, FavoriteFilter from geonode.groups.models import GroupProfile, GroupMember @@ -329,6 +329,7 @@ class RelationTypeViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModel serializer_class = RelationTypeSerializer pagination_class = GeoNodeApiPagination + class RelatedIdentifierTypeViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): """ API endpoint that lists relatedidentifiertypes. @@ -342,6 +343,7 @@ class RelatedIdentifierTypeViewSet(WithDynamicViewSetMixin, ListModelMixin, Retr serializer_class = RelatedIdentifierTypeSerializer pagination_class = GeoNodeApiPagination + class FundingReferenceViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): """ API endpoint that lists fundingreference. @@ -355,6 +357,7 @@ class FundingReferenceViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveM serializer_class = FundingReferenceSerializer pagination_class = GeoNodeApiPagination + class RelatedProjectViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): """ API endpoint that lists relatedprojects. @@ -369,7 +372,6 @@ class RelatedProjectViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveMod pagination_class = GeoNodeApiPagination -RelatedProjectSerializer class OwnerViewSet(WithDynamicViewSetMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): """ API endpoint that lists all possible owners. diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 9c88afa3544..ab20f848eb1 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -360,15 +360,91 @@ class ResourceBaseForm(TranslationModelForm): """Base form for metadata, should be inherited by childres classes of ResourceBase""" abstract = forms.CharField(label=_("Abstract"), required=False, widget=TinyMCE()) - abstract_translated = forms.CharField(label=_("translated abstract"), help_text=ResourceBase.abstract_translated_help_text ,required=False, widget=TinyMCE()) + abstract_translated = forms.CharField( + label=_("translated abstract"), + help_text=ResourceBase.abstract_translated_help_text, + required=True, + widget=TinyMCE(), + ) - subtitle = forms.CharField(required=False, help_text=ResourceBase.subtitle_help_text ,widget=TinyMCE()) - method_description = forms.CharField(required=False, help_text=ResourceBase.method_description_help_text, widget=TinyMCE()) - series_information = forms.CharField(required=False, help_text=ResourceBase.series_information_help_text, widget=TinyMCE()) - table_of_content = forms.CharField(required=False, help_text=ResourceBase.table_of_content_help_text, widget=TinyMCE()) + subtitle = forms.CharField(required=False, help_text=ResourceBase.subtitle_help_text, widget=TinyMCE()) + method_description = forms.CharField( + required=False, help_text=ResourceBase.method_description_help_text, widget=TinyMCE() + ) + series_information = forms.CharField( + required=False, help_text=ResourceBase.series_information_help_text, widget=TinyMCE() + ) + table_of_content = forms.CharField( + required=False, help_text=ResourceBase.table_of_content_help_text, widget=TinyMCE() + ) technical_info = forms.CharField(required=False, help_text=ResourceBase.technical_info_help_text, widget=TinyMCE()) - other_description = forms.CharField(required=False, help_text=ResourceBase.other_description_help_text, widget=TinyMCE()) - + other_description = forms.CharField( + required=False, help_text=ResourceBase.other_description_help_text, widget=TinyMCE() + ) + + date_available = forms.DateTimeField( + label=_("Date Available*"), + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + date_created = forms.DateTimeField( + label=_("Date Created*"), + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + date_issued = forms.DateTimeField( + label=_("Date Issued*"), + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + date_updated = forms.DateTimeField( + label=_("Date Updated"), + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + date_accepted = forms.DateTimeField( + label=_("Date Accepted"), + required=False, + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + + date_collected = forms.DateTimeField( + label=_("Date Collected"), + required=False, + localize=True, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + date_copyrighted = forms.DateTimeField( + label=_("Date Copyright"), + localize=True, + required=False, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + + date_submitted = forms.DateTimeField( + label=_("Date Submitted"), + localize=True, + required=False, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + + date_valid = forms.DateTimeField( + label=_("Date Valid"), + localize=True, + required=False, + input_formats=["%Y-%m-%d"], + widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD"}), + ) + purpose = forms.CharField(label=_("Purpose"), required=False, widget=TinyMCE()) constraints_other = forms.CharField(label=_("Other constraints"), required=False, widget=TinyMCE()) diff --git a/geonode/base/migrations/0085_auto_20221114_1000.py b/geonode/base/migrations/0085_auto_20221114_1000.py deleted file mode 100644 index 835b8ec0d00..00000000000 --- a/geonode/base/migrations/0085_auto_20221114_1000.py +++ /dev/null @@ -1,94 +0,0 @@ -# Generated by Django 3.2.16 on 2022-11-14 10:00 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('base', '0084_remove_comments_from_actions'), - ] - - operations = [ - migrations.CreateModel( - name='AlternateType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('alternate_type', models.CharField(choices=[('Alternative', ''), ('Subtitle', ''), ('Translated', ''), ('Other', '')], help_text='Alternate title type', max_length=255)), - ], - ), - migrations.CreateModel( - name='DescriptionType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description_type', models.CharField(choices=[('Methods', 'The methodology employed for the study or research'), ('SeriesInformation', 'Information about a repeating series, such as volumne, issue, number'), ('TableOfContents', 'A listing of the Table of Contents'), ('TechnicalInfo', 'Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system'), ('other', 'Other description information that does not fit into an existing category')], help_text='abstract description type', max_length=255)), - ], - ), - migrations.CreateModel( - name='FundingReference', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('funder_name', models.CharField(help_text='Name of the funding provider. (e.g. European Commission)', max_length=255)), - ('funder_identifier', models.CharField(help_text='Uniquely identifies a funding entity, according to various types. (e.g. http://doi.org/10.13039/501100000780)', max_length=255)), - ('funder_identifier_type', models.CharField(help_text='The type of the Identifier. (e.g. BMBF)', max_length=255)), - ('award_number', models.CharField(help_text='The code assigned by the funder to a sponsored award (grant). (e.g. 282625)', max_length=255)), - ('award_uri', models.CharField(help_text='The URI leading to a page provided by the funder for more information about the award (grant). (e.g. http://cordis.europa.eu/project/rcn/100180_en.html)', max_length=255)), - ('award_title', models.CharField(help_text='The human readable title of the award (grant). (e.g. MOTivational strength of ecosystem services)', max_length=255)), - ], - ), - migrations.CreateModel( - name='RelatedIdentifier', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('related_identifier', models.CharField(help_text='Name of the funding provider. (e.g. European Commission)', max_length=255)), - ('related_identifier_type', models.CharField(help_text='The type of the Related identifier. If Related identifier is used Identifier type is mandatory. (e.g. bibcode)', max_length=255)), - ('relation_type', models.CharField(help_text='Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory.', max_length=500)), - ], - ), - migrations.AddField( - model_name='resourcebase', - name='abstract_de', - field=models.TextField(blank=True, help_text='brief german narrative summary of the content of the resource(s)', max_length=2000, verbose_name='abstract_de'), - ), - migrations.AddField( - model_name='resourcebase', - name='parent_ressource', - field=models.ForeignKey(blank=True, help_text='Parent Dataset, this dataset belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.resourcebase'), - ), - migrations.AddField( - model_name='resourcebase', - name='title_de', - field=models.CharField(default='', help_text='german name by which the cited resource is known', max_length=255, verbose_name='title_de'), - ), - migrations.AddField( - model_name='resourcebase', - name='use_contraints', - field=models.TextField(blank=True, help_text='This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)', max_length=2000, verbose_name='use_constraints'), - ), - migrations.AlterField( - model_name='resourcebase', - name='date_type', - field=models.CharField(choices=[('publication', 'Publication')], default='publication', help_text='identification of when a given event occurred', max_length=255, verbose_name='date type'), - ), - migrations.AddField( - model_name='resourcebase', - name='alternate_type', - field=models.ForeignKey(help_text='Type of the alternate field', null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.alternatetype'), - ), - migrations.AddField( - model_name='resourcebase', - name='description_type', - field=models.ForeignKey(help_text='Descripion Type of abstract.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.descriptiontype'), - ), - migrations.AddField( - model_name='resourcebase', - name='funding_reference', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.fundingreference'), - ), - migrations.AddField( - model_name='resourcebase', - name='related_identifier', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.relatedidentifier'), - ), - ] diff --git a/geonode/base/migrations/0086_auto_20230324_1219.py b/geonode/base/migrations/0086_auto_20230324_1219.py new file mode 100644 index 00000000000..33396ff9b06 --- /dev/null +++ b/geonode/base/migrations/0086_auto_20230324_1219.py @@ -0,0 +1,116 @@ +# Generated by Django 3.2.16 on 2023-03-24 12:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0085_alter_resourcebase_uuid'), + ] + + operations = [ + migrations.CreateModel( + name='FundingReference', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('funder_name', models.CharField(help_text='Name of the funding provider. (e.g. European Commission)', max_length=255)), + ('funder_identifier', models.CharField(help_text='Uniquely identifies a funding entity, according to various types. (e.g. http://doi.org/10.13039/501100000780)', max_length=255)), + ('funder_identifier_type', models.CharField(help_text='The type of the Identifier. (e.g. BMBF)', max_length=255)), + ], + ), + migrations.CreateModel( + name='RelatedIdentifierType', + fields=[ + ('label', models.CharField(help_text='related identifier, available identifier types', max_length=255, primary_key=True, serialize=False, unique=True)), + ('description', models.CharField(help_text='label description', max_length=255)), + ], + ), + migrations.CreateModel( + name='RelationType', + fields=[ + ('label', models.CharField(help_text='Description of the relationship of the resource being registered (A) and the related resource (B). If Related identifier is used Relation type is mandatory', max_length=255, primary_key=True, serialize=False, unique=True)), + ('description', models.CharField(help_text='label description', max_length=255)), + ], + ), + migrations.AddField( + model_name='resourcebase', + name='abstract_translated', + field=models.TextField(blank=True, help_text='brief german narrative summary of the content of the resource(s)', max_length=2000, verbose_name='abstract_translated'), + ), + migrations.AddField( + model_name='resourcebase', + name='method_description', + field=models.TextField(blank=True, help_text='The methodology employed for the study or research', max_length=2000, verbose_name='method_description'), + ), + migrations.AddField( + model_name='resourcebase', + name='other_description', + field=models.TextField(blank=True, help_text='Other description information that does not fit into an existing category', max_length=2000, verbose_name='other_description'), + ), + migrations.AddField( + model_name='resourcebase', + name='series_information', + field=models.TextField(blank=True, help_text='Information about a repeating series, such as volumne, issue, number', max_length=2000, verbose_name='series_information'), + ), + migrations.AddField( + model_name='resourcebase', + name='subtitle', + field=models.TextField(blank=True, help_text='subtitle of the dataset', max_length=400, verbose_name='subtitle'), + ), + migrations.AddField( + model_name='resourcebase', + name='table_of_content', + field=models.TextField(blank=True, help_text='A listing of the Table of Contents', max_length=2000, verbose_name='table_of_content'), + ), + migrations.AddField( + model_name='resourcebase', + name='technical_info', + field=models.TextField(blank=True, help_text='Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process or system', max_length=2000, verbose_name='technical_info'), + ), + migrations.AddField( + model_name='resourcebase', + name='title_translated', + field=models.CharField(blank=True, help_text='german name by which the cited resource is known', max_length=255, verbose_name='title_translated'), + ), + migrations.AddField( + model_name='resourcebase', + name='use_contraints', + field=models.TextField(blank=True, help_text='This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)', max_length=2000, verbose_name='use_constraints'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_type', + field=models.CharField(choices=[('publication', 'Publication')], default='publication', help_text='identification of when a given event occurred', max_length=255, verbose_name='date type'), + ), + migrations.CreateModel( + name='RelatedIdentifier', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('related_identifier', models.CharField(help_text='Identifiers of related resources. These must be globally unique identifiers.', max_length=255)), + ('related_identifier_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.relatedidentifiertype')), + ('relation_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.relationtype')), + ], + ), + migrations.CreateModel( + name='Funder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('award_number', models.CharField(help_text='The code assigned by the funder to a sponsored award (grant). (e.g. 282625)', max_length=255)), + ('award_uri', models.CharField(help_text='The URI leading to a page provided by the funder for more information about the award (grant). (e.g. http://cordis.europa.eu/project/rcn/100180_en.html)', max_length=255)), + ('award_title', models.CharField(help_text='The human readable title of the award (grant). (e.g. MOTivational strength of ecosystem services)', max_length=255)), + ('funding_reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.fundingreference')), + ], + ), + migrations.AddField( + model_name='resourcebase', + name='funders', + field=models.ManyToManyField(blank=True, help_text='List of funders, funded dataset creators', null=True, to='base.Funder', verbose_name='Funder names'), + ), + migrations.AddField( + model_name='resourcebase', + name='related_identifier', + field=models.ManyToManyField(blank=True, help_text='Identifiers of related resources. These must be globally unique identifiers.', null=True, to='base.RelatedIdentifier', verbose_name='Related Identifier'), + ), + ] diff --git a/geonode/base/migrations/0087_auto_20230329_1513.py b/geonode/base/migrations/0087_auto_20230329_1513.py new file mode 100644 index 00000000000..6a08868d400 --- /dev/null +++ b/geonode/base/migrations/0087_auto_20230329_1513.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.18 on 2023-03-29 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0086_auto_20230324_1219'), + ] + + operations = [ + migrations.CreateModel( + name='RelatedProject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(help_text='label of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)', max_length=255, unique=True)), + ('display_name', models.CharField(help_text='Name of the hierarchy levels for which the metadata is provided. (e.g. signal)', max_length=255)), + ], + ), + migrations.AddField( + model_name='resourcebase', + name='related_projects', + field=models.ManyToManyField(blank=True, help_text='Name of the hierarchy levels for which the metadata is provided. (e.g. SIGNAL)', null=True, to='base.RelatedProject', verbose_name='related project'), + ), + ] diff --git a/geonode/base/migrations/0088_auto_20230330_0640.py b/geonode/base/migrations/0088_auto_20230330_0640.py new file mode 100644 index 00000000000..d5d7b0ca149 --- /dev/null +++ b/geonode/base/migrations/0088_auto_20230330_0640.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.18 on 2023-03-30 06:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0087_auto_20230329_1513'), + ] + + operations = [ + migrations.AddField( + model_name='resourcebase', + name='use_constrains', + field=models.ForeignKey(default=None, help_text='This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)', limit_choices_to=models.Q(('is_choice', True)), on_delete=django.db.models.deletion.CASCADE, related_name='use_constrains', to='base.restrictioncodetype', verbose_name='use_constrains'), + ), + migrations.AlterField( + model_name='resourcebase', + name='abstract', + field=models.TextField(help_text='brief narrative summary of the content of the resource(s)', max_length=2000, verbose_name='abstract'), + ), + migrations.AlterField( + model_name='resourcebase', + name='abstract_translated', + field=models.TextField(help_text='brief german narrative summary of the content of the resource(s)', max_length=2000, verbose_name='abstract_translated'), + ), + migrations.AlterField( + model_name='resourcebase', + name='restriction_code_type', + field=models.ForeignKey(blank=True, help_text='limitation(s) placed upon the access or use of the data.', limit_choices_to=models.Q(('is_choice', True)), null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='restriction_code_type', to='base.restrictioncodetype', verbose_name='restrictions'), + ), + migrations.AlterField( + model_name='resourcebase', + name='title_translated', + field=models.CharField(help_text='german name by which the cited resource is known', max_length=255, verbose_name='title_translated'), + ), + ] diff --git a/geonode/base/migrations/0089_alter_resourcebase_use_constrains.py b/geonode/base/migrations/0089_alter_resourcebase_use_constrains.py new file mode 100644 index 00000000000..ebdd2bfb7a7 --- /dev/null +++ b/geonode/base/migrations/0089_alter_resourcebase_use_constrains.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.18 on 2023-03-30 07:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0088_auto_20230330_0640'), + ] + + operations = [ + migrations.AlterField( + model_name='resourcebase', + name='use_constrains', + field=models.ForeignKey(blank=True, help_text='This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)', limit_choices_to=models.Q(('is_choice', True)), null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='use_constrains', to='base.restrictioncodetype', verbose_name='use_constrains'), + ), + ] diff --git a/geonode/base/migrations/0090_auto_20230330_0718.py b/geonode/base/migrations/0090_auto_20230330_0718.py new file mode 100644 index 00000000000..9ecfb28a725 --- /dev/null +++ b/geonode/base/migrations/0090_auto_20230330_0718.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.18 on 2023-03-30 07:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0089_alter_resourcebase_use_constrains'), + ] + + operations = [ + migrations.RemoveField( + model_name='resourcebase', + name='restriction_code_type', + ), + migrations.AddField( + model_name='resourcebase', + name='restriction_code_type', + field=models.ManyToManyField(blank=True, help_text='limitation(s) placed upon the access or use of the data.', limit_choices_to=models.Q(('is_choice', True)), null=True, related_name='restriction_code_type', to='base.RestrictionCodeType', verbose_name='restrictions'), + ), + migrations.RemoveField( + model_name='resourcebase', + name='use_constrains', + ), + migrations.AddField( + model_name='resourcebase', + name='use_constrains', + field=models.ManyToManyField(blank=True, help_text='This metadata element shall provide information on the Use constraints applied to assure the protection of privacy or intellectual property (e.g. Trademark)', limit_choices_to=models.Q(('is_choice', True)), null=True, related_name='use_constrains', to='base.RestrictionCodeType', verbose_name='use_constrains'), + ), + ] diff --git a/geonode/base/migrations/0091_auto_20230331_0757.py b/geonode/base/migrations/0091_auto_20230331_0757.py new file mode 100644 index 00000000000..b3ed2f0bbda --- /dev/null +++ b/geonode/base/migrations/0091_auto_20230331_0757.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.18 on 2023-03-31 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0090_auto_20230330_0718'), + ] + + operations = [ + migrations.AddField( + model_name='resourcebase', + name='conformity_explanation', + field=models.CharField(blank=True, help_text='Give an Explanation about the conformity check. (e.g. See the referenced specification.', max_length=2000, null=True, verbose_name='conformity explanation'), + ), + migrations.AddField( + model_name='resourcebase', + name='conformity_results', + field=models.CharField(choices=[('Passed', 'Passed'), ('Not Passed', 'Not Passed'), ('Unknown', 'Unknown')], default='Unknown', help_text='This is the degree of conformity of the dataset to the implementing rules the BonaRes Schema.', max_length=40, verbose_name='conformity result'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_type', + field=models.CharField(choices=[('creation', 'Creation'), ('publication', 'Publication'), ('revision', 'Revision')], default='publication', help_text='identification of when a given event occurred', max_length=255, verbose_name='date type'), + ), + ] diff --git a/geonode/base/migrations/0092_alter_resourcebase_conformity_explanation.py b/geonode/base/migrations/0092_alter_resourcebase_conformity_explanation.py new file mode 100644 index 00000000000..562e84b233d --- /dev/null +++ b/geonode/base/migrations/0092_alter_resourcebase_conformity_explanation.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.18 on 2023-03-31 07:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0091_auto_20230331_0757'), + ] + + operations = [ + migrations.AlterField( + model_name='resourcebase', + name='conformity_explanation', + field=models.CharField(blank=True, default='', help_text='Give an Explanation about the conformity check. (e.g. See the referenced specification.', max_length=2000, verbose_name='conformity explanation'), + preserve_default=False, + ), + ] diff --git a/geonode/base/migrations/0093_resourcebase_parent_identifier.py b/geonode/base/migrations/0093_resourcebase_parent_identifier.py new file mode 100644 index 00000000000..ebb9b0e2fda --- /dev/null +++ b/geonode/base/migrations/0093_resourcebase_parent_identifier.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.18 on 2023-03-31 08:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0092_alter_resourcebase_conformity_explanation'), + ] + + operations = [ + migrations.AddField( + model_name='resourcebase', + name='parent_identifier', + field=models.ForeignKey(blank=True, help_text='A file identifier of the metadata to which this metadata is a subset (child). (e.g. 73c0f49f-1502-48ee-b038-052563f36527)', null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.resourcebase'), + ), + ] diff --git a/geonode/base/migrations/0094_auto_20230331_0918.py b/geonode/base/migrations/0094_auto_20230331_0918.py new file mode 100644 index 00000000000..c5bdbed7884 --- /dev/null +++ b/geonode/base/migrations/0094_auto_20230331_0918.py @@ -0,0 +1,59 @@ +# Generated by Django 3.2.18 on 2023-03-31 09:18 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0093_resourcebase_parent_identifier'), + ] + + operations = [ + migrations.AddField( + model_name='resourcebase', + name='date_accepted', + field=models.DateField(help_text='The date that the publisher accepted the resource into their system.', null=True, verbose_name='date accepted'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_available', + field=models.DateField(default=datetime.date.today, help_text='The date the resource is made publicly available. To indicate the end of an embargo period.', verbose_name='date available'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_collected', + field=models.DateField(help_text='The date or date range in which the dataset content was collected', null=True, verbose_name='date collected'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_copyrighted', + field=models.DateField(help_text='The specific, documented date at which the dataset receives a copyrighted status, if applicable.', null=True, verbose_name='date copyrighted'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_created', + field=models.DateField(default=datetime.date.today, help_text='The date the resource itself was put together; a single date for a final component, e.g. the finalised file with all of the data.', verbose_name='date created'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_issued', + field=models.DateField(help_text='The date that the resource is published or distributed e.g. to a data centre.', null=True, verbose_name='date issued'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_submitted', + field=models.DateField(help_text='The date the creator submits the resource to the publisher. This could be different from Accepted if the publisher the applies a selection process. To indicate the start of an embargo period. ', null=True, verbose_name='date submitted'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_updated', + field=models.DateField(default=datetime.date.today, help_text='The date of the last update (last revision) to the dataset, when the dataset is being added to.', verbose_name='date updated'), + ), + migrations.AddField( + model_name='resourcebase', + name='date_valid', + field=models.DateField(help_text='The date or date range during which the dataset or resource is accurate.', null=True, verbose_name='date valid'), + ), + ] diff --git a/geonode/base/migrations/0095_auto_20230331_0923.py b/geonode/base/migrations/0095_auto_20230331_0923.py new file mode 100644 index 00000000000..4cfc737ae64 --- /dev/null +++ b/geonode/base/migrations/0095_auto_20230331_0923.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.18 on 2023-03-31 09:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0094_auto_20230331_0918'), + ] + + operations = [ + migrations.AlterField( + model_name='resourcebase', + name='date_accepted', + field=models.DateField(blank=True, help_text='The date that the publisher accepted the resource into their system.', null=True, verbose_name='date accepted'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_collected', + field=models.DateField(blank=True, help_text='The date or date range in which the dataset content was collected', null=True, verbose_name='date collected'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_copyrighted', + field=models.DateField(blank=True, help_text='The specific, documented date at which the dataset receives a copyrighted status, if applicable.', null=True, verbose_name='date copyrighted'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_issued', + field=models.DateField(blank=True, help_text='The date that the resource is published or distributed e.g. to a data centre.', null=True, verbose_name='date issued'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_submitted', + field=models.DateField(blank=True, help_text='The date the creator submits the resource to the publisher. This could be different from Accepted if the publisher the applies a selection process. To indicate the start of an embargo period. ', null=True, verbose_name='date submitted'), + ), + migrations.AlterField( + model_name='resourcebase', + name='date_valid', + field=models.DateField(blank=True, help_text='The date or date range during which the dataset or resource is accurate.', null=True, verbose_name='date valid'), + ), + ] diff --git a/geonode/documents/migrations/0037_alter_document_abstract_en.py b/geonode/documents/migrations/0037_alter_document_abstract_en.py new file mode 100644 index 00000000000..7b0aff023d2 --- /dev/null +++ b/geonode/documents/migrations/0037_alter_document_abstract_en.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-03-30 06:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0036_clean_document_thumbnails'), + ] + + operations = [ + migrations.AlterField( + model_name='document', + name='abstract_en', + field=models.TextField(help_text='brief narrative summary of the content of the resource(s)', max_length=2000, null=True, verbose_name='abstract'), + ), + ] diff --git a/geonode/layers/api/urls.py b/geonode/layers/api/urls.py index 5305a6dbc08..fd35cea0e02 100644 --- a/geonode/layers/api/urls.py +++ b/geonode/layers/api/urls.py @@ -21,5 +21,5 @@ from . import views router.register(r"datasets", views.DatasetViewSet, "datasets") - +#router.register(r"datasets/(?P)/attributes",views.DatasetAttributesViewSet, "attributes") urlpatterns = [] diff --git a/geonode/layers/migrations/0045_alter_dataset_abstract_en.py b/geonode/layers/migrations/0045_alter_dataset_abstract_en.py new file mode 100644 index 00000000000..a36c1059a26 --- /dev/null +++ b/geonode/layers/migrations/0045_alter_dataset_abstract_en.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-03-30 06:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('layers', '0044_alter_dataset_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='dataset', + name='abstract_en', + field=models.TextField(help_text='brief narrative summary of the content of the resource(s)', max_length=2000, null=True, verbose_name='abstract'), + ), + ] diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 36e825549b7..3d5c1499ba3 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -405,6 +405,26 @@ class Attribute(models.Model): default="xsd:string", unique=False, ) + # unit = models.CharField( + # _("attribute unit"), + # help_text=_("Unit of the data; Ideally SI-Unit."), + # max_length=50, + # blank=True, + # null=True, + # unique=True, + # ) + # quality = models.CharField( + # _("attribute quality statement"), + # help_text=_("If measures are taken to check the Quality of the data, please specify."), + # max_length=2000, + # blank=True, + # null=True, + # ) + # keywords + + + + visible = models.BooleanField( _("visible?"), help_text=_("specifies if the attribute should be displayed in identify results"), default=True ) diff --git a/geonode/maps/migrations/0043_alter_map_abstract_en.py b/geonode/maps/migrations/0043_alter_map_abstract_en.py new file mode 100644 index 00000000000..82219ccd821 --- /dev/null +++ b/geonode/maps/migrations/0043_alter_map_abstract_en.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-03-30 06:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('maps', '0042_remove_maplayer_styles'), + ] + + operations = [ + migrations.AlterField( + model_name='map', + name='abstract_en', + field=models.TextField(help_text='brief narrative summary of the content of the resource(s)', max_length=2000, null=True, verbose_name='abstract'), + ), + ] diff --git a/geonode/people/migrations/0037_profile_orcid_identifier.py b/geonode/people/migrations/0037_profile_orcid_identifier.py new file mode 100644 index 00000000000..bc30d359af1 --- /dev/null +++ b/geonode/people/migrations/0037_profile_orcid_identifier.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-04-03 07:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('people', '0036_merge_20210706_0951'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='orcid_identifier', + field=models.CharField(blank=True, default='', help_text='ORCID identifier of profile.', max_length=255, verbose_name='orcid identifier'), + ), + ] diff --git a/geonode/people/models.py b/geonode/people/models.py index 37d4c21631c..1d97672c6ad 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -127,6 +127,21 @@ class Profile(AbstractUser): blank=True, ) + orcid_identifier = models.CharField( + _("orcid identifier"), + max_length=255, + default="", + blank=True, + help_text=_("ORCID identifier of profile."), + ) + + @property + def identifier_schema(self): + return "ORCID" + + def get_orcid_url(self): + return "https://orcid.org/" + self.orcid_identifier + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._previous_active_state = self.is_active From a7fe9bc10e7d21a545648b18c957deb8b5e7081f Mon Sep 17 00:00:00 2001 From: mwallschlaeger Date: Wed, 9 Aug 2023 16:39:29 +0200 Subject: [PATCH 31/49] Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity --- geonode/base/models.py | 49 ++++++- geonode/base/templatetags/base_tags.py | 6 + geonode/documents/api/tests.py | 1 + .../templates/layouts/doc_panels.html | 97 +++---------- geonode/documents/views.py | 6 +- .../geoapps/templates/layouts/app_panels.html | 127 +++++------------- geonode/geoapps/views.py | 5 +- geonode/geoserver/helpers.py | 6 +- geonode/layers/templates/layouts/panels.html | 98 +++----------- geonode/layers/templatetags/contact_roles.py | 44 ++++++ geonode/layers/tests.py | 1 + geonode/layers/views.py | 6 +- .../maps/templates/layouts/map_panels.html | 109 ++++----------- geonode/maps/views.py | 5 +- geonode/people/__init__.py | 37 +++-- geonode/settings.py | 7 + 16 files changed, 238 insertions(+), 366 deletions(-) create mode 100644 geonode/layers/templatetags/contact_roles.py diff --git a/geonode/base/models.py b/geonode/base/models.py index df5cd7f7889..71724a03ae0 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1649,7 +1649,7 @@ def add_missing_metadata_author_or_poc(self): @staticmethod def get_multivalue_role_property_names() -> List[str]: - """_summary_: returns list of property names for all contact roles able to + """returns list of property names for all contact roles able to handle multiple profile_users Returns: @@ -1660,7 +1660,7 @@ def get_multivalue_role_property_names() -> List[str]: @staticmethod def get_multivalue_required_role_property_names() -> List[str]: - """_summary_: returns list of property names for all contact roles that are required + """returns list of property names for all contact roles that are required Returns: _type_: List(str) @@ -1668,9 +1668,19 @@ def get_multivalue_required_role_property_names() -> List[str]: """ return [role.name for role in (set(Roles.get_multivalue_ones()) & set(Roles.get_required_ones()))] + @staticmethod + def get_ui_toggled_role_property_names() -> List[str]: + """returns list of property names for all contact roles that are toggled of in metadata_editor + + Returns: + _type_: List(str) + _description: list of names + """ + return [role.name for role in (set(Roles.get_toggled_ones()) & set(Roles.get_toggled_ones()))] + # typing not possible due to: from geonode.base.forms import ResourceBaseForm; unable due to circular ... def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: - """_summary_: gets a ResourceBaseForm and extracts the Contact Role elements from it + """gets a ResourceBaseForm and extracts the Contact Role elements from it Args: resource_base_form (ResourceBaseForm): ResourceBaseForm with contact roles set @@ -1688,7 +1698,7 @@ def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool: return failed def __get_contact_role_elements__(self, role: str) -> Optional[List[settings.AUTH_USER_MODEL]]: - """_summary_: general getter of for all contact roles except owner + """general getter of for all contact roles except owner Args: role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested @@ -1706,7 +1716,7 @@ def __get_contact_role_elements__(self, role: str) -> Optional[List[settings.AUT CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES = Union[settings.AUTH_USER_MODEL, QuerySet, List[settings.AUTH_USER_MODEL]] def __set_contact_role_element__(self, user_profile: CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES, role: str): - """_summary_: general setter for all contact roles except owner in resource base + """general setter for all contact roles except owner in resource base Args: user_profile (CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES): _description_ @@ -1727,11 +1737,13 @@ def __create_role__( elif isinstance(user_profile, list) and all(isinstance(x, get_user_model()) for x in user_profile): ContactRole.objects.filter(role=role, resource=self).delete() return [__create_role__(self, role, profile) for profile in user_profile] + elif user_profile is None: + ContactRole.objects.filter(role=role, resource=self).delete() else: logger.error(f"Bad profile format for role: {role} ...") def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]: - """_summary_: Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. + """Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy. Returns: _type_: List[Tuple[List[people object], roles_label_name]] @@ -1743,6 +1755,22 @@ def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_ if self.__getattribute__(role.name) } + def get_first_contact_of_role(self, role: str) -> Optional[ContactRole]: + """ + Get the first contact from the specified role. + + Parameters: + role (str): The role of the contact. + + Returns: + ContactRole or None: The first contact with the specified role, or None if not found. + """ + if contact := ContactRole.objects.filter(role=role).first(): + return contact + else: + return None + + # Contact Role: POC (pointOfContact) def __get_poc__(self) -> List[settings.AUTH_USER_MODEL]: return self.__get_contact_role_elements__(role="pointOfContact") @@ -1755,6 +1783,7 @@ def __set_poc__(self, user_profile): def poc_csv(self): return ",".join(p.get_full_name() or p.username for p in self.poc) + # Contact Role: metadata_author def _get_metadata_author(self): return self.__get_contact_role_elements__(role="author") @@ -1767,6 +1796,7 @@ def _set_metadata_author(self, user_profile): def metadata_author_csv(self): return ",".join(p.get_full_name() or p.username for p in self.metadata_author) + # Contact Role: PROCESSOR def _get_processor(self): return self.__get_contact_role_elements__(role=Roles.PROCESSOR.name) @@ -1779,6 +1809,7 @@ def _set_processor(self, user_profile): def processor_csv(self): return ",".join(p.get_full_name() or p.username for p in self.processor) + # Contact Role: PUBLISHER def _get_publisher(self): return self.__get_contact_role_elements__(role=Roles.PUBLISHER.name) @@ -1791,6 +1822,7 @@ def _set_publisher(self, user_profile): def publisher_csv(self): return ",".join(p.get_full_name() or p.username for p in self.publisher) + # Contact Role: CUSTODIAN def _get_custodian(self): return self.__get_contact_role_elements__(role=Roles.CUSTODIAN.name) @@ -1803,6 +1835,7 @@ def _set_custodian(self, user_profile): def custodian_csv(self): return ",".join(p.get_full_name() or p.username for p in self.custodian) + # Contact Role: DISTRIBUTOR def _get_distributor(self): return self.__get_contact_role_elements__(role=Roles.DISTRIBUTOR.name) @@ -1815,6 +1848,7 @@ def _set_distributor(self, user_profile): def distributor_csv(self): return ",".join(p.get_full_name() or p.username for p in self.distributor) + # Contact Role: RESOURCE_USER def _get_resource_user(self): return self.__get_contact_role_elements__(role=Roles.RESOURCE_USER.name) @@ -1827,6 +1861,7 @@ def _set_resource_user(self, user_profile): def resource_user_csv(self): return ",".join(p.get_full_name() or p.username for p in self.resource_user) + # Contact Role: RESOURCE_PROVIDER def _get_resource_provider(self): return self.__get_contact_role_elements__(role=Roles.RESOURCE_PROVIDER.name) @@ -1839,6 +1874,7 @@ def _set_resource_provider(self, user_profile): def resource_provider_csv(self): return ",".join(p.get_full_name() or p.username for p in self.resource_provider) + # Contact Role: ORIGINATOR def _get_originator(self): return self.__get_contact_role_elements__(role=Roles.ORIGINATOR.name) @@ -1851,6 +1887,7 @@ def _set_originator(self, user_profile): def originator_csv(self): return ",".join(p.get_full_name() or p.username for p in self.originator) + # Contact Role: PRINCIPAL_INVESTIGATOR def _get_principal_investigator(self): return self.__get_contact_role_elements__(role=Roles.PRINCIPAL_INVESTIGATOR.name) diff --git a/geonode/base/templatetags/base_tags.py b/geonode/base/templatetags/base_tags.py index 2306f411d2c..23960e0eeaa 100644 --- a/geonode/base/templatetags/base_tags.py +++ b/geonode/base/templatetags/base_tags.py @@ -58,6 +58,12 @@ def template_trans(text): return text +@register.filter(name="get_item") +def get_item(dictionary, key): + """Get a element for a dict by name""" + return dictionary.get(key) + + @register.simple_tag def num_ratings(obj): ct = ContentType.objects.get_for_model(obj) diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index 0b8c5b1a046..b80fef7912d 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -446,6 +446,7 @@ def test_patch_principal_investigator(self): for user_id in user_ids ) ) + def test_file_path_and_doc_path_are_not_returned(self): """ If file_path and doc_path should not be visible diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html index ded63cd6d76..455fa0b2bf4 100644 --- a/geonode/documents/templates/layouts/doc_panels.html +++ b/geonode/documents/templates/layouts/doc_panels.html @@ -1,6 +1,8 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load base_tags %} +{% load contact_roles %} @@ -558,14 +560,14 @@
            -
            {% trans "Responsible Parties" %}
            - {% block document_poc %} -
            - - {{ document_form.poc }} -
            - {% endblock document_poc %} -
            +
            {% trans "Responsible Parties" %}
            + {% block document_poc %} +
            + + {{ document_form.poc }} +
            + {% endblock document_poc %} +
            {% trans "Responsible and Permissions" %}
            @@ -578,84 +580,23 @@
            {% trans "toggle more Contact Roles" %} + {% block document_more_contact_roles %}
            -
            {% trans "more metadata contact roles" %}
            -
            - {% block document_metadata_author %} -
            - - {{ document_form.metadata_author }} -
            - {% endblock document_metadata_author %} -
            -
            - {% block document_processor %} -
            - - {{ document_form.processor }} -
            - {% endblock document_processor %} -
            +
            {% trans "more metadata contact roles" %}
            + {% for contact_role in UI_ROLES_IN_TOGGLE_VIEW %} + {% getattribute document_form contact_role as cr %}
            - {% block document_publisher %}
            - - {{ document_form.publisher }} + + {{ cr}}
            - {% endblock document_publisher %}
            -
            - {% block document_custodian %} -
            - - {{ document_form.custodian }} -
            - {% endblock document_custodian %} -
            -
            - {% block document_distributor %} -
            - - {{ document_form.distributor }} -
            - {% endblock document_distributor %} -
            -
            - {% block document_resource_user %} -
            - - {{ document_form.resource_user }} -
            - {% endblock document_resource_user %} -
            -
            - {% block document_resource_provider %} -
            - - {{ document_form.resource_provider }} -
            - {% endblock document_resource_provider %} -
            -
            - {% block document_originator %} -
            - - {{ document_form.originator }} -
            - {% endblock document_originator %} -
            -
            - {% block document_principal_investigator %} -
            - - {{ document_form.principal_investigator }} -
            - {% endblock document_principal_investigator %} -
            -
            + {% endfor %}
            + {% endblock document_more_contact_roles %} + diff --git a/geonode/documents/views.py b/geonode/documents/views.py index 311679ee8e8..722cffbc80d 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -468,7 +468,6 @@ def document_metadata( contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) - if not AdvancedSecurityWorkflowManager.is_allowed_to_publish(request.user, document): document_form.fields["is_published"].widget.attrs.update({"disabled": "true"}) if not AdvancedSecurityWorkflowManager.is_allowed_to_approve(request.user, document): @@ -493,8 +492,9 @@ def document_metadata( set(getattr(settings, "UI_DEFAULT_MANDATORY_FIELDS", [])) | set(getattr(settings, "UI_REQUIRED_FIELDS", [])) ), - } - | contact_role_forms_context, + **contact_role_forms_context, + "UI_ROLES_IN_TOGGLE_VIEW": document.get_ui_toggled_role_property_names(), + }, ) diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html index 8fceeb75152..6f6bc70ca7f 100644 --- a/geonode/geoapps/templates/layouts/app_panels.html +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -1,6 +1,7 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load contact_roles %} @@ -485,108 +486,46 @@ {% endblock geoapp_extra_metadata %} - -
            -
            -
            {% trans "Responsible Parties" %}
            - {% block geoapp_poc %} -
            - - {{ geoapp_form.poc }} + +
            +
            +
            {% trans "Responsible Parties" %}
            + {% block geoapp_poc %} +
            + + {{ geoapp_form.poc }} +
            + {% endblock geoapp_poc %} +
            +
            +
            {% trans "Responsible and Permissions" %}
            +
            + {% block geoapp_owner %} +
            + + {{ geoapp_form.owner }}
            - {% endblock geoapp_poc %} -
            -
            -
            {% trans "Responsible and Permissions" %}
            -
            - {% block geoapp_owner %} -
            - - {{ geoapp_form.owner }} -
            - {% endblock geoapp_owner %} -
            + {% endblock geoapp_owner %}
            - {% trans "toggle more Contact Roles" %} -
            -
            {% trans "more metadata contact roles" %}
            -
            - {% block geoapp_metadata_author %} -
            - - {{ geoapp_form.metadata_author }} -
            - {% endblock geoapp_metadata_author %} -
            +
            + {% trans "toggle more Contact Roles" %} + {% block geoapp_more_contact_roles %} +
            +
            {% trans "more metadata contact roles" %}
            + {% for contact_role in UI_ROLES_IN_TOGGLE_VIEW %} + {% getattribute geoapp_form contact_role as cr %}
            - {% block geoapp_processor %} -
            - - {{ geoapp_form.processor }} -
            - {% endblock geoapp_processor %} -
            -
            - {% block geoapp_publisher %} -
            - - {{ geoapp_form.publisher }} -
            - {% endblock geoapp_publisher %} -
            -
            - {% block geoapp_custodian %} -
            - - {{ geoapp_form.custodian }} -
            - {% endblock geoapp_custodian %} -
            -
            - {% block geoapp_distributor %} -
            - - {{ geoapp_form.distributor }} -
            - {% endblock geoapp_distributor %} -
            -
            - {% block geoapp_resource_user %} -
            - - {{ geoapp_form.resource_user }} -
            - {% endblock geoapp_resource_user %} -
            -
            - {% block geoapp_resource_provider %} -
            - - {{ geoapp_form.resource_provider }} -
            - {% endblock geoapp_resource_provider %} -
            -
            - {% block geoapp_originator %} -
            - - {{ geoapp_form.originator }} -
            - {% endblock geoapp_originator %} -
            -
            - {% block geoapp_principal_investigator %} -
            - - {{ geoapp_form.principal_investigator }} -
            - {% endblock geoapp_principal_investigator %} +
            + + {{ cr}}
            -
            + {% endfor %}
            + {% endblock geoapp_more_contact_roles %}
            +
            diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index 11d0ea9101a..5519e0fcf4f 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -385,8 +385,9 @@ def geoapp_metadata( set(getattr(settings, "UI_DEFAULT_MANDATORY_FIELDS", [])) | set(getattr(settings, "UI_REQUIRED_FIELDS", [])) ), - } - | contact_role_forms_context, + **contact_role_forms_context, + "UI_ROLES_IN_TOGGLE_VIEW": geoapp_obj.get_ui_toggled_role_property_names(), + }, ) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index b8707ca1d9b..8067f272adb 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2081,9 +2081,9 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): if updatemetadata: gs_resource.metadata_links = metadata_links - + default_poc = instance.get_first_contact_of_role(role="poc") # Update Attribution link - if instance.poc: + if default_poc: # gsconfig now utilizes an attribution dictionary gs_resource.attribution = { "title": str(instance.poc_csv), @@ -2093,7 +2093,7 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): "url": None, "type": None, } - profile = get_user_model().objects.get(username=instance.poc[0].username) + profile = get_user_model().objects.get(username=default_poc.username) site_url = ( settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL ) diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index cd4b6d30cd8..27c9b23164f 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -1,6 +1,8 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load contact_roles %} +