Skip to content

Commit

Permalink
[Fixes #10270] Document creation via API v2
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiagiupponi committed Nov 8, 2022
1 parent 63a9b9f commit eb7843a
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 16 deletions.
6 changes: 4 additions & 2 deletions geonode/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
26 changes: 26 additions & 0 deletions geonode/documents/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
#
#########################################################################
from rest_framework.exceptions import APIException


class DocumentException(APIException):
status_code = 400
default_detail = "invalid document"
default_code = "document_exception"
category = "document_api"
26 changes: 15 additions & 11 deletions geonode/documents/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,30 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
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 DocumentSerializer(ResourceBaseSerializer):
def __init__(self, *args, **kwargs):
# Instantiate the superclass normally
super().__init__(*args, **kwargs)

file_path = GeonodeFilePathField()

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")
81 changes: 81 additions & 0 deletions geonode/documents/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
import os
from django.contrib.auth import get_user_model
import logging

from urllib.parse import urljoin
from django.test import override_settings

from django.urls import reverse
from mock import patch
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
from geonode.documents.api.exceptions import DocumentException

logger = logging.getLogger(__name__)

Expand All @@ -42,6 +48,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):
"""
Expand Down Expand Up @@ -72,3 +82,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': ['This field is required.'], 'code': 'invalid'}
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
}
}
expected = {'success': False, 'errors': ['This field may not be null.'], 'code': 'invalid'}
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)

56 changes: 53 additions & 3 deletions geonode/documents/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
from drf_spectacular.utils import extend_schema

from pkgutil import extend_path
from drf_spectacular.utils import extend_schema
from pathlib import Path
from dynamic_rest.viewsets import DynamicModelViewSet
from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter

from rest_framework.decorators import action
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.storage.manager import StorageManager
from geonode.resource.manager import resource_manager

from geonode.documents.views import ALLOWED_DOC_TYPES
from .serializers import DocumentSerializer
from .permissions import DocumentPermissionsFilter

Expand All @@ -46,9 +53,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
Expand All @@ -57,6 +64,49 @@ class DocumentViewSet(DynamicModelViewSet):
serializer_class = DocumentSerializer
pagination_class = GeoNodeApiPagination

def perform_create(self, serializer):
'''
Function to create document via API v2.
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.
Is going to be cloned by the storage manager
'''
manager = None
serializer.is_valid(raise_exception=True)
_has_file = serializer.validated_data.pop("file_path", None)
extension = serializer.validated_data.pop("extension", None)

if not _has_file:
raise DocumentException(detail="A filepath must be speficied")

if not extension:
extension = Path(_has_file).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.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'])
Expand Down

0 comments on commit eb7843a

Please sign in to comment.