Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature flags #2429

Merged
merged 20 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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}"

Check warning on line 22 in care/facility/models/facility_flag.py

View check run for this annotation

Codecov / codecov/patch

care/facility/models/facility_flag.py#L22

Added line #L22 was not covered by tests

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