this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add API's for Specimen and Observation Definition

+803 -55
+1
.cursorignore
··· 1 + data/*
+59
.cursorrules
··· 1 + You are an expert in Python, Django, and scalable web application development. 2 + 3 + Key Principles 4 + 5 + - Write clear, technical responses with precise Django examples. 6 + - Use Django's built-in features and tools wherever possible to leverage its full capabilities. 7 + - Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance). 8 + - Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables). 9 + - Structure your project in a modular way using Django apps to promote reusability and separation of concerns. 10 + 11 + Django/Python 12 + 13 + - Use Django’s class-based views (CBVs) for more complex views; prefer function-based views (FBVs) for simpler logic. 14 + - Leverage Django’s ORM for database interactions; avoid raw SQL queries unless necessary for performance. 15 + - Use Django’s built-in user model and authentication framework for user management. 16 + - Utilize Django's form and model form classes for form handling and validation. 17 + - Follow the MVT (Model-View-Template) pattern strictly for clear separation of concerns. 18 + - Use middleware judiciously to handle cross-cutting concerns like authentication, logging, and caching. 19 + 20 + Error Handling and Validation 21 + 22 + - Implement error handling at the view level and use Django's built-in error handling mechanisms. 23 + - Use Django's validation framework to validate form and model data. 24 + - Prefer try-except blocks for handling exceptions in business logic and views. 25 + - Customize error pages (e.g., 404, 500) to improve user experience and provide helpful information. 26 + - Use Django signals to decouple error handling and logging from core business logic. 27 + 28 + Dependencies 29 + 30 + - Django 31 + - Django REST Framework (for API development) 32 + - Celery (for background tasks) 33 + - Redis (for caching and task queues) 34 + - PostgreSQL or MySQL (preferred databases for production) 35 + 36 + Django-Specific Guidelines 37 + 38 + - Use Django templates for rendering HTML and DRF serializers for JSON responses. 39 + - Keep business logic in models and forms; keep views light and focused on request handling. 40 + - Use Django's URL dispatcher (urls.py) to define clear and RESTful URL patterns. 41 + - Apply Django's security best practices (e.g., CSRF protection, SQL injection protection, XSS prevention). 42 + - Use Django’s built-in tools for testing (unittest and pytest-django) to ensure code quality and reliability. 43 + - Leverage Django’s caching framework to optimize performance for frequently accessed data. 44 + - Use Django’s middleware for common tasks such as authentication, logging, and security. 45 + 46 + Performance Optimization 47 + 48 + - Optimize query performance using Django ORM's select_related and prefetch_related for related object fetching. 49 + - Use Django’s cache framework with backend support (e.g., Redis or Memcached) to reduce database load. 50 + - Implement database indexing and query optimization techniques for better performance. 51 + - Use asynchronous views and background tasks (via Celery) for I/O-bound or long-running operations. 52 + 53 + Key Conventions 54 + 55 + 1. Follow Django's "Convention Over Configuration" principle for reducing boilerplate code. 56 + 2. Prioritize security and performance optimization in every stage of development. 57 + 3. Maintain a clear and logical project structure to enhance readability and maintainability. 58 + 59 + Refer to Django documentation for best practices in views, models, forms, and security considerations.
+1 -1
care/emr/api/viewsets/observation_definition.py
··· 23 23 facility = filters.UUIDFilter(field_name="facility__external_id") 24 24 25 25 26 - class ObservationViewSet( 26 + class ObservationDefinitionViewSet( 27 27 EMRCreateMixin, EMRRetrieveMixin, EMRUpdateMixin, EMRListMixin, EMRBaseViewSet 28 28 ): 29 29 database_model = ObservationDefinition
+71
care/emr/api/viewsets/specimen_definition.py
··· 1 + from django_filters import rest_framework as filters 2 + from rest_framework.exceptions import PermissionDenied 3 + from rest_framework.generics import get_object_or_404 4 + 5 + from care.emr.api.viewsets.base import ( 6 + EMRBaseViewSet, 7 + EMRCreateMixin, 8 + EMRListMixin, 9 + EMRRetrieveMixin, 10 + EMRUpdateMixin, 11 + ) 12 + from care.emr.models.specimen_definition import SpecimenDefinition 13 + from care.emr.resources.specimen_definition.spec import ( 14 + BaseSpecimenDefinitionSpec, 15 + SpecimenDefinitionReadSpec, 16 + ) 17 + from care.facility.models import Facility 18 + from care.security.authorization import AuthorizationController 19 + 20 + 21 + class SpecimenDefinitionFilters(filters.FilterSet): 22 + status = filters.CharFilter(field_name="status", lookup_expr="iexact") 23 + 24 + 25 + class SpecimenDefinitionViewSet( 26 + EMRCreateMixin, EMRRetrieveMixin, EMRUpdateMixin, EMRListMixin, EMRBaseViewSet 27 + ): 28 + database_model = SpecimenDefinition 29 + pydantic_model = BaseSpecimenDefinitionSpec 30 + pydantic_read_model = SpecimenDefinitionReadSpec 31 + filterset_class = SpecimenDefinitionFilters 32 + filter_backends = [filters.DjangoFilterBackend] 33 + 34 + def get_facility_obj(self): 35 + return get_object_or_404( 36 + Facility, external_id=self.kwargs["facility_external_id"] 37 + ) 38 + 39 + def perform_create(self, instance): 40 + instance.facility = self.get_facility_obj() 41 + super().perform_create(instance) 42 + 43 + def authorize_create(self, instance): 44 + """ 45 + The user must have permission to create specimen definition in the facility. 46 + """ 47 + facility = self.get_facility_obj() 48 + if not AuthorizationController.call( 49 + "can_write_facility_specimen_definition", 50 + self.request.user, 51 + facility, 52 + ): 53 + raise PermissionDenied("Access Denied to Specimen Definition") 54 + 55 + def authorize_update(self, request_obj, model_instance): 56 + self.authorize_create(model_instance) 57 + 58 + def get_queryset(self): 59 + """ 60 + If no facility filters are applied, all objects must be returned without a facility filter. 61 + If facility filter is applied, check for read permission and return all inside facility. 62 + """ 63 + base_queryset = self.database_model.objects.all() 64 + facility_obj = self.get_facility_obj() 65 + if not AuthorizationController.call( 66 + "can_list_facility_specimen_definition", 67 + self.request.user, 68 + facility_obj, 69 + ): 70 + raise PermissionDenied("Access Denied to Specimen Definition") 71 + return base_queryset.filter(facility=facility_obj)
-49
care/emr/migrations/0022_observationdefinition.py
··· 1 - # Generated by Django 5.1.4 on 2025-04-07 07:16 2 - 3 - import django.db.models.deletion 4 - import uuid 5 - from django.conf import settings 6 - from django.db import migrations, models 7 - 8 - 9 - class Migration(migrations.Migration): 10 - 11 - dependencies = [ 12 - ('emr', '0021_metaartifact'), 13 - ('facility', '0476_facility_default_internal_organization_and_more'), 14 - migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 - ] 16 - 17 - operations = [ 18 - migrations.CreateModel( 19 - name='ObservationDefinition', 20 - fields=[ 21 - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 - ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), 23 - ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), 24 - ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), 25 - ('deleted', models.BooleanField(db_index=True, default=False)), 26 - ('history', models.JSONField(default=dict)), 27 - ('meta', models.JSONField(default=dict)), 28 - ('version', models.IntegerField(default=1)), 29 - ('slug', models.CharField(max_length=255)), 30 - ('name', models.CharField(max_length=1024)), 31 - ('status', models.CharField(max_length=255)), 32 - ('description', models.TextField()), 33 - ('derived_from_uri', models.TextField()), 34 - ('category', models.JSONField()), 35 - ('code', models.JSONField()), 36 - ('permitted_data_type', models.JSONField()), 37 - ('body_site', models.JSONField()), 38 - ('method', models.JSONField()), 39 - ('permitted_unit', models.JSONField()), 40 - ('component', models.JSONField()), 41 - ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), 42 - ('facility', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='facility.facility')), 43 - ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), 44 - ], 45 - options={ 46 - 'abstract': False, 47 - }, 48 - ), 49 - ]
+105
care/emr/migrations/0030_observationdefinition_specimen_specimendefinition.py
··· 1 + # Generated by Django 5.1.4 on 2025-04-08 19:21 2 + 3 + import django.db.models.deletion 4 + import uuid 5 + from django.conf import settings 6 + from django.db import migrations, models 7 + 8 + 9 + class Migration(migrations.Migration): 10 + 11 + dependencies = [ 12 + ('emr', '0029_encounter_discharge_summary_advice'), 13 + ('facility', '0476_facility_default_internal_organization_and_more'), 14 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 + ] 16 + 17 + operations = [ 18 + migrations.CreateModel( 19 + name='ObservationDefinition', 20 + fields=[ 21 + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), 23 + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), 24 + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), 25 + ('deleted', models.BooleanField(db_index=True, default=False)), 26 + ('history', models.JSONField(default=dict)), 27 + ('meta', models.JSONField(default=dict)), 28 + ('version', models.IntegerField(default=1)), 29 + ('slug', models.CharField(max_length=255)), 30 + ('title', models.CharField(max_length=1024)), 31 + ('status', models.CharField(max_length=255)), 32 + ('description', models.TextField()), 33 + ('derived_from_uri', models.TextField()), 34 + ('category', models.JSONField()), 35 + ('code', models.JSONField()), 36 + ('permitted_data_type', models.JSONField()), 37 + ('body_site', models.JSONField()), 38 + ('method', models.JSONField()), 39 + ('permitted_unit', models.JSONField()), 40 + ('component', models.JSONField()), 41 + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), 42 + ('facility', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='facility.facility')), 43 + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), 44 + ], 45 + options={ 46 + 'abstract': False, 47 + }, 48 + ), 49 + migrations.CreateModel( 50 + name='Specimen', 51 + fields=[ 52 + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 53 + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), 54 + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), 55 + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), 56 + ('deleted', models.BooleanField(db_index=True, default=False)), 57 + ('history', models.JSONField(default=dict)), 58 + ('meta', models.JSONField(default=dict)), 59 + ('accession_identifier', models.JSONField(default=list)), 60 + ('status', models.CharField(max_length=20)), 61 + ('specimen_type', models.JSONField()), 62 + ('received_time', models.DateTimeField(blank=True, null=True)), 63 + ('note', models.TextField(blank=True, null=True)), 64 + ('condition', models.JSONField(default=list)), 65 + ('processing', models.JSONField(default=list)), 66 + ('collection', models.JSONField(default=dict)), 67 + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), 68 + ('encounter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.encounter')), 69 + ('facility', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='facility.facility')), 70 + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.patient')), 71 + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), 72 + ], 73 + options={ 74 + 'abstract': False, 75 + }, 76 + ), 77 + migrations.CreateModel( 78 + name='SpecimenDefinition', 79 + fields=[ 80 + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 81 + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), 82 + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), 83 + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), 84 + ('deleted', models.BooleanField(db_index=True, default=False)), 85 + ('history', models.JSONField(default=dict)), 86 + ('meta', models.JSONField(default=dict)), 87 + ('version', models.IntegerField(default=1)), 88 + ('slug', models.CharField(max_length=255)), 89 + ('title', models.CharField(max_length=1024)), 90 + ('derived_from_uri', models.TextField(blank=True, null=True)), 91 + ('status', models.CharField(max_length=255)), 92 + ('description', models.TextField()), 93 + ('type_collected', models.JSONField(blank=True, null=True)), 94 + ('patient_preparation', models.JSONField(default=list)), 95 + ('collection', models.JSONField(blank=True, null=True)), 96 + ('type_tested', models.JSONField(default=dict)), 97 + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), 98 + ('facility', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='facility.facility')), 99 + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), 100 + ], 101 + options={ 102 + 'abstract': False, 103 + }, 104 + ), 105 + ]
+2
care/emr/models/__init__.py
··· 11 11 from .device import * # noqa F403 12 12 from .meta_artifact import * # noqa F403 13 13 from .observation_definition import * # noqa F403 14 + from .specimen_definition import * # noqa F403 15 + from .specimen import * # noqa F403
+1 -1
care/emr/models/observation_definition.py
··· 13 13 ) 14 14 version = models.IntegerField(default=1) 15 15 slug = models.CharField(max_length=255) 16 - name = models.CharField(max_length=1024) 16 + title = models.CharField(max_length=1024) 17 17 status = models.CharField(max_length=255) 18 18 description = models.TextField() 19 19 derived_from_uri = models.TextField()
+26
care/emr/models/specimen.py
··· 1 + from django.db import models 2 + 3 + from care.emr.models import EMRBaseModel 4 + 5 + 6 + class Specimen(EMRBaseModel): 7 + facility = models.ForeignKey( 8 + "facility.Facility", 9 + on_delete=models.PROTECT, 10 + default=None, 11 + null=True, 12 + blank=True, 13 + ) 14 + 15 + accession_identifier = models.JSONField(default=list) 16 + status = models.CharField(max_length=20) 17 + specimen_type = models.JSONField() 18 + patient = models.ForeignKey("emr.Patient", on_delete=models.CASCADE) 19 + encounter = models.ForeignKey( 20 + "emr.Encounter", on_delete=models.CASCADE, null=True, blank=True 21 + ) 22 + received_time = models.DateTimeField(null=True, blank=True) 23 + note = models.TextField(null=True, blank=True) 24 + condition = models.JSONField(default=list) 25 + processing = models.JSONField(default=list) 26 + collection = models.JSONField(default=dict)
+23
care/emr/models/specimen_definition.py
··· 1 + from django.db import models 2 + 3 + from care.emr.models import EMRBaseModel 4 + 5 + 6 + class SpecimenDefinition(EMRBaseModel): 7 + facility = models.ForeignKey( 8 + "facility.Facility", 9 + on_delete=models.PROTECT, 10 + default=None, 11 + null=True, 12 + blank=True, 13 + ) 14 + version = models.IntegerField(default=1) 15 + slug = models.CharField(max_length=255) 16 + title = models.CharField(max_length=1024) 17 + derived_from_uri = models.TextField(null=True, blank=True) 18 + status = models.CharField(max_length=255) 19 + description = models.TextField() 20 + type_collected = models.JSONField(null=True, blank=True) 21 + patient_preparation = models.JSONField(default=list) 22 + collection = models.JSONField(null=True, blank=True) 23 + type_tested = models.JSONField(default=dict)
+1 -1
care/emr/resources/observation_definition/spec.py
··· 31 31 32 32 id: str 33 33 slug: str 34 - name: str 34 + title: str 35 35 status: str 36 36 description: str 37 37 category: Coding | None = None
care/emr/resources/specimen/__init__.py

This is a binary file and will not be displayed.

+130
care/emr/resources/specimen/spec.py
··· 1 + import datetime 2 + from enum import Enum 3 + 4 + from pydantic import UUID4, BaseModel, field_validator 5 + 6 + from care.emr.models.specimen import Specimen 7 + from care.emr.resources.base import EMRResource 8 + from care.emr.resources.common import Coding 9 + from care.emr.resources.observation.valueset import CARE_BODY_SITE_VALUESET 10 + from care.emr.resources.specimen.valueset import ( 11 + COLLECTION_METHOD_VALUESET, 12 + FASTING_STATUS_VALUESET, 13 + SPECIMEN_CONDITION_VALUESET, 14 + ) 15 + from care.emr.resources.specimen_definition.valueset import SPECIMEN_TYPE_CODE_VALUESET 16 + from care.emr.utils.valueset_coding_type import ValueSetBoundCoding 17 + from care.facility.models import Facility 18 + from care.users.models import User 19 + 20 + 21 + class SpecimenStatusOptions(str, Enum): 22 + """Status options for specimen""" 23 + 24 + available = "available" 25 + unavailable = "unavailable" 26 + unsatisfactory = "unsatisfactory" 27 + entered_in_error = "entered_in_error" 28 + 29 + 30 + class QuantitySpec(BaseModel): 31 + """Represents a quantity with value and unit""" 32 + 33 + value: float 34 + unit: Coding 35 + 36 + 37 + class DurationSpec(BaseModel): 38 + """Duration specification using value and unit""" 39 + 40 + # Needs to be moved into the common specs, with datetime based valueset check 41 + value: int 42 + unit: Coding 43 + 44 + 45 + class CollectionSpec(BaseModel): 46 + """Specimen collection details""" 47 + 48 + collector: UUID4 | None = None 49 + collected_date_time: datetime.datetime | None = None # Check for TZ 50 + quantity: QuantitySpec | None = None 51 + method: ValueSetBoundCoding[COLLECTION_METHOD_VALUESET.slug] | None = None 52 + procedure: UUID4 | None = None 53 + body_site: ValueSetBoundCoding[CARE_BODY_SITE_VALUESET.slug] | None = None 54 + fasting_status_codeable_concept: ( 55 + ValueSetBoundCoding[FASTING_STATUS_VALUESET.slug] | None 56 + ) = None 57 + fasting_status_duration: DurationSpec | None = None 58 + 59 + @field_validator("collector") 60 + @classmethod 61 + def validate_collector(cls, collector): 62 + if collector and not User.objects.filter(external_id=collector).exists(): 63 + raise ValueError("Collector user not found") 64 + return collector 65 + 66 + 67 + class ProcessingSpec(BaseModel): 68 + """Specimen processing details""" 69 + 70 + description: str 71 + method: Coding | None = None 72 + performer: UUID4 | None = None 73 + time_date_time: str 74 + 75 + @field_validator("performer") 76 + @classmethod 77 + def validate_performer(cls, performer): 78 + if performer and not User.objects.filter(external_id=performer).exists(): 79 + raise ValueError("Performer user not found") 80 + return performer 81 + 82 + 83 + class BaseSpecimenSpec(EMRResource): 84 + """Base model for specimen""" 85 + 86 + __model__ = Specimen 87 + __exclude__ = ["facility", "subject", "request", "collection", "processing"] 88 + 89 + id: str | None = None 90 + accession_identifier: list[str] = [] 91 + status: SpecimenStatusOptions 92 + specimen_type: ValueSetBoundCoding[SPECIMEN_TYPE_CODE_VALUESET.slug] 93 + subject: dict | None = None 94 + received_time: str | None = None 95 + request: dict | None = None 96 + collection: CollectionSpec 97 + processing: list[ProcessingSpec] = [] 98 + condition: list[ValueSetBoundCoding[SPECIMEN_CONDITION_VALUESET.slug]] = [] 99 + note: str | None = None 100 + 101 + 102 + class SpecimenCreateSpec(BaseSpecimenSpec): 103 + """Specimen creation specification""" 104 + 105 + facility: UUID4 | None = None 106 + subject_patient: UUID4 107 + subject_encounter: UUID4 | None = None 108 + request: UUID4 | None = None 109 + collection: CollectionSpec 110 + processing: list[ProcessingSpec] = [] 111 + 112 + @field_validator("facility") 113 + @classmethod 114 + def validate_facility_exists(cls, facility): 115 + if not Facility.objects.filter(external_id=facility).exists(): 116 + # Check if user is in the given facility 117 + raise ValueError("Facility not found") 118 + return facility 119 + 120 + def perform_extra_deserialization(self, is_update, obj): 121 + if self.facility: 122 + obj.facility = Facility.objects.get(external_id=self.facility) 123 + 124 + 125 + class SpecimenReadSpec(BaseSpecimenSpec): 126 + """Specimen read specification""" 127 + 128 + @classmethod 129 + def perform_extra_serialization(cls, mapping, obj): 130 + mapping["id"] = obj.external_id
+86
care/emr/resources/specimen/valueset.py
··· 1 + from care.emr.registries.care_valueset.care_valueset import CareValueset 2 + from care.emr.resources.common.valueset import ValueSetCompose, ValueSetInclude 3 + from care.emr.resources.valueset.spec import ValueSetStatusOptions 4 + 5 + # Collection method valueset (SNOMED) 6 + COLLECTION_METHOD_VALUESET = CareValueset( 7 + "Collection Method", 8 + "system-collection-method-code", 9 + ValueSetStatusOptions.active.value, 10 + ) 11 + 12 + COLLECTION_METHOD_VALUESET.register_valueset( 13 + ValueSetCompose( 14 + include=[ 15 + ValueSetInclude( 16 + system="http://snomed.info/sct", 17 + concept=[ 18 + { 19 + "code": "129316008", 20 + "display": "Aspiration - action", 21 + }, 22 + {"code": "129314006", "display": "Biopsy - action"}, 23 + {"code": "129300006", "display": "Puncture - action"}, 24 + {"code": "129304002", "display": "Excision - action"}, 25 + {"code": "129323009", "display": "Scraping - action"}, 26 + { 27 + "code": "73416001", 28 + "display": "Urine specimen collection, clean catch", 29 + }, 30 + {"code": "225113003", "display": "Timed urine collection"}, 31 + { 32 + "code": "70777001", 33 + "display": "Urine specimen collection, catheterized", 34 + }, 35 + {"code": "386089008", "display": "Collection of coughed sputum"}, 36 + { 37 + "code": "278450005", 38 + "display": "Finger-prick sampling", 39 + }, 40 + ], 41 + ) 42 + ] 43 + ) 44 + ) 45 + 46 + COLLECTION_METHOD_VALUESET.register_as_system() 47 + 48 + # Fasting status valueset 49 + FASTING_STATUS_VALUESET = CareValueset( 50 + "Fasting Status", 51 + "system-fasting-status-code", 52 + ValueSetStatusOptions.active.value, 53 + ) 54 + 55 + FASTING_STATUS_VALUESET.register_valueset( 56 + ValueSetCompose( 57 + include=[ 58 + ValueSetInclude( 59 + system="http://terminology.hl7.org/CodeSystem/v2-0916", 60 + version="2.0.0", 61 + ) 62 + ] 63 + ) 64 + ) 65 + 66 + FASTING_STATUS_VALUESET.register_as_system() 67 + 68 + # Specimen condition valueset 69 + SPECIMEN_CONDITION_VALUESET = CareValueset( 70 + "Specimen Condition", 71 + "system-specimen-condition-code", 72 + ValueSetStatusOptions.active.value, 73 + ) 74 + 75 + SPECIMEN_CONDITION_VALUESET.register_valueset( 76 + ValueSetCompose( 77 + include=[ 78 + ValueSetInclude( 79 + system="http://terminology.hl7.org/CodeSystem/v2-0493", 80 + version="2.0.0", 81 + ) 82 + ] 83 + ) 84 + ) 85 + 86 + SPECIMEN_CONDITION_VALUESET.register_as_system()
care/emr/resources/specimen_definition/__init__.py

This is a binary file and will not be displayed.

+112
care/emr/resources/specimen_definition/spec.py
··· 1 + from enum import Enum 2 + 3 + from pydantic import BaseModel, model_validator 4 + 5 + from care.emr.models.specimen_definition import SpecimenDefinition 6 + from care.emr.resources.base import EMRResource 7 + from care.emr.resources.common import Coding 8 + from care.emr.resources.specimen_definition.valueset import ( 9 + CONTAINER_CAP_VALUESET, 10 + PREPARE_PATIENT_PRIOR_SPECIMEN_CODE_VALUESET, 11 + SPECIMEN_COLLECTION_CODE_VALUESET, 12 + SPECIMEN_TYPE_CODE_VALUESET, 13 + ) 14 + from care.emr.utils.valueset_coding_type import ValueSetBoundCoding 15 + 16 + 17 + class SpecimenDefinitionStatusOptions(str, Enum): 18 + """Status options for specimen definition""" 19 + 20 + draft = "draft" 21 + active = "active" 22 + retired = "retired" 23 + unknown = "unknown" 24 + 25 + 26 + class PreferenceOptions(str, Enum): 27 + """Preference options for specimen type testing""" 28 + 29 + preferred = "preferred" 30 + alternate = "alternate" 31 + 32 + 33 + class QuantitySpec(BaseModel): 34 + """Represents a quantity with value and unit""" 35 + 36 + value: float 37 + unit: Coding 38 + 39 + 40 + class MinimumVolumeSpec(BaseModel): 41 + """Specification for minimum volume with support for quantity or string representation""" 42 + 43 + quantity: QuantitySpec | None = None 44 + string: str | None = None 45 + 46 + @model_validator(mode="after") 47 + def validate_minimum_volume(self): 48 + """Validates that only one minimum volume field is provided""" 49 + if self.quantity and self.string: 50 + raise ValueError("Only one of quantity or string should be provided") 51 + return self 52 + 53 + 54 + class ContainerSpec(BaseModel): 55 + """Container specification for specimen collection""" 56 + 57 + description: str | None = None 58 + capacity: QuantitySpec | None = None 59 + minimum_volume: MinimumVolumeSpec | None = None 60 + cap: ValueSetBoundCoding[CONTAINER_CAP_VALUESET.slug] | None = None 61 + preparation: str | None = None 62 + 63 + 64 + class DurationSpec(BaseModel): 65 + """Duration specification using value and unit""" 66 + 67 + value: int 68 + unit: Coding # Nees to be restricted to Datetime Units 69 + 70 + 71 + class TypeTestedSpec(BaseModel): 72 + """Specification for tested specimen types""" 73 + 74 + is_derived: bool 75 + specimen_type: ValueSetBoundCoding[SPECIMEN_TYPE_CODE_VALUESET.slug] 76 + preference: PreferenceOptions 77 + container: ContainerSpec | None = None 78 + requirement: str | None = None 79 + retention_time: DurationSpec | None = None 80 + single_use: bool | None = None 81 + 82 + 83 + class BaseSpecimenDefinitionSpec(EMRResource): 84 + """Base model for specimen definition""" 85 + 86 + __model__ = SpecimenDefinition 87 + __exclude__ = ["facility"] 88 + 89 + id: str | None = None 90 + slug: str 91 + title: str 92 + derived_from_uri: str | None = None 93 + status: SpecimenDefinitionStatusOptions 94 + description: str 95 + type_collected: ValueSetBoundCoding[SPECIMEN_TYPE_CODE_VALUESET.slug] | None = None 96 + patient_preparation: list[ 97 + ValueSetBoundCoding[PREPARE_PATIENT_PRIOR_SPECIMEN_CODE_VALUESET.slug] 98 + ] = [] 99 + collection: ValueSetBoundCoding[SPECIMEN_COLLECTION_CODE_VALUESET.slug] | None = ( 100 + None 101 + ) 102 + type_tested: TypeTestedSpec | None = None 103 + 104 + 105 + class SpecimenDefinitionReadSpec(BaseSpecimenDefinitionSpec): 106 + """Specimen definition read specification""" 107 + 108 + version: int | None = None 109 + 110 + @classmethod 111 + def perform_extra_serialization(cls, mapping, obj): 112 + mapping["id"] = obj.external_id
+107
care/emr/resources/specimen_definition/valueset.py
··· 1 + from care.emr.registries.care_valueset.care_valueset import CareValueset 2 + from care.emr.resources.common.valueset import ValueSetCompose, ValueSetInclude 3 + from care.emr.resources.valueset.spec import ValueSetStatusOptions 4 + 5 + SPECIMEN_TYPE_CODE_VALUESET = CareValueset( 6 + "Specimen Type Code", 7 + "system-specimen_type-code", 8 + ValueSetStatusOptions.active.value, 9 + ) 10 + 11 + SPECIMEN_TYPE_CODE_VALUESET.register_valueset( 12 + ValueSetCompose( 13 + include=[ 14 + ValueSetInclude( 15 + system="http://terminology.hl7.org/CodeSystem/v2-0487", 16 + version="2.0.0", 17 + ) 18 + ] 19 + ) 20 + ) 21 + 22 + SPECIMEN_TYPE_CODE_VALUESET.register_as_system() 23 + 24 + 25 + PREPARE_PATIENT_PRIOR_SPECIMEN_CODE_VALUESET = CareValueset( 26 + "Prepare Patient Prior Specimen Code", 27 + "system-prepare_patient_prior_specimen_code", 28 + ValueSetStatusOptions.active.value, 29 + ) 30 + 31 + PREPARE_PATIENT_PRIOR_SPECIMEN_CODE_VALUESET.register_valueset( 32 + ValueSetCompose( 33 + include=[ 34 + ValueSetInclude( 35 + system="http://snomed.info/sct", 36 + filter=[ 37 + { 38 + "property": "concept", 39 + "op": "is-a", 40 + "value": "703763000", 41 + } 42 + ], 43 + ) 44 + ] 45 + ) 46 + ) 47 + 48 + PREPARE_PATIENT_PRIOR_SPECIMEN_CODE_VALUESET.register_as_system() 49 + 50 + 51 + SPECIMEN_COLLECTION_CODE_VALUESET = CareValueset( 52 + "Specimen Collection Code", 53 + "system-specimen_collection_code", 54 + ValueSetStatusOptions.active.value, 55 + ) 56 + 57 + SPECIMEN_COLLECTION_CODE_VALUESET.register_valueset( 58 + ValueSetCompose( 59 + include=[ 60 + ValueSetInclude( 61 + system="http://snomed.info/sct", 62 + concept=[ 63 + { 64 + "code": "129316008", 65 + "display": "Aspiration - action", 66 + }, 67 + {"code": "129314006", "display": "Biopsy - action"}, 68 + {"code": "129300006", "display": "Puncture - action"}, 69 + {"code": "129304002", "display": "Excision - action"}, 70 + {"code": "129323009", "display": "Scraping - action"}, 71 + { 72 + "code": "73416001", 73 + "display": "Urine specimen collection, clean catch", 74 + }, 75 + {"code": "225113003", "display": "Timed urine collection"}, 76 + { 77 + "code": "70777001", 78 + "display": "Urine specimen collection, catheterized", 79 + }, 80 + {"code": "386089008", "display": "Collection of coughed sputum"}, 81 + {"code": "278450005", "display": "Finger-prick sampling"}, 82 + ], 83 + ) 84 + ] 85 + ) 86 + ) 87 + 88 + SPECIMEN_COLLECTION_CODE_VALUESET.register_as_system() 89 + 90 + 91 + CONTAINER_CAP_VALUESET = CareValueset( 92 + "Container Cap", 93 + "system-container_cap-code", 94 + ValueSetStatusOptions.active.value, 95 + ) 96 + 97 + CONTAINER_CAP_VALUESET.register_valueset( 98 + ValueSetCompose( 99 + include=[ 100 + ValueSetInclude( 101 + system="http://terminology.hl7.org/CodeSystem/container-cap" 102 + ) 103 + ] 104 + ) 105 + ) 106 + 107 + CONTAINER_CAP_VALUESET.register_as_system()
+26
care/security/authorization/specimen_definition.py
··· 1 + from care.security.authorization.base import ( 2 + AuthorizationHandler, 3 + ) 4 + from care.security.permissions.specimen_definition import SpecimenDefinitionPermissions 5 + 6 + 7 + class SpecimenDefinitionAccess(AuthorizationHandler): 8 + def can_list_facility_specimen_definition(self, user, facility): 9 + """ 10 + Check if the user has permission to view observation definitions in the facility 11 + """ 12 + return self.check_permission_in_facility_organization( 13 + [SpecimenDefinitionPermissions.can_read_specimen_definition.name], 14 + user, 15 + facility=facility, 16 + ) 17 + 18 + def can_write_facility_specimen_definition(self, user, facility): 19 + """ 20 + Check if the user has permission to view observation definitions in the facility 21 + """ 22 + return self.check_permission_in_facility_organization( 23 + [SpecimenDefinitionPermissions.can_write_specimen_definition.name], 24 + user, 25 + facility=facility, 26 + )
+3 -1
care/security/permissions/base.py
··· 11 11 from care.security.permissions.organization import OrganizationPermissions 12 12 from care.security.permissions.patient import PatientPermissions 13 13 from care.security.permissions.questionnaire import QuestionnairePermissions 14 + from care.security.permissions.specimen_definition import SpecimenDefinitionPermissions 14 15 from care.security.permissions.user import UserPermissions 15 16 from care.security.permissions.user_schedule import UserSchedulePermissions 16 17 ··· 39 40 UserSchedulePermissions, 40 41 FacilityLocationPermissions, 41 42 ObservationDefinitionPermissions, 42 - DevicePermissions 43 + DevicePermissions, 44 + SpecimenDefinitionPermissions, 43 45 ] 44 46 45 47 cache = {}
+2 -2
care/security/permissions/observation_definition.py
··· 5 5 ADMIN_ROLE, 6 6 DOCTOR_ROLE, 7 7 FACILITY_ADMIN_ROLE, 8 - GEO_ADMIN, 8 + ADMINISTRATOR, 9 9 NURSE_ROLE, 10 10 STAFF_ROLE, 11 11 VOLUNTEER_ROLE, ··· 25 25 PermissionContext.FACILITY, 26 26 [ 27 27 FACILITY_ADMIN_ROLE, 28 - GEO_ADMIN, 28 + ADMINISTRATOR, 29 29 ADMIN_ROLE, 30 30 STAFF_ROLE, 31 31 DOCTOR_ROLE,
+35
care/security/permissions/specimen_definition.py
··· 1 + import enum 2 + 3 + from care.security.permissions.constants import Permission, PermissionContext 4 + from care.security.roles.role import ( 5 + ADMIN_ROLE, 6 + DOCTOR_ROLE, 7 + FACILITY_ADMIN_ROLE, 8 + ADMINISTRATOR, 9 + NURSE_ROLE, 10 + STAFF_ROLE, 11 + VOLUNTEER_ROLE, 12 + ) 13 + 14 + 15 + class SpecimenDefinitionPermissions(enum.Enum): 16 + can_write_specimen_definition = Permission( 17 + "Can Create Specimen Definition on Facility", 18 + "", 19 + PermissionContext.FACILITY, 20 + [FACILITY_ADMIN_ROLE, ADMIN_ROLE], 21 + ) 22 + can_read_specimen_definition = Permission( 23 + "Can Read Specimen Definition", 24 + "", 25 + PermissionContext.FACILITY, 26 + [ 27 + FACILITY_ADMIN_ROLE, 28 + ADMINISTRATOR, 29 + ADMIN_ROLE, 30 + STAFF_ROLE, 31 + DOCTOR_ROLE, 32 + NURSE_ROLE, 33 + VOLUNTEER_ROLE, 34 + ], 35 + )
+12
config/api_router.py
··· 44 44 from care.emr.api.viewsets.mfa_login import MFALoginViewSet 45 45 from care.emr.api.viewsets.notes import NoteMessageViewSet, NoteThreadViewSet 46 46 from care.emr.api.viewsets.observation import ObservationViewSet 47 + from care.emr.api.viewsets.observation_definition import ObservationDefinitionViewSet 47 48 from care.emr.api.viewsets.organization import ( 48 49 OrganizationPublicViewSet, 49 50 OrganizationUsersViewSet, ··· 69 70 AvailabilityExceptionsViewSet, 70 71 ) 71 72 from care.emr.api.viewsets.scheduling.booking import TokenBookingViewSet 73 + from care.emr.api.viewsets.specimen_definition import SpecimenDefinitionViewSet 72 74 from care.emr.api.viewsets.totp import TOTPViewSet 73 75 from care.emr.api.viewsets.user import UserViewSet 74 76 from care.emr.api.viewsets.valueset import ValueSetViewSet ··· 99 101 router.register( 100 102 "questionnaire_tag", QuestionnaireTagsViewSet, basename="questionnaire_tags" 101 103 ) 104 + 105 + router.register( 106 + "observation_definition", ObservationDefinitionViewSet, basename="observation_definition" 107 + ) 108 + 102 109 103 110 router.register("organization", OrganizationViewSet, basename="organization") 104 111 ··· 186 193 basename="device", 187 194 ) 188 195 196 + facility_nested_router.register( 197 + r"specimen_definition", 198 + SpecimenDefinitionViewSet, 199 + basename="specimen_definition", 200 + ) 189 201 device_nested_router = NestedSimpleRouter( 190 202 facility_nested_router, r"device", lookup="device" 191 203 )