Skip to content

Commit

Permalink
chore(OpenRosa): remove some deprecated endpoints of v1 not called an…
Browse files Browse the repository at this point in the history
…ymore by KPI TASK-1356 (#5438)

### 📣 Summary
The OpenRosa V1 API contains many endpoints that are deprecated because
the functionality is no longer needed as a REST API but rather as code
utilities and classes, instead. The deleted endpoints are:

* bulk_delete
* bulk_validation_status
* validation_status
* destroy
* enketo
* enketo_edit
* enketo_view


### 📖 Description
This PR deletes deprecated code, endpoints and tests, from the OpenRosa v1 API
  • Loading branch information
Guitlle authored Feb 19, 2025
1 parent 43700ba commit a62c289
Show file tree
Hide file tree
Showing 5 changed files with 23 additions and 489 deletions.
7 changes: 7 additions & 0 deletions kobo/apps/openrosa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ For this reason, you will no longer see a separate kobocat container: the code t
pyxform. It also provides a map and single survey view.
- **main** - This app is the glue that brings logger and viewer
together.

## Deprecation Notices

Much of the user-facing and REST API features of this application are being refactored.
For more details and discussion, please refer to
<https://community.kobotoolbox.org/t/contemplating-the-future-of-kobocat/2743>. Please
see [REMOVALS.md](REMOVALS.md) for a complete list.
15 changes: 15 additions & 0 deletions kobo/apps/openrosa/REMOVALS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# OpenRosa endpoint removals

## Obsolete Data Access and Management

The following data access and management API endpoints have been deprecated in favor of using the OpenRosa classes withing python code in Kpi.

URL Pattern | View Class or Function | Description
-- | -- | --
*`DELETE`* `/api/v1/data/<instance id>/<submission id>` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.destroy` | Delete submissions
*`PATCH`, `GET`* `/api/v1/data/<instance id>/<submission id>/validation_status` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.validation_status` | Modify validation status of specific instance.
*`GET`* `/api/v1/data/<instance id>/bulk_validation_status` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.bulk_validation_status` | Bulk delete submissions
*`GET`* `/api/v1/data/<instance id>/bulk_delete` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.bulk_delete` | Bulk set multiple instance validation status
*`GET`* `/api/v1/data/<instance id>/<submission_id>/enketo` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.enketo` | Proxy for enketo_edit
*`GET`* `/api/v1/data/<instance id>/<submission_id>/enketo_edit` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.enketo_edit` | Handle enketo edit request
*`GET`* `/api/v1/data/<instance id>/<submission_id>/enketo_view` | `kobo.apps.openrosa.apps.api.viewsets.data_viewset.DataViewSet.enketo_view` | Handle enketo view request
180 changes: 0 additions & 180 deletions kobo/apps/openrosa/apps/api/tests/viewsets/test_data_viewset.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# coding: utf-8
import requests
from django.test import RequestFactory
from httmock import HTTMock, all_requests
from rest_framework import status

from kobo.apps.openrosa.apps.api.viewsets.data_viewset import DataViewSet
from kobo.apps.openrosa.apps.api.viewsets.xform_viewset import XFormViewSet
from kobo.apps.openrosa.apps.logger.models import XForm
from kobo.apps.openrosa.apps.main.tests.test_base import TestBase
from kobo.apps.openrosa.apps.viewer.models import ParsedInstance
from kobo.apps.openrosa.libs.constants import (
CAN_CHANGE_XFORM,
CAN_DELETE_DATA_XFORM,
Expand All @@ -17,14 +14,6 @@
from kobo.apps.openrosa.libs.utils.guardian import assign_perm, remove_perm


@all_requests
def enketo_mock(url, request):
response = requests.Response()
response.status_code = 201
response._content = b'{"url": "https://hmh2a.enketo.formhub.org"}'
return response


def _data_list(formid):
return [{
'id': formid,
Expand Down Expand Up @@ -274,77 +263,6 @@ def test_data_list_filter_by_user(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, [])

def test_cannot_get_enketo_edit_url_without_require_auth(self):
"""
It's not currently possible to support authenticated Enketo submission
editing while simultaneously accepting anonymous submissions. The
less-bad option is to reject edit requests with an explicit error
message when anonymous submissions are enabled.
"""
self.xform.require_auth = False
self.xform.save(update_fields=['require_auth'])
self.assertFalse(self.xform.require_auth)
self._make_submissions()

for view_ in ['enketo', 'enketo_edit']:
view = DataViewSet.as_view({'get': view_})
formid = self.xform.pk
dataid = self.xform.instances.all().order_by('id')[0].pk
request = self.factory.get(
'/',
data={'return_url': 'http://test.io/test_url'},
**self.extra
)
response = view(request, pk=formid, dataid=dataid)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(
response.data[0].startswith(
'Cannot edit submissions while "Require authentication '
'to see form and submit data" is disabled for your '
'project'
)
)

def test_get_enketo_edit_url(self):
self._make_submissions()
for view_ in ['enketo', 'enketo_edit']:
# ensure both legacy `/enketo` and the new `/enketo_edit` endpoints
# do the same thing
view = DataViewSet.as_view({'get': view_})
formid = self.xform.pk
dataid = self.xform.instances.all().order_by('id')[0].pk

request = self.factory.get('/', **self.extra)
response = view(request, pk=formid, dataid=dataid)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# add data check
self.assertEqual(
response.data['detail'], '`return_url` not provided.'
)

request = self.factory.get(
'/', data={'return_url': 'http://test.io/test_url'}, **self.extra
)

with HTTMock(enketo_mock):
response = view(request, pk=formid, dataid=dataid)
self.assertEqual(
response.data['url'], 'https://hmh2a.enketo.formhub.org'
)

def test_get_enketo_view_url(self):
self._make_submissions()
view = DataViewSet.as_view({'get': 'enketo_view'})
request = self.factory.get('/', **self.extra)
formid = self.xform.pk
dataid = self.xform.instances.all().order_by('id')[0].pk

with HTTMock(enketo_mock):
response = view(request, pk=formid, dataid=dataid)
self.assertEqual(
response.data['url'], 'https://hmh2a.enketo.formhub.org'
)

def test_get_form_public_data(self):
self._make_submissions()
view = DataViewSet.as_view({'get': 'list'})
Expand Down Expand Up @@ -485,104 +403,6 @@ def test_cannot_delete_submission_as_granted_user(self):
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
)

def test_cannot_bulk_delete_submissions_as_granted_user(self):
self._make_submissions()
view = DataViewSet.as_view({'delete': 'bulk_delete'})
formid = self.xform.pk
submission_ids = self.xform.instances.values_list(
'pk', flat=True
).all()[:2]
data = {'submission_ids': list(submission_ids)}
self._create_user_and_login(username='alice', password='alice')
assign_perm(CAN_VIEW_XFORM, self.user, self.xform)
assign_perm(CAN_DELETE_DATA_XFORM, self.user, self.xform)
self.extra = {'HTTP_AUTHORIZATION': f'Token {self.user.auth_token}'}
request = self.factory.delete(
'/', data=data, format='json', **self.extra,
)
response = view(request, pk=formid)
# Even with correct permissions, Alice is not allowed to delete submissions
self.assertContains(
response,
'This is not supported by the legacy API anymore',
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
)

def test_cannot_bulk_delete_submissions(self):
self._make_submissions()
before_count = self.xform.instances.all().count()
view = DataViewSet.as_view({'delete': 'bulk_delete'})
formid = self.xform.pk
submission_ids = self.xform.instances.values_list(
'pk', flat=True
).all()[:2]
data = {'submission_ids': list(submission_ids)}
request = self.factory.delete(
'/', data=data, format='json', **self.extra,
)
response = view(request, pk=formid)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['detail'], '2 submissions have been deleted')
count = self.xform.instances.all().count()
self.assertEqual(before_count - 2, count)

def test_update_validation_status(self):
self._make_submissions()
view = DataViewSet.as_view({'patch': 'validation_status'})
formid = self.xform.pk
dataid = self.xform.instances.all().order_by('id')[0].pk
data = {
'validation_status.uid': 'validation_status_on_hold'
}
request = self.factory.patch(
'/', data=data, format='json', **self.extra
)
response = view(request, pk=formid, dataid=dataid)
self.assertEqual(response.status_code, status.HTTP_200_OK)
cursor = ParsedInstance.query_mongo_minimal(
query={'_id': dataid},
fields=None,
sort=None,
)
submission = next(cursor)
self.assertEqual(
submission['_validation_status']['uid'], 'validation_status_on_hold'
)
self.assertEqual(
submission['_validation_status']['by_whom'], self.user.username # bob
)

def test_bulk_update_validation_status(self):
self._make_submissions()
view = DataViewSet.as_view({'patch': 'bulk_validation_status'})
formid = self.xform.pk
submission_ids = list(self.xform.instances.values_list(
'pk', flat=True
).all().order_by('pk')[:2])
data = {
'submission_ids': submission_ids,
'validation_status.uid': 'validation_status_on_hold'

}
request = self.factory.patch(
'/', data=data, format='json', **self.extra,
)
response = view(request, pk=formid)
self.assertEqual(response.status_code, status.HTTP_200_OK)
cursor = ParsedInstance.query_mongo_minimal(
query={'_id': {'$in': submission_ids}},
fields=None,
sort=None,
)
for submission in cursor:
self.assertEqual(
submission['_validation_status']['uid'],
'validation_status_on_hold'
)
self.assertEqual(
submission['_validation_status']['by_whom'], self.user.username # bob
)

def test_cannot_access_data_of_pending_delete_xform(self):
# Ensure bob is able to see their data
self.test_data()
Expand Down
47 changes: 0 additions & 47 deletions kobo/apps/openrosa/apps/api/tests/viewsets/test_user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import base64
import os

import pytest
Expand Down Expand Up @@ -92,52 +91,6 @@ def test_cannot_delete_myself(self):
response = self.client.delete(url)
assert response.status_code == status.HTTP_404_NOT_FOUND

def test_only_open_rosa_endpoints_allowed_with_not_validated_password(self):
# log in as bob
self._login_user_and_profile()
self.user.profile.validated_password = True
self.user.profile.save()

# Password is valid, bob should be able to create a new form, submit data
# and browse the API
self.publish_xls_form()
self._submit_data()
assert self.response.status_code == status.HTTP_201_CREATED
# Validate bob is allowed to access all endpoints
self._access_endpoints(access_granted=True)

# Flag Bob's password as not trusted
self.user.profile.validated_password = False
self.user.profile.save()
# Access denied to API endpoints with not validated password - Session auth
self._access_endpoints(access_granted=False)
# Still able to submit data
self._submit_data()
# We are sending a duplicate, we should receive a 202 if not blocked
assert self.response.status_code == status.HTTP_202_ACCEPTED

# Access denied to API endpoints with not validated password - Basic auth
self.client.logout()
headers = {
'HTTP_AUTHORIZATION': 'Basic '
+ base64.b64encode(b'bob:bobbob').decode('ascii')
}
self._access_endpoints(access_granted=False, headers=headers)
# Still able to submit data
self._submit_data()
# We are sending a duplicate, we should receive a 202 if not blocked
assert self.response.status_code == status.HTTP_202_ACCEPTED

# Access denied to API endpoints with not validated password - Token auth
headers = {
'HTTP_AUTHORIZATION': f'Token {self.user.auth_token}'
}
self._access_endpoints(access_granted=False, headers=headers)
# Still able to submit data
self._submit_data()
# We are sending a duplicate, we should receive a 202 if not blocked
assert self.response.status_code == status.HTTP_202_ACCEPTED

def _access_endpoints(self, access_granted: bool, headers: dict = {}):
"""
Validate if `GET` requests return expected status code.
Expand Down
Loading

0 comments on commit a62c289

Please sign in to comment.