Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions django_project/cloud_native_gis/api/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

import copy

from psycopg2 import sql
from django.db import connection
from django.core.exceptions import PermissionDenied
from django.core.files.storage import FileSystemStorage
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from cloud_native_gis.api.base import BaseApi, BaseReadApi
from cloud_native_gis.forms.layer import LayerForm
Expand Down Expand Up @@ -182,3 +186,98 @@ def get_queryset(self):
"""Return queryset of API."""
layer = self._get_layer()
return layer.layerattributes_set.all()


class DataPreviewAPI(APIView):
"""API to preview data."""

permission_classes = [IsAuthenticated]

def _get_search_query(self, layer: Layer, search):
text_attributes = layer.layerattributes_set.filter(
attribute_type='text'
).values_list('attribute_name', flat=True)
search_query = []
params = []
attrs = []
for attr in text_attributes:
attrs.append(sql.Identifier(attr))
search_query.append("{} ILIKE %s")
params.append(f"%{search}%")
return ' OR '.join(search_query), params, attrs

def _get_count(self, layer: Layer, search=None):
"""Get count of features in layer."""
if search is None or search == '':
return layer.metadata['FEATURE COUNT']

search_cond, params, attrs = self._get_search_query(layer, search)
if search_cond == '':
return layer.metadata['FEATURE COUNT']
query = sql.SQL("SELECT COUNT(*) FROM {}.{} WHERE {}").format(
sql.Identifier(layer.schema_name),
sql.Identifier(layer.table_name),
sql.SQL(search_cond).format(*attrs)
)
with connection.cursor() as cursor:
cursor.execute(query, params)
return cursor.fetchone()[0]

def get(self, request, *args, **kwargs):
"""Get data from layer table."""
layer = get_object_or_404(
Layer,
id=kwargs.get('layer_id')
)

page_size = int(request.GET.get('page_size', 10))
page = int(request.GET.get('page', 1))
search = request.GET.get('search', None)
total_count = self._get_count(layer, search)
columns = layer.layerattributes_set.all().values_list(
'attribute_name', flat=True
).order_by('attribute_order')
id_col = 'id'
if id_col not in columns:
id_col = columns[0]
search_cond = sql.SQL('')
params = []
if search is not None and search != '':
search_cond, params, attrs = self._get_search_query(layer, search)
if search_cond != '':
search_cond = sql.SQL('WHERE {}').format(
sql.SQL(search_cond).format(*attrs)
)
query = sql.SQL("""
SELECT {} FROM {}.{}
{}
ORDER BY {} ASC
OFFSET %s LIMIT %s
""").format(
sql.SQL(',').join(map(sql.Identifier, columns)),
sql.Identifier(layer.schema_name),
sql.Identifier(layer.table_name),
search_cond,
sql.Identifier(id_col)
)
rows = []
with connection.cursor() as cursor:
cursor.execute(
query,
params + [(int(page) - 1) * int(page_size), int(page_size)]
)
_rows = cursor.fetchall()
for _row in _rows:
_data = {}
for i, col in enumerate(columns):
_data[col] = _row[i]
rows.append(_data)

return Response(data={
'layer_id': layer.id,
'page': page,
'page_size': page_size,
'count': total_count,
'data': rows,
'columns': columns
})
Binary file not shown.
113 changes: 113 additions & 0 deletions django_project/cloud_native_gis/tests/api/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
from django.contrib.auth import get_user_model
from django.test.testcases import TestCase
from django.urls import reverse
from rest_framework.test import APIRequestFactory
from django.core.files.storage import FileSystemStorage

from core.settings.utils import absolute_path
from cloud_native_gis.models.layer import Layer, LayerType
from cloud_native_gis.models.layer_upload import LayerUpload
from cloud_native_gis.tests.base import BaseTest
from cloud_native_gis.tests.model_factories import create_user
from cloud_native_gis.api.layer import DataPreviewAPI

User = get_user_model()

