Skip to content

Commit

Permalink
Feature/missing user serialiser init (#182)
Browse files Browse the repository at this point in the history
* init serializer with user

* add message

* enforce uuid conversion in retriever

* get rid of technical user

* better retrievers

* more precise errors

* rework to have user in serializer/converter

* missing migration
  • Loading branch information
delcroip authored Oct 2, 2024
1 parent 038d1fb commit daaf5a6
Show file tree
Hide file tree
Showing 51 changed files with 353 additions and 151 deletions.
2 changes: 1 addition & 1 deletion api_fhir_r4/containedResources/containedResourceHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ def _is_saved_in_db(self, obj: models.Model):

def _model_to_dict(self, instance):
# Due to how serializers are build simple __dict__ is used instead of builtin model_to_dict
return instance.__dict__
return {k: v for k, v in instance.__dict__.items() if not k.startswith('_')}
4 changes: 3 additions & 1 deletion api_fhir_r4/containedResources/serializerMixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def _get_converted_resources(self, obj):

def to_internal_value(self, data):
audit_user_id = self.get_audit_user_id()
return self.fhirConverter.to_imis_obj(data, audit_user_id).__dict__
imis_obj = self.fhirConverter(user=self.user).to_imis_obj(data, audit_user_id)
# Filter out special attributes
return {k: v for k, v in imis_obj.__dict__.items() if not k.startswith('_') and v is not None}

def create(self, validated_data):
self._create_or_update_contained(validated_data)
Expand Down
40 changes: 31 additions & 9 deletions api_fhir_r4/converters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from fhir.resources.R4B.reference import Reference
from fhir.resources.R4B.identifier import Identifier
from api_fhir_r4.configurations import GeneralConfiguration

from uuid import UUID

class BaseFHIRConverter(ABC):

Expand All @@ -25,13 +25,8 @@ def __init__(self, user=None):
if user:
self.user = user
else:
self.user = core.models.InteractiveUser.objects.filter(
user_roles=core.models.UserRole.objects.filter(
role__is_system=64,
*core.utils.filter_validity()
),
*core.utils.filter_validity()
).first()
raise Exception("Converter init need a valid user")


@classmethod
def to_fhir_obj(cls, obj, reference_type):
Expand Down Expand Up @@ -275,7 +270,34 @@ def get_id_from_reference(cls, reference):
id_from_reference = splited_reference_string.pop()
return id_from_reference


@classmethod
def build_imis_identifier(cls, imis_obj, fhir_obj, errors):
history_model = issubclass(imis_obj.__class__, core.models.HistoryModel)
code = cls.get_fhir_identifier_by_code(
fhir_obj.identifier,
R4IdentifierConfig.get_fhir_generic_type_code()
)
uuid_str = cls.get_fhir_identifier_by_code(
fhir_obj.identifier,
R4IdentifierConfig.get_fhir_uuid_type_code()
)
id_str = None
if not history_model:
id_str = cls.get_fhir_identifier_by_code(
fhir_obj.identifier,
R4IdentifierConfig.get_fhir_acsn_type_code()
)
if id_str:
imis_obj.id = id_str
if uuid_str:
if history_model:
imis_obj.id = UUID(uuid_str)
else:
imis_obj.uuid = UUID(uuid_str)
# if we have the id th uuid in not used
if code:
imis_obj.code = code

from api_fhir_r4.converters.personConverterMixin import PersonConverterMixin
from api_fhir_r4.converters.referenceConverterMixin import ReferenceConverterMixin
from api_fhir_r4.converters.medicationConverter import MedicationConverter
Expand Down
13 changes: 7 additions & 6 deletions api_fhir_r4/converters/medicationConverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from api_fhir_r4.configurations import GeneralConfiguration
import core
import re
from uuid import UUID


class MedicationConverter(BaseFHIRConverter, ReferenceConverterMixin):
Expand All @@ -44,7 +45,7 @@ def to_imis_obj(cls, fhir_medication, audit_user_id):
imis_medication = Item()
imis_medication.audit_user_id = audit_user_id
cls.build_imis_identifier(imis_medication, fhir_medication, errors)
cls.build_imis_item_code(imis_medication, fhir_medication, errors)
#cls.build_imis_item_code(imis_medication, fhir_medication, errors)
cls.build_imis_item_name(imis_medication, fhir_medication, errors)
cls.build_imis_item_package(imis_medication, fhir_medication, errors)
cls.build_imis_item_extension(imis_medication, fhir_medication, errors)
Expand Down Expand Up @@ -85,11 +86,11 @@ def build_fhir_identifiers(cls, fhir_medication, imis_medication):

@classmethod
def build_imis_identifier(cls, imis_medication, fhir_medication, errors):
value = cls.get_fhir_identifier_by_code(fhir_medication.identifier,
R4IdentifierConfig.get_fhir_uuid_type_code())
if value:
imis_medication.code = value
cls.valid_condition(imis_medication.code is None, gettext('Missing the item code'), errors)
super().build_imis_identifier(imis_medication, fhir_medication, errors)
cls.valid_condition(
not imis_medication.code,
gettext('Missing medication `item_code` attribute'), errors
)

@classmethod
def build_fhir_package_form(cls, fhir_medication, imis_medication):
Expand Down
100 changes: 93 additions & 7 deletions api_fhir_r4/mixins.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,105 @@
import logging
from abc import abstractmethod, ABC
from collections.abc import Iterable

from typing import List
from rest_framework import status

from django.core.exceptions import ObjectDoesNotExist, FieldError
from django.http import Http404

from rest_framework import mixins

from api_fhir_r4.model_retrievers import GenericModelRetriever
from rest_framework.response import Response

from core.models import HistoryModel
from api_fhir_r4.multiserializer.mixins import MultiSerializerUpdateModelMixin, MultiSerializerRetrieveModelMixin
from rest_framework.mixins import (
CreateModelMixin as RestCreateModelMixin,
UpdateModelMixin as RestUpdateModelMixin,
ListModelMixin as RestListModelMixin,
DestroyModelMixin as RestDestroyModelMixin,
RetrieveModelMixin as RestRetrieveModelMixin,
)

logger = logging.getLogger(__name__)


class CreateModelMixin(RestCreateModelMixin):

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, user=request.user)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
headers=headers
)


