Skip to content

Commit

Permalink
Add feature flags (#2429)
Browse files Browse the repository at this point in the history
* Added Facility Flag Implementation

* added caching and user flags

* Fix class name

* update migrations

* fixes

* format

* add tests

* fix tests

* Fix variable naming

* abstract feature flag model

* cleanup

* fix migrations

* fix failing tests

* Add merge migration

* Clean migrations

* Fix linting

* fix lint

---------

Co-authored-by: Aakash Singh <[email protected]>
  • Loading branch information
vigneshhari and sainak authored Sep 19, 2024
1 parent a06913e commit 2bb7357
Show file tree
Hide file tree
Showing 18 changed files with 563 additions and 4 deletions.
18 changes: 18 additions & 0 deletions care/facility/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from djangoql.admin import DjangoQLSearchMixin
Expand All @@ -12,12 +13,14 @@
PatientConsultation,
)
from care.facility.models.patient_sample import PatientSample
from care.utils.registries.feature_flag import FlagRegistry, FlagType

from .models import (
Building,
Disease,
Facility,
FacilityCapacity,
FacilityFlag,
FacilityInventoryItem,
FacilityInventoryItemTag,
FacilityInventoryUnit,
Expand Down Expand Up @@ -188,6 +191,19 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin):
actions = ["export_as_csv"]


class FacilityFlagAdmin(admin.ModelAdmin):
class FacilityFeatureFlagForm(forms.ModelForm):
flag = forms.ChoiceField(
choices=lambda: FlagRegistry.get_all_flags_as_choices(FlagType.FACILITY)
)

class Meta:
fields = "__all__"
model = FacilityFlag

form = FacilityFeatureFlagForm


admin.site.register(Facility, FacilityAdmin)
admin.site.register(FacilityStaff, FacilityStaffAdmin)
admin.site.register(FacilityCapacity, FacilityCapacityAdmin)
Expand Down Expand Up @@ -217,3 +233,5 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin):
admin.site.register(PatientConsent)
admin.site.register(FileUpload)
admin.site.register(PatientConsultation)

admin.site.register(FacilityFlag, FacilityFlagAdmin)
6 changes: 6 additions & 0 deletions care/facility/api/serializers/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ class FacilitySerializer(FacilityBasicInfoSerializer):
)
bed_count = serializers.SerializerMethodField()

facility_flags = serializers.SerializerMethodField()

def get_facility_flags(self, facility):
return facility.get_facility_flags()

class Meta:
model = Facility
fields = [
Expand Down Expand Up @@ -140,6 +145,7 @@ class Meta:
"read_cover_image_url",
"patient_count",
"bed_count",
"facility_flags",
]
read_only_fields = ("modified_date", "created_date")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 4.2.10 on 2024-09-19 12:58

import uuid

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("facility", "0457_patientmetainfo_domestic_healthcare_support_and_more"),
]

operations = [
migrations.CreateModel(
name="FacilityFlag",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"external_id",
models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
),
(
"created_date",
models.DateTimeField(auto_now_add=True, db_index=True, null=True),
),
(
"modified_date",
models.DateTimeField(auto_now=True, db_index=True, null=True),
),
("deleted", models.BooleanField(db_index=True, default=False)),
("flag", models.CharField(max_length=1024)),
(
"facility",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="facility.facility",
),
),
],
options={
"verbose_name": "Facility Flag",
},
),
migrations.AddConstraint(
model_name="facilityflag",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted", False)),
fields=("facility", "flag"),
name="unique_facility_flag",
),
),
]
1 change: 1 addition & 0 deletions care/facility/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .encounter_symptom import * # noqa
from .events import * # noqa
from .facility import * # noqa
from .facility_flag import * # noqa
from .icd11_diagnosis import * # noqa
from .inventory import * # noqa
from .patient import * # noqa
Expand Down
4 changes: 4 additions & 0 deletions care/facility/models/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from simple_history.models import HistoricalRecords