Expand Down Expand Up @@ -127,3 +132,111 @@ def test_delete_api(self):
self.assertRequestDeleteView(url, 403, user=self.user_1)
self.assertRequestDeleteView(url, 204, user=self.user)
self.assertFalse(Layer.objects.filter(id=_id).first())


class DataPreviewAPITest(TestCase):

def setUp(self):
"""Init test class."""
self.factory = APIRequestFactory()

# add superuser
self.superuser = create_user(
is_staff=True,
is_superuser=True,
is_active=True
)

# add normal user
self.user = create_user(
is_active=True
)

self.layer = Layer.objects.create(
created_by=self.user,
is_ready=True
)
self.layer_upload = LayerUpload.objects.create(
created_by=self.user, layer=self.layer
)
file_path = absolute_path(
'cloud_native_gis',
'tests',
'_fixtures',
'polygons_import.zip'
)
with open(file_path, 'rb') as data:
FileSystemStorage(
location=self.layer_upload.folder
).save(f'polygons_import.zip', data)
self.layer_upload.save()
self.layer_upload.import_data()

self.layer.refresh_from_db()

def tearDown(self):
"""Clean up after tests."""
self.layer_upload.delete_folder()
self.layer.delete()

def test_data_preview_api(self):
"""Test data preview API."""
view = DataPreviewAPI.as_view()
request = self.factory.get(
reverse('data-preview', kwargs={
'layer_id': self.layer.id
}),
data={
'page_size': 10,
'page': 1
}
)
request.user = self.superuser
response = view(request, layer_id=self.layer.id)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['data']), 2)
self.assertEqual(response.data['data'][0]['id'], 1)
self.assertEqual(response.data['data'][0]['name'], 'kenya')

def test_data_preview_api_with_search(self):
"""Test data preview API with search."""
view = DataPreviewAPI.as_view()

request = self.factory.get(
reverse('data-preview', kwargs={
'layer_id': self.layer.id
}),
data={
'page_size': 10,
'page': 1,
'search': 'KENY'
}
)
request.user = self.superuser
response = view(request, layer_id=self.layer.id)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['data']), 1)
self.assertEqual(response.data['data'][0]['id'], 1)
self.assertEqual(response.data['data'][0]['name'], 'kenya')

def test_data_preview_api_no_features(self):
"""Test data preview API with no features."""
view = DataPreviewAPI.as_view()

request = self.factory.get(
reverse('data-preview', kwargs={
'layer_id': self.layer.id
}),
data={
'page_size': 10,
'page': 1,
'search': 'FEATURE_NOT_FOUND'
}
)
request.user = self.superuser
response = view(request, layer_id=self.layer.id)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 0)
self.assertEqual(len(response.data['data']), 0)
10 changes: 8 additions & 2 deletions django_project/cloud_native_gis/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# coding=utf-8
"""Cloud Native GIS."""

from django.urls import include, path
from django.urls import include, path, re_path
from django.views.generic import TemplateView

from rest_framework.routers import DefaultRouter
Expand All @@ -14,7 +14,7 @@
from cloud_native_gis.api.context import ContextAPIView
from cloud_native_gis.api.layer import (
LayerViewSet, LayerStyleViewSet, LayerUploadViewSet,
LayerAttributesViewSet
LayerAttributesViewSet, DataPreviewAPI
)
from cloud_native_gis.api.pmtile import serve_pmtiles
from cloud_native_gis.api.vector_tile import (VectorTileLayer)
Expand Down Expand Up @@ -52,6 +52,7 @@
basename='cloud-native-layer-attributes'
)


urlpatterns = [
path(
'<str:identifier>/tile/<int:z>/<int:x>/<int:y>/',
Expand All @@ -60,6 +61,11 @@
),
path('api/', include(router.urls)),
path('api/', include(layer_router.urls)),
re_path(
r'^api/layer/(?P<layer_id>[\da-f-]+)/data-preview/$',
DataPreviewAPI.as_view(),
name='data-preview'
),
path('api/context/',
ContextAPIView.as_view(),
name='cloud-native-gis-context'),
Expand Down
Loading