From 9e03d628f6bc657e02b2d266fea8f5dbf215a3a8 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Tue, 22 Mar 2022 17:00:50 +0530 Subject: [PATCH] Allow Middleware to view users --- care/facility/admin.py | 14 ++- care/facility/api/viewsets/patient.py | 19 ++- .../models/mixins/permissions/patient.py | 12 +- care/facility/models/patient.py | 112 ++++++++++++++---- care/users/migrations/0041_user_asset.py | 20 ++++ care/users/models.py | 6 + config/authentication.py | 68 +++++++++-- config/health_views.py | 3 +- config/views.py | 4 - 9 files changed, 206 insertions(+), 52 deletions(-) create mode 100644 care/users/migrations/0041_user_asset.py diff --git a/care/facility/admin.py b/care/facility/admin.py index a04cf46132..8124ff7365 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -4,6 +4,7 @@ from djqscsv import render_to_csv_response from care.facility.models.ambulance import Ambulance, AmbulanceDriver +from care.facility.models.bed import AssetBed from care.facility.models.patient_sample import PatientSample from care.facility.models.patient_tele_consultation import PatientTeleConsultation @@ -22,12 +23,12 @@ Inventory, InventoryItem, InventoryLog, + PatientExternalTest, + PatientInvestigation, + PatientInvestigationGroup, PatientRegistration, Room, StaffRoomAllocation, - PatientExternalTest, - PatientInvestigationGroup, - PatientInvestigation, ) @@ -37,7 +38,7 @@ class BuildingAdmin(admin.ModelAdmin): class DistrictFilter(SimpleListFilter): - """DistrictFilter """ + """DistrictFilter""" title = "District" parameter_name = "district" @@ -170,7 +171,9 @@ def export_as_csv(self, request, queryset): queryset = FacilityUser.objects.all().values(*FacilityUser.CSV_MAPPING.keys()) return render_to_csv_response( - queryset, field_header_map=FacilityUser.CSV_MAPPING, field_serializer_map=FacilityUser.CSV_MAKE_PRETTY, + queryset, + field_header_map=FacilityUser.CSV_MAPPING, + field_serializer_map=FacilityUser.CSV_MAKE_PRETTY, ) export_as_csv.short_description = "Export Selected" @@ -205,3 +208,4 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin): admin.site.register(PatientExternalTest, PatientExternalTestAdmin) admin.site.register(PatientInvestigation, PatientTestAdmin) admin.site.register(PatientInvestigationGroup, PatientTestGroupAdmin) +admin.site.register(AssetBed) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 7497c10d92..e49199ec0f 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -44,12 +44,14 @@ ShiftingRequest, ) from care.facility.models.base import covert_choice_dict +from care.facility.models.bed import AssetBed from care.facility.models.patient_base import DISEASE_STATUS_DICT from care.facility.tasks.patient.discharge_report import generate_discharge_report from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters import CareChoiceFilter, MultiSelectFilter from care.utils.queryset.patient import get_patient_queryset +from config.authentication import CustomBasicAuthentication, CustomJWTAuthentication, MiddlewareAuthentication REVERSE_FACILITY_TYPES = covert_choice_dict(FACILITY_TYPES) @@ -111,7 +113,13 @@ class PatientDRYFilter(DRYPermissionFiltersBase): def filter_queryset(self, request, queryset, view): if view.action == "list": queryset = self.filter_list_queryset(request, queryset, view) - + if request.user.asset: + return queryset.filter( + last_consultation__last_daily_round__bed_id__in=AssetBed.objects.filter( + asset=request.user.asset + ).values("id"), + last_consultation__last_daily_round__bed__isnull=False, + ) if not request.user.is_superuser: if request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: queryset = queryset.filter(facility__state=request.user.state) @@ -145,6 +153,8 @@ class PatientViewSet( mixins.UpdateModelMixin, GenericViewSet, ): + + authentication_classes = [CustomBasicAuthentication, CustomJWTAuthentication, MiddlewareAuthentication] permission_classes = (IsAuthenticated, DRYPermissions) lookup_field = "external_id" queryset = PatientRegistration.objects.all().select_related( @@ -242,12 +252,9 @@ def list(self, request, *args, **kwargs): slice_obj = temp.form.cleaned_data.get(field) if slice_obj: if not slice_obj.start or not slice_obj.stop: - raise ValidationError( - {field: f"both starting and ending date must be provided for export"} - ) + raise ValidationError({field: f"both starting and ending date must be provided for export"}) days_difference = ( - temp.form.cleaned_data.get(field).stop - - temp.form.cleaned_data.get(field).start + temp.form.cleaned_data.get(field).stop - temp.form.cleaned_data.get(field).start ).days if days_difference <= self.CSV_EXPORT_LIMIT: within_limits = True diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index 9ce1262780..10e5977018 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -5,6 +5,8 @@ class PatientPermissionMixin(BasePermissionMixin): @staticmethod def has_write_permission(request): + if request.user.asset: + return False if ( request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] @@ -38,6 +40,8 @@ def has_object_read_permission(self, request): ) def has_object_write_permission(self, request): + if request.user.asset: + return False doctor_allowed = False if self.last_consultation: doctor_allowed = self.last_consultation.assigned_to == request.user or request.user == self.assigned_to @@ -65,6 +69,8 @@ def has_object_write_permission(self, request): ) def has_object_update_permission(self, request): + if request.user.asset: + return False doctor_allowed = False if self.last_consultation: doctor_allowed = self.last_consultation.assigned_to == request.user or request.user == self.assigned_to @@ -96,6 +102,8 @@ def has_object_icmr_sample_permission(self, request): return self.has_object_read_permission(request) def has_object_transfer_permission(self, request): + if request.user.asset: + return False if ( request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] @@ -103,9 +111,7 @@ def has_object_transfer_permission(self, request): ): return False new_facility = Facility.objects.filter(id=request.data.get("facility", None)).first() - return self.has_object_update_permission(request) or ( - new_facility and request.user in new_facility.users.all() - ) + return self.has_object_update_permission(request) or (new_facility and request.user in new_facility.users.all()) class PatientRelatedPermissionMixin(BasePermissionMixin): diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index cd2ab25d22..dcf313ae89 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -89,7 +89,10 @@ class TestTypeEnum(enum.Enum): source = models.IntegerField(choices=SourceChoices, default=SourceEnum.CARE.value) facility = models.ForeignKey("Facility", on_delete=models.SET_NULL, null=True) nearest_facility = models.ForeignKey( - "Facility", on_delete=models.SET_NULL, null=True, related_name="nearest_facility", + "Facility", + on_delete=models.SET_NULL, + null=True, + related_name="nearest_facility", ) meta_info = models.OneToOneField("PatientMetaInfo", on_delete=models.SET_NULL, null=True) @@ -120,7 +123,11 @@ class TestTypeEnum(enum.Enum): is_medical_worker = models.BooleanField(default=False, verbose_name="Is the Patient a Medical Worker") blood_group = models.CharField( - choices=BLOOD_GROUP_CHOICES, null=True, blank=False, max_length=4, verbose_name="Blood Group of Patient", + choices=BLOOD_GROUP_CHOICES, + null=True, + blank=False, + max_length=4, + verbose_name="Blood Group of Patient", ) contact_with_confirmed_carrier = models.BooleanField( @@ -132,14 +139,20 @@ class TestTypeEnum(enum.Enum): estimated_contact_date = models.DateTimeField(null=True, blank=True) past_travel = models.BooleanField( - default=False, verbose_name="Travelled to Any Foreign Countries in the last 28 Days", + default=False, + verbose_name="Travelled to Any Foreign Countries in the last 28 Days", ) countries_travelled_old = models.TextField( - null=True, blank=True, verbose_name="Countries Patient has Travelled to", editable=False, + null=True, + blank=True, + verbose_name="Countries Patient has Travelled to", + editable=False, ) countries_travelled = JSONField(null=True, blank=True, verbose_name="Countries Patient has Travelled to") date_of_return = models.DateTimeField( - blank=True, null=True, verbose_name="Return Date from the Last Country if Travelled", + blank=True, + null=True, + verbose_name="Return Date from the Last Country if Travelled", ) allergies = models.TextField(default="", blank=True, verbose_name="Patient's Known Allergies") @@ -157,27 +170,43 @@ class TestTypeEnum(enum.Enum): district = models.ForeignKey(District, on_delete=models.SET_NULL, null=True, blank=True) state = models.ForeignKey(State, on_delete=models.SET_NULL, null=True, blank=True) - is_migrant_worker = models.BooleanField(default=False, verbose_name="Is Patient a Migrant Worker",) + is_migrant_worker = models.BooleanField( + default=False, + verbose_name="Is Patient a Migrant Worker", + ) disease_status = models.IntegerField( - choices=DISEASE_STATUS_CHOICES, default=1, blank=True, verbose_name="Disease Status", + choices=DISEASE_STATUS_CHOICES, + default=1, + blank=True, + verbose_name="Disease Status", ) number_of_aged_dependents = models.IntegerField( - default=0, verbose_name="Number of people aged above 60 living with the patient", blank=True, + default=0, + verbose_name="Number of people aged above 60 living with the patient", + blank=True, ) number_of_chronic_diseased_dependents = models.IntegerField( - default=0, verbose_name="Number of people who have chronic diseases living with the patient", blank=True, + default=0, + verbose_name="Number of people who have chronic diseases living with the patient", + blank=True, ) - last_edited = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="patient_last_edited_by",) + last_edited = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name="patient_last_edited_by", + ) action = models.IntegerField(choices=ActionChoices, default=ActionEnum.PENDING.value) review_time = models.DateTimeField(null=True, blank=True, verbose_name="Patient's next review time") created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="patient_created_by") is_active = models.BooleanField( - default=True, help_text="Not active when discharged, or removed from the watchlist", + default=True, + help_text="Not active when discharged, or removed from the watchlist", ) patient_search_id = EncryptedIntegerField(help_text="FKey to PatientSearch", null=True) @@ -197,16 +226,24 @@ class TestTypeEnum(enum.Enum): last_consultation = models.ForeignKey(PatientConsultation, on_delete=models.SET_NULL, null=True, default=None) will_donate_blood = models.BooleanField( - default=None, null=True, verbose_name="Is Patient Willing to donate Blood", + default=None, + null=True, + verbose_name="Is Patient Willing to donate Blood", ) fit_for_blood_donation = models.BooleanField( - default=None, null=True, verbose_name="Is Patient fit for donating Blood", + default=None, + null=True, + verbose_name="Is Patient fit for donating Blood", ) # IDSP REQUIREMENTS village = models.CharField( - max_length=255, default=None, verbose_name="Vilalge Name of Patient (IDSP Req)", null=True, blank=True, + max_length=255, + default=None, + verbose_name="Vilalge Name of Patient (IDSP Req)", + null=True, + blank=True, ) designation_of_health_care_worker = models.CharField( max_length=255, @@ -216,13 +253,25 @@ class TestTypeEnum(enum.Enum): blank=True, ) instituion_of_health_care_worker = models.CharField( - max_length=255, default=None, verbose_name="Institution of Healtcare Worker (IDSP Req)", null=True, blank=True, + max_length=255, + default=None, + verbose_name="Institution of Healtcare Worker (IDSP Req)", + null=True, + blank=True, ) transit_details = models.CharField( - max_length=255, default=None, verbose_name="Transit Details (IDSP Req)", null=True, blank=True, + max_length=255, + default=None, + verbose_name="Transit Details (IDSP Req)", + null=True, + blank=True, ) frontline_worker = models.CharField( - max_length=255, default=None, verbose_name="Front Line Worker (IDSP Req)", null=True, blank=True, + max_length=255, + default=None, + verbose_name="Front Line Worker (IDSP Req)", + null=True, + blank=True, ) date_of_result = models.DateTimeField(null=True, blank=True, default=None, verbose_name="Patient's result Date") number_of_primary_contacts = models.IntegerField( @@ -241,15 +290,27 @@ class TestTypeEnum(enum.Enum): vaccine_name = models.CharField(choices=vaccineChoices, default=None, null=True, blank=False, max_length=15) covin_id = models.CharField( - max_length=15, default=None, null=True, blank=True, verbose_name="COVID-19 Vaccination ID", + max_length=15, + default=None, + null=True, + blank=True, + verbose_name="COVID-19 Vaccination ID", ) last_vaccinated_date = models.DateTimeField(null=True, blank=True, verbose_name="Date Last Vaccinated") # Extras cluster_name = models.CharField( - max_length=255, default=None, verbose_name="Name/ Cluster of Contact", null=True, blank=True, + max_length=255, + default=None, + verbose_name="Name/ Cluster of Contact", + null=True, + blank=True, + ) + is_declared_positive = models.BooleanField( + default=None, + null=True, + verbose_name="Is Patient Declared Positive", ) - is_declared_positive = models.BooleanField(default=None, null=True, verbose_name="Is Patient Declared Positive",) date_declared_positive = models.DateTimeField( null=True, blank=True, verbose_name="Date Patient is Declared Positive" ) @@ -525,7 +586,10 @@ class ModeOfContactEnum(enum.IntEnum): patient = models.ForeignKey(PatientRegistration, on_delete=models.PROTECT, related_name="contacted_patients") patient_in_contact = models.ForeignKey( - PatientRegistration, on_delete=models.PROTECT, null=True, related_name="contacts", + PatientRegistration, + on_delete=models.PROTECT, + null=True, + related_name="contacts", ) relation_with_patient = models.IntegerField(choices=RelationChoices) mode_of_contact = models.IntegerField(choices=ModeOfContactChoices) @@ -590,5 +654,9 @@ class PatientMobileOTP(BaseModel): class PatientNotes(FacilityBaseModel): patient = models.ForeignKey(PatientRegistration, on_delete=models.PROTECT, null=False, blank=False) facility = models.ForeignKey(Facility, on_delete=models.PROTECT, null=False, blank=False) - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True,) + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + ) note = models.TextField(default="", blank=True) diff --git a/care/users/migrations/0041_user_asset.py b/care/users/migrations/0041_user_asset.py new file mode 100644 index 0000000000..11b2e94ead --- /dev/null +++ b/care/users/migrations/0041_user_asset.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.11 on 2022-03-22 09:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0285_asset_asset_class'), + ('users', '0040_auto_20210616_1821'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='asset', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='facility.Asset', unique=True), + ), + ] diff --git a/care/users/models.py b/care/users/models.py index a2f7a2e95f..ed77837774 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -209,6 +209,12 @@ class User(AbstractUser): pf_p256dh = models.TextField(default=None, null=True) pf_auth = models.TextField(default=None, null=True) + # Asset Fields + + asset = models.ForeignKey( + "facility.Asset", default=None, null=True, blank=True, on_delete=models.PROTECT, unique=True + ) + objects = CustomUserManager() REQUIRED_FIELDS = [ diff --git a/config/authentication.py b/config/authentication.py index 1704d245aa..9b8aad70c4 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -8,6 +8,8 @@ from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from care.facility.models import Facility +from care.facility.models.asset import Asset +from care.users.models import User class CustomJWTAuthentication(JWTAuthentication): @@ -44,18 +46,15 @@ def authenticate(self, request): return None if self.FACILITY_HEADER not in request.headers: - print("Yeah") return None - external_id = request.headers[self.FACILITY_HEADER] try: UUID(external_id) - except ValueError: - raise InvalidToken({"detail": "Invalid Facility", "messages": []}) + except ValueError as e: + raise InvalidToken({"detail": "Invalid Facility", "messages": []}) from e facility = Facility.objects.filter(external_id=external_id).first() - if not facility: raise InvalidToken({"detail": "Invalid Facility", "messages": []}) @@ -64,7 +63,32 @@ def authenticate(self, request): validated_token = self.get_validated_token(open_id_url, raw_token) - return self.get_user(validated_token), validated_token + return self.get_user(validated_token, facility), validated_token + + def get_raw_token(self, header): + """ + Extracts an unvalidated JSON web token from the given "Authorization" + header value. + """ + parts = header.split() + + if len(parts) == 0: + # Empty AUTHORIZATION header sent + return None + if parts[0] not in (b"Middleware_Bearer",): + + # Assume the header does not contain a JSON web token + return None + + if len(parts) != 2: + raise InvalidToken( + { + "detail": "Given token not valid for any token type", + "messages": [], + } + ) + + return parts[1] def get_validated_token(self, url, raw_token): """ @@ -83,17 +107,39 @@ def get_validated_token(self, url, raw_token): } ) - def get_user(self, validated_token): + def get_user(self, validated_token, facility): """ Attempts to find and return a user using the given validated token. """ if "asset_id" not in validated_token: raise TokenError() asset_external_id = validated_token["asset_id"] - # TODO Check Asset Facility Relation here! try: UUID(asset_external_id) - except ValueError: - raise InvalidToken({"detail": "Invalid Facility", "messages": []}) + except ValueError as e: + raise InvalidToken({"detail": "Invalid Facility", "messages": []}) from e # Create/Retrieve User and return them - return None + asset_obj = Asset.objects.filter(external_id=asset_external_id).first() + + if not asset_obj: + raise InvalidToken({"detail": "Asset object not valid"}) + + if asset_obj.current_location.facility != facility: + raise InvalidToken({"detail": "Facility not connected to Asset"}) + + asset_user = User.objects.filter(asset=asset_obj).first() + if not asset_user: + password = User.objects.make_random_password() + asset_user = User( + username="asset" + str(asset_obj.external_id), + email="support@coronasafe.network", + password=password + "123", + gender=3, + phone_number="919999999999", + user_type=User.TYPE_VALUE_MAP["Staff"], + verified=True, + asset=asset_obj, + age=10, + ) # The 123 makes it inaccesible without hashing + asset_user.save() + return asset_user diff --git a/config/health_views.py b/config/health_views.py index ba2d74dbeb..b3e2fc6d72 100644 --- a/config/health_views.py +++ b/config/health_views.py @@ -1,6 +1,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from care.users.api.serializers.user import UserBaseMinimumSerializer from config.authentication import MiddlewareAuthentication @@ -9,4 +10,4 @@ class MiddlewareAuthenticationVerifyView(APIView): authentication_classes = [MiddlewareAuthentication] def get(self, request): - return Response({}) + return Response(UserBaseMinimumSerializer(request.user).data) diff --git a/config/views.py b/config/views.py index 53dba552ae..4d7d7dab44 100644 --- a/config/views.py +++ b/config/views.py @@ -1,7 +1,3 @@ -import logging - -from django.views.generic import TemplateView - from django.shortcuts import render