class UpdateModelMixin(RestUpdateModelMixin):
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial, user=request.user)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)

if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}

return Response(serializer.data)


class ListModelMixin(RestListModelMixin):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if not isinstance(queryset, Iterable):
queryset = queryset.all().order_by('code' if hasattr(queryset.model, 'code') else 'id')
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, user=request.user)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

class DestroyModelMixin(RestDestroyModelMixin):
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.user = request.user
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

def perform_destroy(self, instance):
if issubclass(instance.__class__, HistoryModel):
instance.delete(user=self.user)
else:
instance.delete()


class RetrieveModelMixin(RestRetrieveModelMixin):
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
self.user = request.user
instance = self.get_object()
serializer = self.get_serializer(instance, user=self.user)
return Response(serializer.data)


class GenericMultiIdentifierMixin(ABC):
lookup_field = 'identifier'

Expand Down Expand Up @@ -44,15 +129,15 @@ def _get_object_with_first_valid_retriever(self, identifier):
raise Http404(f"Resource for identifier {identifier} not found")


class MultiIdentifierRetrieverMixin(mixins.RetrieveModelMixin, GenericMultiIdentifierMixin, ABC):
class MultiIdentifierRetrieverMixin(RetrieveModelMixin, GenericMultiIdentifierMixin, ABC):

def retrieve(self, request, *args, **kwargs):
ref_type, instance = self._get_object_with_first_valid_retriever(kwargs['identifier'])
serializer = self.get_serializer(instance, reference_type=ref_type)
serializer = self.get_serializer(instance, reference_type=ref_type, user=request.user)
return Response(serializer.data)


class MultiIdentifierUpdateMixin(mixins.UpdateModelMixin, GenericMultiIdentifierMixin, ABC):
class MultiIdentifierUpdateMixin(UpdateModelMixin, GenericMultiIdentifierMixin, ABC):

def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
Expand Down Expand Up @@ -101,7 +186,7 @@ def update(self, request, *args, **kwargs):
for serializer, (qs, _, _) in self.get_eligible_serializers_iterator():
ref_type, instance = self._get_object_with_first_valid_retriever(qs, kwargs['identifier'])
update_result = self._update_for_serializer(serializer, instance, request.data, partial,
reference_type=ref_type)
reference_type=ref_type, user=request.user)
results.append(update_result)

response = results[0] # By default there should be only one eligible serializer
Expand All @@ -126,3 +211,4 @@ def retrieve(self, request, *args, **kwargs):
raise Http404(f"Resource for identifier {kwargs['identifier']} not found")

return Response(retrieved[0])