from care.facility.models import FacilityBaseModel, reverse_choices
from care.facility.models.facility_flag import FacilityFlag
from care.facility.models.mixins.permissions.facility import (
FacilityPermissionMixin,
FacilityRelatedPermissionMixin,
Expand Down Expand Up @@ -274,6 +275,9 @@ def get_features_display(self):
return []
return [FacilityFeature(f).label for f in self.features]

def get_facility_flags(self):
return FacilityFlag.get_all_flags(self.id)

CSV_MAPPING = {
"name": "Facility Name",
"facility_type": "Facility Type",
Expand Down
40 changes: 40 additions & 0 deletions care/facility/models/facility_flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.db import models

from care.utils.models.base import BaseFlag
from care.utils.registries.feature_flag import FlagName, FlagType

FACILITY_FLAG_CACHE_KEY = "facility_flag_cache:{facility_id}:{flag_name}"
FACILITY_ALL_FLAGS_CACHE_KEY = "facility_all_flags_cache:{facility_id}"
FACILITY_FLAG_CACHE_TTL = 60 * 60 * 24 # 1 Day


class FacilityFlag(BaseFlag):
facility = models.ForeignKey(
"facility.Facility", on_delete=models.CASCADE, null=False, blank=False
)

cache_key_template = "facility_flag_cache:{entity_id}:{flag_name}"
all_flags_cache_key_template = "facility_all_flags_cache:{entity_id}"
flag_type = FlagType.FACILITY
entity_field_name = "facility"

def __str__(self) -> str:
return f"Facility Flag: {self.facility.name} - {self.flag}"

class Meta:
verbose_name = "Facility Flag"
constraints = [
models.UniqueConstraint(
fields=["facility", "flag"],
condition=models.Q(deleted=False),
name="unique_facility_flag",
)
]

@classmethod
def check_facility_has_flag(cls, facility_id: int, flag_name: FlagName) -> bool:
return cls.check_entity_has_flag(facility_id, flag_name)

@classmethod
def get_all_flags(cls, facility_id: int) -> tuple[FlagName]:
return super().get_all_flags(facility_id)
55 changes: 55 additions & 0 deletions care/facility/tests/test_facility_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.db import IntegrityError
from rest_framework.test import APITestCase

from care.facility.models.facility_flag import FacilityFlag
from care.utils.registries.feature_flag import FlagRegistry, FlagType
from care.utils.tests.test_utils import TestUtils


class FacilityFlagsTestCase(TestUtils, APITestCase):
@classmethod
def setUpTestData(cls):
FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG")
FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG_2")
cls.district = cls.create_district(cls.create_state())
cls.local_body = cls.create_local_body(cls.district)
cls.super_user = cls.create_super_user("su", cls.district)

def setUp(self) -> None:
self.facility = self.create_facility(
self.super_user, self.district, self.local_body
)

def test_facility_flags(self):
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG")
self.assertTrue(
FacilityFlag.check_facility_has_flag(self.facility.id, "TEST_FLAG")
)

def test_facility_flags_negative(self):
self.assertFalse(
FacilityFlag.check_facility_has_flag(self.facility.id, "TEST_FLAG")
)

def test_create_duplicate_flag(self):
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG")
with self.assertRaises(IntegrityError):
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG")

def test_get_all_flags(self):
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG")
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG_2")
self.assertEqual(
FacilityFlag.get_all_flags(self.facility.id), ("TEST_FLAG", "TEST_FLAG_2")
)

def test_get_user_flags_api(self):
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG")
FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG_2")
user = self.create_user("user", self.district, home_facility=self.facility)
self.client.force_authenticate(user=user)
response = self.client.get(f"/api/v1/facility/{self.facility.external_id}/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json()["facility_flags"], ["TEST_FLAG", "TEST_FLAG_2"]
)
27 changes: 25 additions & 2 deletions care/users/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from djqscsv import render_to_csv_response

from care.users.forms import UserChangeForm, UserCreationForm
from care.users.models import District, LocalBody, Skill, State, UserSkill, Ward
from care.users.models import (
District,
LocalBody,
Skill,
State,
UserFlag,
UserSkill,
Ward,
)
from care.utils.registries.feature_flag import FlagRegistry, FlagType

User = get_user_model()

Expand Down Expand Up @@ -72,6 +82,19 @@ class WardAdmin(admin.ModelAdmin):
autocomplete_fields = ["local_body"]


admin.site.register(Skill)
@admin.register(UserFlag)
class UserFlagAdmin(admin.ModelAdmin):
class UserFlagForm(forms.ModelForm):
flag = forms.ChoiceField(
choices=lambda: FlagRegistry.get_all_flags_as_choices(FlagType.USER)
)

class Meta:
fields = "__all__"
model = UserFlag

form = UserFlagForm


admin.site.register(Skill)
admin.site.register(UserSkill)
6 changes: 6 additions & 0 deletions care/users/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ class UserSerializer(SignUpSerializer):

date_of_birth = serializers.DateField(required=True)

user_flags = serializers.SerializerMethodField()

def get_user_flags(self, user) -> tuple[str]:
return user.get_all_flags()

class Meta:
model = User
fields = (
Expand Down Expand Up @@ -316,6 +321,7 @@ class Meta:
"pf_endpoint",
"pf_p256dh",
"pf_auth",
"user_flags",
)
read_only_fields = (
"is_superuser",
Expand Down
62 changes: 62 additions & 0 deletions care/users/migrations/0017_userflag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 5.1.1 on 2024-09-19 12:22

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0016_upgrade_user_skills"),
]

operations = [
migrations.CreateModel(
name="UserFlag",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"external_id",
models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
),
(
"created_date",
models.DateTimeField(auto_now_add=True, db_index=True, null=True),
),
(
"modified_date",
models.DateTimeField(auto_now=True, db_index=True, null=True),
),
("deleted", models.BooleanField(db_index=True, default=False)),
("flag", models.CharField(max_length=1024)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "User Flag",
"constraints": [
models.UniqueConstraint(
condition=models.Q(("deleted", False)),
fields=("user", "flag"),
name="unique_user_flag",
)
],
},
),
]
Loading

0 comments on commit 2bb7357

Please sign in to comment.