-
Notifications
You must be signed in to change notification settings - Fork 9
[OSDEV-2357] Add API endpoint to list partner fields #873
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
Merged
protsack-stephan
merged 8 commits into
main
from
OSDEV-2357-add-parnter-field-endpoints
Feb 10, 2026
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7dc9a9e
Add list partner fields API endpoint
protsack-stephan 021038b
Add docstrings to pratner fields view set and serializer
protsack-stephan f0f3126
Add unit tests
protsack-stephan 190d00f
Merge branch 'main' of github.com:opensupplyhub/open-supply-hub into …
protsack-stephan 47dbbc7
Update release notes and fix secuirty flag
protsack-stephan 23254b7
Update another password to be autogenerated
protsack-stephan f92a099
Merge branch 'main' of github.com:opensupplyhub/open-supply-hub into …
protsack-stephan c02f535
Remove usless code and update the docs
protsack-stephan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/django/api/serializers/partner_field/partner_field_serializer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """ | ||
| Serializer for partner fields. | ||
| """ | ||
|
|
||
| from rest_framework.serializers import ModelSerializer | ||
| from api.models.partner_field import PartnerField | ||
|
|
||
|
|
||
| class PartnerFieldSerializer(ModelSerializer): | ||
| """ | ||
| Serializer for partner fields. | ||
| """ | ||
|
|
||
| class Meta: | ||
| """ | ||
| Meta class for partner field serializer. | ||
| """ | ||
|
|
||
| model = PartnerField | ||
| fields = [ | ||
| "uuid", | ||
| "name", | ||
| "type", | ||
| "json_schema", | ||
| "active", | ||
| "system_field", | ||
| "created_at", | ||
| "updated_at", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| """ | ||
| Tests for the PartnerFieldsViewSet. | ||
| """ | ||
|
|
||
| import secrets | ||
|
|
||
| from rest_framework import status | ||
| from rest_framework.test import APITestCase | ||
|
|
||
| from api.models import User | ||
| from api.models.partner_field import PartnerField | ||
|
|
||
|
|
||
| class PartnerFieldsViewSetTest(APITestCase): | ||
| """ | ||
| Test cases for the partner fields API endpoint. | ||
| """ | ||
|
|
||
| def setUp(self): | ||
| self.user_email = "[email protected]" | ||
| self.user_password = "password123" | ||
| self.user = User.objects.create(email=self.user_email) | ||
| self.user.set_password(self.user_password) | ||
| self.user.save() | ||
|
|
||
| self.superuser_email = "[email protected]" | ||
| self.superuser_password = secrets.token_urlsafe(16) | ||
| self.superuser = User.objects.create_superuser( | ||
| self.superuser_email, self.superuser_password | ||
| ) | ||
|
|
||
| self.url = "/api/partner-fields/" | ||
|
|
||
| def _login_superuser(self): | ||
| """Helper to login as superuser.""" | ||
| self.client.login( | ||
| email=self.superuser_email, | ||
| password=self.superuser_password, | ||
| ) | ||
|
|
||
| def _login_user(self): | ||
| """Helper to login as user.""" | ||
| self.client.login( | ||
| email=self.user_email, | ||
| password=self.user_password, | ||
| ) | ||
|
|
||
| def _get_field_uuids(self, response): | ||
| """Helper to get field uuids from response.""" | ||
| return [result["uuid"] for result in response.data["results"]] | ||
|
|
||
| def _create_partner_field( | ||
| self, | ||
| name, | ||
| field_type=PartnerField.STRING, | ||
| active=True, | ||
| system_field=False, | ||
| ): | ||
| """Helper to create a partner field.""" | ||
| return PartnerField.objects.get_all_including_inactive().create( | ||
| name=name, | ||
| type=field_type, | ||
| active=active, | ||
| system_field=system_field, | ||
| ) | ||
|
|
||
| def test_returns_401_for_unauthenticated_request(self): | ||
| """Verify endpoint returns 401 for unauthenticated requests.""" | ||
| response = self.client.get(self.url) | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_401_UNAUTHORIZED, | ||
| ) | ||
|
|
||
| def test_returns_403_for_non_superuser(self): | ||
| """Verify endpoint returns 403 for non-superuser requests.""" | ||
| self._login_user() | ||
| response = self.client.get(self.url) | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
||
| def test_returns_200_for_superuser(self): | ||
| """Verify endpoint returns 200 for superuser requests.""" | ||
| self._login_superuser() | ||
| response = self.client.get(self.url) | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_200_OK, | ||
| ) | ||
|
|
||
| def test_returns_partner_fields_for_superuser(self): | ||
| """Verify endpoint returns paginated partner fields for superusers.""" | ||
| fields = [ | ||
| self._create_partner_field("test_field_1"), | ||
| self._create_partner_field("test_field_2", PartnerField.INT), | ||
| ] | ||
| self._login_superuser() | ||
| response = self.client.get(self.url) | ||
|
|
||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_200_OK, | ||
| ) | ||
| self.assertIn("results", response.data) | ||
|
|
||
| result_uuids = self._get_field_uuids(response) | ||
|
|
||
| for field in fields: | ||
| self.assertIn(str(field.uuid), result_uuids) | ||
|
|
||
| def test_limit_parameter_controls_page_size(self): | ||
| """Verify ?limit= parameter controls page size.""" | ||
| for i in range(5): | ||
| self._create_partner_field(f"limit_test_field_{i}") | ||
|
|
||
| self._login_superuser() | ||
| response = self.client.get(self.url, {"limit": 2}) | ||
|
|
||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_200_OK, | ||
| ) | ||
| self.assertEqual(len(response.data["results"]), 2) | ||
|
|
||
| def test_cursor_pagination_works_correctly(self): | ||
| """Verify cursor pagination returns different results on next page.""" | ||
| for i in range(5): | ||
| self._create_partner_field(f"cursor_test_field_{i}") | ||
|
|
||
| self._login_superuser() | ||
|
|
||
| first_response = self.client.get(self.url, {"limit": 2}) | ||
| self.assertEqual(first_response.status_code, status.HTTP_200_OK) | ||
| first_page_uuids = self._get_field_uuids(first_response) | ||
|
|
||
| next_url = first_response.data["next"] | ||
| self.assertIsNotNone(next_url) | ||
|
|
||
| second_response = self.client.get(next_url) | ||
| self.assertEqual( | ||
| second_response.status_code, | ||
| status.HTTP_200_OK, | ||
| ) | ||
| second_page_uuids = self._get_field_uuids(second_response) | ||
|
|
||
| self.assertTrue( | ||
| set(first_page_uuids).isdisjoint( | ||
| set(second_page_uuids), | ||
| ), | ||
| ) | ||
|
|
||
| def test_only_active_fields_are_returned(self): | ||
| """Verify only active partner fields are returned.""" | ||
| active_field = self._create_partner_field( | ||
| "active_field", | ||
| active=True, | ||
| ) | ||
| inactive_field = self._create_partner_field( | ||
| "inactive_field", | ||
| active=False, | ||
| ) | ||
|
|
||
| self._login_superuser() | ||
| response = self.client.get(self.url) | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
| result_uuids = self._get_field_uuids(response) | ||
|
|
||
| self.assertIn(str(active_field.uuid), result_uuids) | ||
| self.assertNotIn(str(inactive_field.uuid), result_uuids) | ||
|
|
||
| def test_max_page_size_is_enforced(self): | ||
| """Verify max page size (100) is enforced.""" | ||
| for i in range(105): | ||
| self._create_partner_field(f"max_size_test_field_{i}") | ||
|
|
||
| self._login_superuser() | ||
| response = self.client.get(self.url, {"limit": 200}) | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
| self.assertLessEqual(len(response.data["results"]), 100) | ||
|
|
||
| def test_cant_update_partner_fields(self): | ||
| """Verify partner fields can't be updated.""" | ||
| field = self._create_partner_field("test_field") | ||
| self._login_superuser() | ||
| response = self.client.put( | ||
| f"{self.url}{field.uuid}/", | ||
| {"name": "new_name"}, | ||
| ) | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| def test_cant_delete_partner_fields(self): | ||
| """Verify partner fields can't be deleted.""" | ||
| field = self._create_partner_field("test_field") | ||
| self._login_superuser() | ||
| response = self.client.delete(f"{self.url}{field.uuid}/") | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| def test_cant_create_partner_fields(self): | ||
| """Verify partner fields can't be created.""" | ||
| self._login_superuser() | ||
| response = self.client.post(self.url, {"name": "new_field"}) | ||
| self.assertEqual( | ||
| response.status_code, | ||
| status.HTTP_405_METHOD_NOT_ALLOWED, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
src/django/api/views/partner_fields/partner_fields_view_set.py
vlad-shapik marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """ | ||
| Viewset for partner fields with cursor-based pagination. | ||
| Allows client to control page size via ?limit=. | ||
| """ | ||
|
|
||
| from drf_yasg.utils import swagger_auto_schema | ||
| from drf_yasg import openapi | ||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||
| from api.models.partner_field import PartnerField | ||
| from api.permissions import IsSuperuser | ||
| from api.serializers.partner_field.partner_field_serializer import ( | ||
| PartnerFieldSerializer, | ||
| ) | ||
| from rest_framework.pagination import CursorPagination | ||
|
|
||
|
|
||
| class PartnerFieldCursorPagination(CursorPagination): | ||
VadimKovalenkoSNF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Pagination class for partner fields with cursor-based pagination. | ||
| Allows client to control page size via ?limit=. | ||
| """ | ||
|
|
||
| page_size = 20 | ||
| ordering = "created_at" | ||
| page_size_query_param = "limit" | ||
| max_page_size = 100 | ||
|
|
||
|
|
||
| class PartnerFieldsViewSet(ReadOnlyModelViewSet): | ||
| """ | ||
| Viewset for partner fields with cursor-based pagination. | ||
| Allows client to control page size via ?limit=. | ||
vlad-shapik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
|
|
||
| queryset = PartnerField.objects.all() | ||
| serializer_class = PartnerFieldSerializer | ||
| permission_classes = [IsSuperuser] | ||
VadimKovalenkoSNF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| throttle_classes = [] | ||
| pagination_class = PartnerFieldCursorPagination | ||
|
|
||
| @swagger_auto_schema( | ||
| operation_description="List partner fields with pagination.", | ||
| manual_parameters=[ | ||
| openapi.Parameter( | ||
| "limit", | ||
| openapi.IN_QUERY, | ||
| description="Number of results to return per page.", | ||
| type=openapi.TYPE_INTEGER, | ||
| ), | ||
| openapi.Parameter( | ||
| "cursor", | ||
| openapi.IN_QUERY, | ||
| description="Cursor for paginating results.", | ||
| type=openapi.TYPE_STRING, | ||
| ), | ||
| ], | ||
| ) | ||
| def list(self, request, *args, **kwargs): | ||
| """ | ||
| List partner fields. | ||
| Supports cursor-based pagination and limit parameter. | ||
| """ | ||
| return super().list(request, *args, **kwargs) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.