26 changes: 14 additions & 12 deletions api_fhir_r4/model_retrievers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from django.db.models.query import QuerySet
from django.db.models import Model

from insuree.services import validate_insuree_number
from api_fhir_r4.converters import ReferenceConverterMixin

from django.core.exceptions import ValidationError

class GenericModelRetriever(ABC):

Expand Down Expand Up @@ -36,7 +36,16 @@ def retriever_additional_queryset_filtering(cls, queryset):

@classmethod
def get_model_object(cls, queryset: QuerySet, identifier_value) -> Model:
return queryset.get(**{cls.identifier_field: identifier_value})
filters = {}
if cls.serializer_reference_type == 'uuid_reference':
identifier_value = uuid.UUID(str(identifier_value))
elif hasattr(queryset.model, 'validity_to'):
filters['validity_to__isnull'] = True
filters[cls.identifier_field] = identifier_value
try:
return queryset.get(**filters)
except Exception as e:
raise ValidationError(f"failed to retrieve {queryset.model.__name__} with the filter {filters}; details {e}")


class UUIDIdentifierModelRetriever(GenericModelRetriever):
Expand Down Expand Up @@ -73,10 +82,7 @@ class CodeIdentifierModelRetriever(GenericModelRetriever):
def identifier_validator(cls, identifier_value):
return isinstance(identifier_value, str)

@classmethod
def add_retriever_queryset_filtering(cls, queryset):
# By default no additional changes are made in queryset
return queryset.filter(validity_to__is_null=True)



class CHFIdentifierModelRetriever(CodeIdentifierModelRetriever):
Expand All @@ -85,11 +91,7 @@ class CHFIdentifierModelRetriever(CodeIdentifierModelRetriever):
@classmethod
def identifier_validator(cls, identifier_value):
# From model specification
return isinstance(identifier_value, str) and len(identifier_value) <= 12

@classmethod
def get_model_object(cls, queryset: QuerySet, identifier_value) -> Model:
return queryset.get(**{cls.identifier_field: identifier_value, 'validity_to__isnull': True})
return isinstance(identifier_value, str) and validate_insuree_number(identifier_value)


class GroupIdentifierModelRetriever(CHFIdentifierModelRetriever):
Expand Down
6 changes: 3 additions & 3 deletions api_fhir_r4/multiserializer/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,15 +379,15 @@ def update(self, request, *args, **kwargs):
results = []
for serializer, (qs, _, _) in self.get_eligible_serializers_iterator():
instance = self.get_object_by_queryset(qs=qs)
update_result = self._update_for_serializer(serializer, instance, request.data, partial)
update_result = self._update_for_serializer(serializer, instance, request.data, partial, user=request.user)
results.append(update_result)

response = results[0] # By default there should be only one eligible serializer
return Response(response)

def _update_for_serializer(self, serializer, instance, data, partial, *args, **kwargs):
def _update_for_serializer(self, serializer, instance, data, partial, user=None, *args, **kwargs):
context = self.get_serializer_context() # Required for audit user id
serializer = serializer(instance, data=data, partial=partial, context=context, *args, **kwargs)
serializer = serializer(instance, data=data, partial=partial, context=context, user=user, *args, **kwargs)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
Expand Down
9 changes: 9 additions & 0 deletions api_fhir_r4/multiserializer/serializerClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ class MultiSerializerSerializerClass(Serializer):
"""
Serves as base serializer class for instances using multiserializer mixin.
"""
user = None

def __init__(self, user=None, **kwargs):
if user:
self.user = user
else:
context = kwargs.get('context', None)
if context and hasattr(context, 'user'):
self.user = context.user

def update(self, instance, validated_data):
raise NotImplementedError("MultiSerializerSerializerClass `update` not supported. Should be"
Expand Down
6 changes: 4 additions & 2 deletions api_fhir_r4/paymentNotice/serializer/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ def create(cls, validated_data, request):
invoice_status = validated_data.pop('invoice_status')

payment_invoice = copy.deepcopy(validated_data)
del payment_invoice['_state']
del payment_invoice['_original_state']
if '_state' in payment_invoice:
del payment_invoice['_state']
if '_original_state' in payment_invoice:
del payment_invoice['_original_state']

payment_invoice_service = PaymentInvoiceService(user)
result = payment_invoice_service.create_with_detail(payment_invoice, imis_payment_detail)
Expand Down
Loading

0 comments on commit daaf5a6

Please sign in to comment.