Skip to content

Commit

Permalink
[Fixes #10270] Document creation via API v2 (#10271) (#10298)
Browse files Browse the repository at this point in the history
* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

* [Fixes #10270] Document creation via API v2

Co-authored-by: Alessio Fabiani <alessio.fabiani@geosolutionsgroup.com>

Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com>
Co-authored-by: Alessio Fabiani <alessio.fabiani@geosolutionsgroup.com>
  • Loading branch information
3 people authored Nov 14, 2022
1 parent 1d751ad commit 8e6029c
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 16 deletions.
19 changes: 19 additions & 0 deletions geonode/base/migrations/0085_alter_resourcebase_uuid.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
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"
33 changes: 22 additions & 11 deletions geonode/documents/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,37 @@
# 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 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")
78 changes: 78 additions & 0 deletions geonode/documents/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# 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
Expand All @@ -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

Expand All @@ -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):
"""
Expand Down Expand Up @@ -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)
74 changes: 71 additions & 3 deletions geonode/documents/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
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

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.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
Expand All @@ -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
Expand All @@ -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'])
Expand Down

0 comments on commit 8e6029c

Please sign in to comment.