Skip to content

Commit 370933a

Browse files
authored
feat(organizations): add new asset endpoint for owners and admins TASK-960 (#5218)
### Summary: Added a new endpoint to retrieve organization assets, accessible to both owners and admins. ### Description: This update introduces a dedicated endpoint that allows organization owners and admins to access a complete list of assets associated with their organization. Owners and admins have unrestricted access to these assets by virtue of their roles, without needing explicit permissions.
1 parent fc38584 commit 370933a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1821
-724
lines changed

hub/models/extra_user_detail.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,6 @@ def _sync_org_name(self):
7878
except (KeyError, AttributeError):
7979
organization_name = None
8080

81-
user_organization.name = organization_name
82-
user_organization.save(update_fields=['name'])
81+
if organization_name:
82+
user_organization.name = organization_name
83+
user_organization.save(update_fields=['name'])

kobo/apps/hook/tests/hook_test_case.py

Lines changed: 4 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
1-
# coding: utf-8
21
import json
32
import uuid
43

5-
import pytest
6-
import responses
7-
from django.conf import settings
8-
from django.urls import reverse
9-
from rest_framework import status
10-
11-
from kpi.constants import SUBMISSION_FORMAT_TYPE_JSON, SUBMISSION_FORMAT_TYPE_XML
12-
from kpi.exceptions import BadFormatException
134
from kpi.tests.kpi_test_case import KpiTestCase
14-
from ..constants import HOOK_LOG_FAILED
15-
from ..exceptions import HookRemoteServerDownError
16-
from ..models import Hook, HookLog
5+
from ..models import Hook
6+
from ..utils.tests.mixins import HookTestCaseMixin
177

188

19-
class HookTestCase(KpiTestCase):
9+
class HookTestCase(HookTestCaseMixin, KpiTestCase):
2010

2111
def setUp(self):
2212
self.client.login(username='someuser', password='someuser')
@@ -49,115 +39,8 @@ def setUp(self):
4939
self.asset.deploy(backend='mock', active=True)
5040
self.asset.save()
5141
self.hook = Hook()
52-
self._submission_pk = 1
53-
54-
settings.CELERY_TASK_ALWAYS_EAGER = True
55-
56-
def _create_hook(self, return_response_only=False, **kwargs):
57-
58-
format_type = kwargs.get('format_type', SUBMISSION_FORMAT_TYPE_JSON)
59-
if format_type not in [
60-
SUBMISSION_FORMAT_TYPE_JSON,
61-
SUBMISSION_FORMAT_TYPE_XML,
62-
]:
63-
raise BadFormatException(
64-
'The format {} is not supported'.format(format_type)
65-
)
66-
67-
self.__prepare_submission()
68-
69-
url = reverse('hook-list', args=(self.asset.uid,))
70-
data = {
71-
'name': kwargs.get('name', 'some external service with token'),
72-
'endpoint': kwargs.get('endpoint', 'http://external.service.local/'),
73-
'settings': kwargs.get('settings', {
74-
'custom_headers': {
75-
'X-Token': '1234abcd'
76-
}
77-
}),
78-
'export_type': format_type,
79-
'active': kwargs.get('active', True),
80-
'subset_fields': kwargs.get('subset_fields', []),
81-
'payload_template': kwargs.get('payload_template', None)
82-
}
83-
84-
response = self.client.post(url, data, format='json')
85-
if return_response_only:
86-
return response
87-
else:
88-
self.assertEqual(
89-
response.status_code, status.HTTP_201_CREATED, msg=response.data
90-
)
91-
hook = self.asset.hooks.last()
92-
self.assertTrue(hook.active)
93-
return hook
94-
95-
def _send_and_fail(self):
96-
"""
97-
The public method which calls this method needs to be decorated by
98-
`@responses.activate`
99-
100-
:return: dict
101-
"""
102-
first_hooklog_response = self._send_and_wait_for_retry()
103-
104-
# Fakes Celery n retries by forcing status to `failed`
105-
# (where n is `settings.HOOKLOG_MAX_RETRIES`)
106-
first_hooklog = HookLog.objects.get(uid=first_hooklog_response.get('uid'))
107-
first_hooklog.change_status(HOOK_LOG_FAILED)
108-
109-
return first_hooklog_response
110-
111-
def _send_and_wait_for_retry(self):
112-
self.hook = self._create_hook()
113-
114-
ServiceDefinition = self.hook.get_service_definition()
115-
submissions = self.asset.deployment.get_submissions(self.asset.owner)
116-
submission_id = submissions[0]['_id']
117-
service_definition = ServiceDefinition(self.hook, submission_id)
118-
first_mock_response = {'error': 'gateway timeout'}
119-
120-
# Mock first request's try
121-
responses.add(
122-
responses.POST,
123-
self.hook.endpoint,
124-
json=first_mock_response,
125-
status=status.HTTP_504_GATEWAY_TIMEOUT,
126-
)
127-
128-
# Mock next requests' tries
129-
responses.add(
130-
responses.POST,
131-
self.hook.endpoint,
132-
status=status.HTTP_200_OK,
133-
content_type='application/json',
134-
)
135-
136-
# Try to send data to external endpoint
137-
with pytest.raises(HookRemoteServerDownError):
138-
service_definition.send()
139-
140-
# Retrieve the corresponding log
141-
url = reverse('hook-log-list', kwargs={
142-
'parent_lookup_asset': self.hook.asset.uid,
143-
'parent_lookup_hook': self.hook.uid
144-
})
145-
146-
response = self.client.get(url)
147-
first_hooklog_response = response.data.get('results')[0]
148-
149-
# Result should match first try
150-
self.assertEqual(
151-
first_hooklog_response.get('status_code'),
152-
status.HTTP_504_GATEWAY_TIMEOUT,
153-
)
154-
self.assertEqual(
155-
json.loads(first_hooklog_response.get('message')),
156-
first_mock_response,
157-
)
158-
return first_hooklog_response
15942

160-
def __prepare_submission(self):
43+
def _add_submissions(self):
16144
v_uid = self.asset.latest_deployed_version.uid
16245
self.submission = {
16346
'__version__': v_uid,

kobo/apps/hook/utils/tests/__init__.py

Whitespace-only changes.

kobo/apps/hook/utils/tests/mixins.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import json
2+
3+
import pytest
4+
import responses
5+
from django.urls import reverse
6+
from rest_framework import status
7+
8+
from kpi.constants import SUBMISSION_FORMAT_TYPE_JSON, SUBMISSION_FORMAT_TYPE_XML
9+
from kpi.exceptions import BadFormatException
10+
from kobo.apps.hook.constants import HOOK_LOG_FAILED
11+
from kobo.apps.hook.exceptions import HookRemoteServerDownError
12+
from kobo.apps.hook.models import HookLog
13+
14+
15+
class HookTestCaseMixin:
16+
17+
def _create_hook(self, return_response_only=False, **kwargs):
18+
19+
format_type = kwargs.get('format_type', SUBMISSION_FORMAT_TYPE_JSON)
20+
if format_type not in [
21+
SUBMISSION_FORMAT_TYPE_JSON,
22+
SUBMISSION_FORMAT_TYPE_XML,
23+
]:
24+
raise BadFormatException(
25+
'The format {} is not supported'.format(format_type)
26+
)
27+
28+
self._add_submissions()
29+
30+
url = reverse('hook-list', args=(self.asset.uid,))
31+
data = {
32+
'name': kwargs.get('name', 'some external service with token'),
33+
'endpoint': kwargs.get('endpoint', 'http://external.service.local/'),
34+
'settings': kwargs.get('settings', {
35+
'custom_headers': {
36+
'X-Token': '1234abcd'
37+
}
38+
}),
39+
'export_type': format_type,
40+
'active': kwargs.get('active', True),
41+
'subset_fields': kwargs.get('subset_fields', []),
42+
'payload_template': kwargs.get('payload_template', None)
43+
}
44+
45+
response = self.client.post(url, data, format='json')
46+
if return_response_only:
47+
return response
48+
else:
49+
self.assertEqual(
50+
response.status_code, status.HTTP_201_CREATED, msg=response.data
51+
)
52+
hook = self.asset.hooks.last()
53+
self.assertTrue(hook.active)
54+
return hook
55+
56+
def _send_and_fail(self) -> dict:
57+
"""
58+
The public method which calls this method needs to be decorated by
59+
`@responses.activate`
60+
"""
61+
62+
first_hooklog_response = self._send_and_wait_for_retry()
63+
64+
# Fakes Celery n retries by forcing status to `failed`
65+
# (where n is `settings.HOOKLOG_MAX_RETRIES`)
66+
first_hooklog = HookLog.objects.get(uid=first_hooklog_response.get('uid'))
67+
first_hooklog.change_status(HOOK_LOG_FAILED)
68+
69+
return first_hooklog_response
70+
71+
def _send_and_wait_for_retry(self):
72+
self.hook = self._create_hook()
73+
74+
ServiceDefinition = self.hook.get_service_definition()
75+
submissions = self.asset.deployment.get_submissions(self.asset.owner)
76+
submission_id = submissions[0]['_id']
77+
service_definition = ServiceDefinition(self.hook, submission_id)
78+
first_mock_response = {'error': 'gateway timeout'}
79+
80+
# Mock first request's try
81+
responses.add(
82+
responses.POST,
83+
self.hook.endpoint,
84+
json=first_mock_response,
85+
status=status.HTTP_504_GATEWAY_TIMEOUT,
86+
)
87+
88+
# Mock next requests' tries
89+
responses.add(
90+
responses.POST,
91+
self.hook.endpoint,
92+
status=status.HTTP_200_OK,
93+
content_type='application/json',
94+
)
95+
96+
# Try to send data to external endpoint
97+
with pytest.raises(HookRemoteServerDownError):
98+
service_definition.send()
99+
100+
# Retrieve the corresponding log
101+
url = reverse('hook-log-list', kwargs={
102+
'parent_lookup_asset': self.hook.asset.uid,
103+
'parent_lookup_hook': self.hook.uid
104+
})
105+
106+
response = self.client.get(url)
107+
first_hooklog_response = response.data.get('results')[0]
108+
109+
# Result should match first try
110+
self.assertEqual(
111+
first_hooklog_response.get('status_code'),
112+
status.HTTP_504_GATEWAY_TIMEOUT,
113+
)
114+
self.assertEqual(
115+
json.loads(first_hooklog_response.get('message')),
116+
first_mock_response,
117+
)
118+
return first_hooklog_response

kobo/apps/kobo_auth/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from kobo.apps.openrosa.libs.permissions import get_model_permission_codenames
99
from kobo.apps.organizations.models import create_organization, Organization
1010
from kpi.utils.database import update_autofield_sequence, use_db
11+
from kpi.utils.permissions import is_user_anonymous
1112

1213

1314
class User(AbstractUser):
@@ -52,6 +53,9 @@ def is_org_owner(self):
5253
@property
5354
@cache_for_request
5455
def organization(self):
56+
if is_user_anonymous(self):
57+
return
58+
5559
# Database allows multiple organizations per user, but we restrict it to one.
5660
if organization := Organization.objects.filter(
5761
organization_users__user=self

kobo/apps/openrosa/apps/api/viewsets/xform_submission_api.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ def create(self, request, *args, **kwargs):
164164
username = user.username
165165

166166
if request.method.upper() == 'HEAD':
167-
return Response(status=status.HTTP_204_NO_CONTENT,
168-
headers=self.get_openrosa_headers(request),
169-
template_name=self.template_name)
167+
return Response(
168+
status=status.HTTP_204_NO_CONTENT,
169+
headers=self.get_openrosa_headers(request),
170+
template_name=self.template_name,
171+
)
170172

171173
is_json_request = is_json(request)
172174

0 commit comments

Comments
 (0)