diff --git a/geonode/base/migrations/0085_alter_resourcebase_uuid.py b/geonode/base/migrations/0085_alter_resourcebase_uuid.py
new file mode 100644
index 00000000000..b7ef374172d
--- /dev/null
+++ b/geonode/base/migrations/0085_alter_resourcebase_uuid.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.16 on 2022-11-08 10:52
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0084_remove_comments_from_actions'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='resourcebase',
+ name='uuid',
+ field=models.CharField(default=uuid.uuid4, max_length=36, unique=True),
+ ),
+ ]
diff --git a/geonode/base/models.py b/geonode/base/models.py
index 99207f07269..31bb13c8439 100644
--- a/geonode/base/models.py
+++ b/geonode/base/models.py
@@ -777,7 +777,7 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase):
extra_metadata_help_text = _(
'Additional metadata, must be in format [ {"metadata_key": "metadata_value"}, {"metadata_key": "metadata_value"} ]')
# internal fields
- uuid = models.CharField(max_length=36, unique=True, default=str(uuid.uuid4))
+ 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(
@@ -1233,7 +1233,9 @@ def save(self, notify=False, *args, **kwargs):
self.pk = self.id = _next_value
- if not self.uuid or len(self.uuid) == 0 or callable(self.uuid):
+ if isinstance(self.uuid, uuid.UUID):
+ self.uuid = str(self.uuid)
+ elif not self.uuid or callable(self.uuid) or len(self.uuid) == 0:
self.uuid = str(uuid.uuid4())
super().save(*args, **kwargs)
diff --git a/geonode/documents/api/exceptions.py b/geonode/documents/api/exceptions.py
new file mode 100644
index 00000000000..4850255b1d9
--- /dev/null
+++ b/geonode/documents/api/exceptions.py
@@ -0,0 +1,26 @@
+#########################################################################
+#
+# Copyright (C) 2022 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 .
+#
+#########################################################################
+from rest_framework.exceptions import APIException
+
+
+class DocumentException(APIException):
+ status_code = 400
+ default_detail = "invalid document"
+ default_code = "document_exception"
+ category = "document_api"
diff --git a/geonode/documents/api/serializers.py b/geonode/documents/api/serializers.py
index 665b6bf514a..0ece3efbb8a 100644
--- a/geonode/documents/api/serializers.py
+++ b/geonode/documents/api/serializers.py
@@ -16,26 +16,37 @@
# along with this program. If not, see .
#
#########################################################################
-from geonode.documents.models import Document
-from geonode.base.api.serializers import ResourceBaseSerializer
-
import logging
+from dynamic_rest.fields.fields import DynamicComputedField
+from geonode.base.api.serializers import ResourceBaseSerializer
+from geonode.documents.models import Document
+
logger = logging.getLogger(__name__)
-class DocumentSerializer(ResourceBaseSerializer):
+class GeonodeFilePathField(DynamicComputedField):
+ def get_attribute(self, instance):
+ return instance.files
+
+
+class DocumentFieldField(DynamicComputedField):
+
+ def get_attribute(self, instance):
+ return instance.files
+
+
+class DocumentSerializer(ResourceBaseSerializer):
def __init__(self, *args, **kwargs):
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
+ file_path = GeonodeFilePathField(required=False)
+ doc_file = DocumentFieldField(required=False)
+
class Meta:
model = Document
- name = 'document'
- view_name = 'documents-list'
- fields = (
- 'pk', 'uuid', 'name', 'href',
- 'subtype', 'extension', 'mime_type',
- 'executions'
- )
+ name = "document"
+ view_name = "documents-list"
+ fields = ("pk", "uuid", "name", "href", "subtype", "extension", "mime_type", "executions", "file_path", "doc_file")
diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py
index 971148a30a6..aff90ad8661 100644
--- a/geonode/documents/api/tests.py
+++ b/geonode/documents/api/tests.py
@@ -16,6 +16,8 @@
# along with this program. If not, see .
#
#########################################################################
+import os
+from django.contrib.auth import get_user_model
import logging
from urllib.parse import urljoin
@@ -24,6 +26,7 @@
from rest_framework.test import APITestCase
from guardian.shortcuts import assign_perm, get_anonymous_user
+from geonode import settings
from geonode.documents.models import Document
from geonode.base.populate_test_data import create_models
@@ -42,6 +45,10 @@ def setUp(self):
create_models(b'document')
create_models(b'map')
create_models(b'dataset')
+ self.admin = get_user_model().objects.get(username="admin")
+ self.url = reverse('documents-list')
+ self.invalid_file_path = f"{settings.PROJECT_ROOT}/tests/data/thesaurus.rdf"
+ self.valid_file_path = f"{settings.PROJECT_ROOT}/base/fixtures/test_xml.xml"
def test_documents(self):
"""
@@ -72,3 +79,74 @@ def test_documents(self):
# import json
# logger.error(f"{json.dumps(layers_data)}")
+
+ def test_creation_return_error_if_file_is_not_passed(self):
+ '''
+ If file_path is not available, should raise error
+ '''
+ self.client.force_login(self.admin)
+ payload = {
+ "document": {
+ "title": "New document",
+ "metadata_only": True
+ }
+ }
+ expected = {'success': False, 'errors': ['A file path or a file must be speficied'], 'code': 'document_exception'}
+ actual = self.client.post(self.url, data=payload, format="json")
+ self.assertEqual(400, actual.status_code)
+ self.assertDictEqual(expected, actual.json())
+
+ def test_creation_return_error_if_file_is_none(self):
+ '''
+ If file_path is not available, should raise error
+ '''
+ self.client.force_login(self.admin)
+ payload = {
+ "document": {
+ "title": "New document",
+ "metadata_only": True,
+ "file_path": None,
+ "doc_file": None
+ }
+ }
+ expected = {'success': False, 'errors': ['A file path or a file must be speficied'], 'code': 'document_exception'}
+ actual = self.client.post(self.url, data=payload, format="json")
+ self.assertEqual(400, actual.status_code)
+ self.assertDictEqual(expected, actual.json())
+
+ def test_creation_should_rase_exec_for_unsupported_files(self):
+ self.client.force_login(self.admin)
+ payload = {
+ "document": {
+ "title": "New document",
+ "metadata_only": True,
+ "file_path": self.invalid_file_path
+ }
+ }
+ expected = {'success': False, 'errors': ['The file provided is not in the supported extension file list'], 'code': 'document_exception'}
+ actual = self.client.post(self.url, data=payload, format="json")
+ self.assertEqual(400, actual.status_code)
+ self.assertDictEqual(expected, actual.json())
+
+ def test_creation_should_create_the_doc(self):
+ '''
+ If file_path is not available, should raise error
+ '''
+ self.client.force_login(self.admin)
+ payload = {
+ "document": {
+ "title": "New document for testing",
+ "metadata_only": True,
+ "file_path": self.valid_file_path
+ }
+ }
+ actual = self.client.post(self.url, data=payload, format="json")
+ self.assertEqual(201, actual.status_code)
+ cloned_path = actual.json().get("document", {}).get("file_path", "")[0]
+ extension = actual.json().get("document", {}).get("extension", "")
+ self.assertTrue(os.path.exists(cloned_path))
+ self.assertEqual('xml', extension)
+ self.assertTrue(Document.objects.filter(title="New document for testing").exists())
+
+ if cloned_path:
+ os.remove(cloned_path)
diff --git a/geonode/documents/api/views.py b/geonode/documents/api/views.py
index 5121fab1912..c8ee34ec65f 100644
--- a/geonode/documents/api/views.py
+++ b/geonode/documents/api/views.py
@@ -16,8 +16,9 @@
# along with this program. If not, see .
#
#########################################################################
-from drf_spectacular.utils import extend_schema
+from drf_spectacular.utils import extend_schema
+from pathlib import Path
from dynamic_rest.viewsets import DynamicModelViewSet
from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter
@@ -25,14 +26,19 @@
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
+from geonode import settings
from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter
from geonode.base.api.pagination import GeoNodeApiPagination
from geonode.base.api.permissions import UserHasPerms
+from geonode.documents.api.exceptions import DocumentException
from geonode.documents.models import Document
from geonode.base.models import ResourceBase
from geonode.base.api.serializers import ResourceBaseSerializer
+from geonode.resource.utils import resourcebase_post_save
+from geonode.storage.manager import StorageManager
+from geonode.resource.manager import resource_manager
from .serializers import DocumentSerializer
from .permissions import DocumentPermissionsFilter
@@ -46,9 +52,9 @@ class DocumentViewSet(DynamicModelViewSet):
"""
API endpoint that allows documents to be viewed or edited.
"""
- http_method_names = ['get', 'patch', 'put']
+ http_method_names = ['get', 'patch', 'put', 'post']
authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication]
- permission_classes = [IsAuthenticatedOrReadOnly, UserHasPerms]
+ permission_classes = [IsAuthenticatedOrReadOnly, UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}})]
filter_backends = [
DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter,
ExtentFilter, DocumentPermissionsFilter
@@ -57,6 +63,68 @@ class DocumentViewSet(DynamicModelViewSet):
serializer_class = DocumentSerializer
pagination_class = GeoNodeApiPagination
+ def perform_create(self, serializer):
+ '''
+ Function to create document via API v2.
+ file_path: path to the file
+ doc_file: the open file
+
+ The API expect this kind of JSON:
+ {
+ "document": {
+ "title": "New document",
+ "metadata_only": true,
+ "file_path": "/home/mattia/example.json"
+ }
+ }
+ File path rappresent the filepath where the file to upload is saved.
+
+ or can be also a form-data:
+ curl --location --request POST 'http://localhost:8000/api/v2/documents' \
+ --form 'title="Super Title2"' \
+ --form 'doc_file=@"/C:/Users/user/Pictures/BcMc-a6T9IM.jpg"' \
+ --form 'metadata_only="False"'
+ '''
+ manager = None
+ serializer.is_valid(raise_exception=True)
+ _has_file = serializer.validated_data.pop("file_path", None) or serializer.validated_data.pop("doc_file", None)
+ extension = serializer.validated_data.pop("extension", None)
+
+ if not _has_file:
+ raise DocumentException(detail="A file path or a file must be speficied")
+
+ if not extension:
+ filename = _has_file if isinstance(_has_file, str) else _has_file.name
+ extension = Path(filename).suffix.replace(".", "")
+
+ if extension not in settings.ALLOWED_DOCUMENT_TYPES:
+ raise DocumentException("The file provided is not in the supported extension file list")
+
+ try:
+ manager = StorageManager(remote_files={"base_file": _has_file})
+ manager.clone_remote_files()
+ files = manager.get_retrieved_paths()
+
+ resource = serializer.save(
+ **{
+ "owner": self.request.user,
+ "extension": extension,
+ "files": [files.get("base_file")],
+ "resource_type": "document"
+ }
+ )
+
+ resource.set_missing_info()
+ resourcebase_post_save(resource.get_real_instance())
+ resource_manager.set_permissions(None, instance=resource, permissions=None, created=True)
+ resource.handle_moderated_uploads()
+ resource_manager.set_thumbnail(resource.uuid, instance=resource, overwrite=False)
+ return resource
+ except Exception as e:
+ if manager:
+ manager.delete_retrieved_paths()
+ raise e
+
@extend_schema(methods=['get'], responses={200: ResourceBaseSerializer(many=True)},
description="API endpoint allowing to retrieve the DocumentResourceLink(s).")
@action(detail=True, methods=['get'])