Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for downloading EntityList dataset #2678

Merged
merged 6 commits into from
Aug 26, 2024
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
25 changes: 25 additions & 0 deletions docs/entities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,31 @@ Body:
]
}

Download EntityList
-------------------
.. raw:: html

<pre class="prettyprint"><b>GET</b> api/v2/entity-lists/&lt;entity_list_id&gt;/download</pre>

or

.. raw:: html

<pre class="prettyprint"><b>GET</b> api/v2/entity-lists/&lt;entity_list_id&gt;.csv</pre>


This endpoints are used to download the dataset in CSV format.

**Example**

.. code-block:: bash

curl -X GET https://api.ona.io/api/v2/entity-lists/1/download \
-H "Authorization: Token ACCESS_TOKEN"

**Response**

Status: ``200 OK``

Delete EntityList
-----------------
Expand Down
115 changes: 115 additions & 0 deletions onadata/apps/api/tests/viewsets/test_entity_list_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,25 @@ def test_soft_deleted(self):
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 404)

def test_render_csv(self):
"""Render in CSV format"""
request = self.factory.get("/", **self.extra)
# Using `.csv` suffix
response = self.view(request, pk=self.entity_list.pk, format="csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.get("Content-Disposition"), "attachment; filename=trees.csv"
)
self.assertEqual(response["Content-Type"], "application/csv")
# Using `Accept` header
request = self.factory.get("/", HTTP_ACCEPT="text/csv", **self.extra)
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.get("Content-Disposition"), "attachment; filename=trees.csv"
)
self.assertEqual(response["Content-Type"], "application/csv")


class DeleteEntityListTestCase(TestAbstractViewSet):
"""Tests for deleting a single EntityList"""
Expand Down Expand Up @@ -1331,3 +1350,99 @@ def test_delete_via_kwarg_invalid(self):
request = self.factory.delete("/", **self.extra)
response = self.view(request, pk=self.entity_list.pk, entity_pk=self.entity.pk)
self.assertEqual(response.status_code, 405)


class DownloadEntityListTestCase(TestAbstractViewSet):
"""Tests for `download` action"""

def setUp(self):
super().setUp()

self.view = EntityListViewSet.as_view({"get": "download"})
self.project = get_user_default_project(self.user)
self.entity_list = EntityList.objects.create(name="trees", project=self.project)
OwnerRole.add(self.user, self.entity_list)

def test_download(self):
"""EntityList dataset is downloaded"""
request = self.factory.get("/", **self.extra)
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["Content-Disposition"], "attachment; filename=trees.csv"
)
self.assertEqual(response["Content-Type"], "application/csv")
# Using `.csv` suffix
request = self.factory.get("/", **self.extra)
response = self.view(request, pk=self.entity_list.pk, format="csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["Content-Disposition"], "attachment; filename=trees.csv"
)
self.assertEqual(response["Content-Type"], "application/csv")
# Using `Accept` header
request = self.factory.get("/", HTTP_ACCEPT="text/csv", **self.extra)
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.get("Content-Disposition"), "attachment; filename=trees.csv"
)
self.assertEqual(response["Content-Type"], "application/csv")
# Unsupported suffix
request = self.factory.get("/", **self.extra)
response = self.view(request, pk=self.entity_list.pk, format="json")
self.assertEqual(response.status_code, 404)
# Unsupported accept header
request = self.factory.get("/", HTTP_ACCEPT="application/json", **self.extra)
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 404)

def test_anonymous_user(self):
"""Anonymous user cannot download a private EntityList"""
# Anonymous user cannot view private EntityList
request = self.factory.get("/")
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 404)
# Anonymous user can view public EntityList
self.project.shared = True
self.project.save()
request = self.factory.get("/")
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 200)

def test_invalid_entity_list(self):
"""Invalid EntityList is handled"""
request = self.factory.get("/", **self.extra)
response = self.view(request, pk=sys.maxsize)
self.assertEqual(response.status_code, 404)

def test_object_permissions(self):
"""User must have object view level permissions"""
alice_data = {
"username": "alice",
"email": "[email protected]",
"password1": "password12345",
"password2": "password12345",
"first_name": "Alice",
"last_name": "Hughes",
}
alice_profile = self._create_user_profile(alice_data)
extra = {"HTTP_AUTHORIZATION": f"Token {alice_profile.user.auth_token}"}

for role in ROLES:
ShareProject(self.project, "alice", role).save()
request = self.factory.get("/", **extra)
response = self.view(request, pk=self.entity_list.pk)

if role in ["owner", "manager"]:
self.assertEqual(response.status_code, 200)

else:
self.assertEqual(response.status_code, 404)

def test_soft_deleted(self):
"""Soft deleted dataset cannot be retrieved"""
self.entity_list.soft_delete()
request = self.factory.get("/", **self.extra)
response = self.view(request, pk=self.entity_list.pk)
self.assertEqual(response.status_code, 404)
31 changes: 31 additions & 0 deletions onadata/apps/api/viewsets/entity_list_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import GenericViewSet
Expand All @@ -13,6 +14,7 @@
ListModelMixin,
)


from onadata.apps.api.permissions import DjangoObjectPermissionsIgnoreModelPerm
from onadata.apps.api.tools import get_baseviewset_class
from onadata.apps.logger.models import Entity, EntityList
Expand All @@ -23,6 +25,7 @@
StandardPageNumberPagination,
)
from onadata.libs.permissions import CAN_ADD_PROJECT_ENTITYLIST
from onadata.libs.renderers import renderers
from onadata.libs.serializers.entity_serializer import (
EntityArraySerializer,
EntitySerializer,
Expand All @@ -31,6 +34,7 @@
EntityListDetailSerializer,
EntityDeleteSerializer,
)
from onadata.libs.utils.api_export_tools import get_entity_list_export_response


BaseViewset = get_baseviewset_class()
Expand Down Expand Up @@ -172,3 +176,30 @@ def get_queryset_entities(self, request, entity_list):
queryset = queryset.order_by("id")

return queryset

@action(
methods=["GET"],
detail=True,
renderer_classes=[renderers.CSVRenderer],
)
def download(self, request, *args, **kwargs):
"""Provides `download` action for dataset"""
accept_header = request.headers.get("Accept", "")

if (
kwargs.get("format") is not None or accept_header
) and not request.accepted_renderer.format == "csv":
raise NotFound(code=status.HTTP_404_NOT_FOUND)

entity_list = self.get_object()

return get_entity_list_export_response(request, entity_list, entity_list.name)

def retrieve(self, request, *args, **kwargs):
"""Override `retrieve` method"""
instance = self.get_object()

if kwargs.get("format") == "csv" or request.accepted_renderer.format == "csv":
return get_entity_list_export_response(request, instance, instance.name)

return super().retrieve(request, format, *args, **kwargs)
Loading