Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
4 changes: 4 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

#### Migrations
* 0179_introduce_enable_v1_claims_flow.py - This migration introduces a new `enable_v1_claims_flow` feature flag that allows switching to the v1 claims flow.
* 0180_add_unit_to_partner_field.py - This migration added new field `unit` to `PartnerField` model.

### What's new
* [OSDEV-2176](https://opensupplyhub.atlassian.net/browse/OSDEV-2176) - Added feature flag for v1 claims flow.
* [OSDEV-2065](https://opensupplyhub.atlassian.net/browse/OSDEV-2065) - Updated v1 production locations `POST/PATCH` endpoints to include partner fields:
* Added `unit` field to `PartnerField` model
* Added type validation for submitted partner fields

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
4 changes: 2 additions & 2 deletions src/django/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ def get_ordering(self, request):


class PartnerFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'created_at')
search_fields = ('name', 'type')
list_display = ('name', 'type', 'unit', 'created_at')
search_fields = ('name', 'type', 'unit')
readonly_fields = ('uuid', 'created_at', 'updated_at')


Expand Down
23 changes: 23 additions & 0 deletions src/django/api/migrations/0180_add_unit_to_partner_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import migrations, models
import uuid

class Migration(migrations.Migration):
"""
Migration add unit field to PartnerField model.
"""

dependencies = [
('api', '0179_introduce_enable_v1_claims_flow'),
]

operations = [
migrations.AddField(
model_name='partnerfield',
name='unit',
field=models.CharField(
max_length=200,
blank=True,
help_text=('The partner field unit.')
),
),
]
4 changes: 4 additions & 0 deletions src/django/api/models/partner_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Meta:
blank=False,
choices=TYPE_CHOICES,
help_text=('The partner field type.'))
unit = models.CharField(
max_length=200,
blank=True,
help_text=('The partner field unit.'))

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Dict, List, Mapping, Tuple, Iterable

from rest_framework import status

Expand All @@ -12,53 +12,115 @@

class PermissionProcessor(ContributionProcessor):

TYPE_VALIDATORS = {
'int': lambda value: isinstance(value, int)
and not isinstance(value, bool),
'float': lambda value: isinstance(value, float)
and not isinstance(value, bool),
'string': lambda value: isinstance(value, str),
'object': lambda value: isinstance(
value, (dict, list)
),
}

def process(
self,
event_dto: CreateModerationEventDTO) -> CreateModerationEventDTO:
event_dto: CreateModerationEventDTO
) -> CreateModerationEventDTO:

partner_field_names = PartnerField.objects \
.values_list('name', flat=True)
raw = event_dto.raw_data or {}
if not raw:
return super().process(event_dto)

if event_dto.raw_data:
matching_partner_field_names = [
key for key in event_dto.raw_data.keys()
if key in partner_field_names
]
incoming_keys = set(raw.keys())

if matching_partner_field_names:
contributor_partner_field_names = event_dto.contributor \
.partner_fields.values_list('name', flat=True)
partner_fields_qs = PartnerField.objects \
.filter(name__in=incoming_keys) \
.values_list("name", "type")
partner_fields: Dict[str, str] = dict(partner_fields_qs)

unauthorized_partner_fields = [
name for name in matching_partner_field_names
if name not in contributor_partner_field_names
]
if not partner_fields:
return super().process(event_dto)

if unauthorized_partner_fields:
validation_errors = self.__transform_fields_errors(
unauthorized_partner_fields
)
event_dto.errors = validation_errors
event_dto.status_code = status.HTTP_403_FORBIDDEN
# Permission validation.
contributor_allowed: set[str] = set(
event_dto.contributor.partner_fields.values_list("name", flat=True)
)
requested_partner_field_names: set[str] = set(partner_fields.keys())
unauthorized: set[str] = requested_partner_field_names \
- contributor_allowed

if unauthorized:
event_dto.errors = self.__transform_permission_errors(unauthorized)
event_dto.status_code = status.HTTP_403_FORBIDDEN
return event_dto

# Type validation.
invalid_type_fields = self.__collect_invalid_type_fields(
raw,
partner_fields,
self.TYPE_VALIDATORS
)

if invalid_type_fields:
event_dto.errors = self.__transform_type_errors(
invalid_type_fields
)
event_dto.status_code = status \
.HTTP_422_UNPROCESSABLE_ENTITY

return event_dto
return event_dto

return super().process(event_dto)

@staticmethod
def __transform_fields_errors(fields_errors: List[str]) -> Dict:
validation_errors = {
def __transform_permission_errors(
fields_errors: Iterable[str]
) -> Dict:
return {
'detail': APIV1CommonErrorMessages.COMMON_REQ_BODY_ERROR,
'errors': []
}

for field_name in fields_errors:
validation_errors['errors'].append(
'errors': [
{
'field': field_name,
'detail': 'You do not have permission '
'to contribute to this field.'
'to contribute to this field.',
}
)
for field_name in fields_errors
],
}

@staticmethod
def __transform_type_errors(
invalid_type_fields: List[tuple]
) -> Dict:
return {
'detail': APIV1CommonErrorMessages.COMMON_REQ_BODY_ERROR,
'errors': [
{
'field': name,
'detail': f'Field {name} must be of type {expected}, '
f'but received {type(value).__name__}',
}
for name, expected, value in invalid_type_fields
],
}

@staticmethod
def __collect_invalid_type_fields(
raw: Mapping[str, object],
partner_fields: Mapping[str, str],
validators: Mapping[str, callable],
) -> List[Tuple[str, str, object]]:

invalid_fields: List[Tuple[str, str, object]] = []

for name, field_type in partner_fields.items():
value = raw.get(name)

if value is not None and field_type in validators:
if not validators[field_type](value):
invalid_fields.append(
(name, field_type, value)
)

return validation_errors
return invalid_fields