Skip to content

Commit

Permalink
feat: limit users based on allotted facilities
Browse files Browse the repository at this point in the history
  • Loading branch information
kennedykori committed Sep 17, 2021
1 parent 5aa4901 commit f460ca0
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 134 deletions.
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
"fahari.common.filters.OrganisationFilterBackend",
"fahari.common.filters.AllottedFacilitiesFilterBackend",
),
"DEFAULT_PAGINATION_CLASS": (
"rest_framework_datatables.pagination.DatatablesPageNumberPagination"
Expand Down
19 changes: 19 additions & 0 deletions fahari/common/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ def filter_queryset(self, request, queryset, view):
return queryset.filter(organisation=request.user.organisation)


class AllottedFacilitiesFilterBackend(filters.BaseFilterBackend):
"""Users are only allowed to view records relating to facilities they have been allotted to.
For this to work, the attribute `facility_field_lookup` must be present on a view. The
attribute should contain a lookup to a facility and should be usable from the queryset
returned by the view. If the aforementioned attribute is not present on a view, then no
filtering is performed and the queryset is returned as is.
"""

def filter_queryset(self, request, queryset, view):
"""Filter records to"""
lookup = getattr(view, "facility_field_lookup", None)
if not lookup:
return queryset
allotted_facilities = UserFacilityAllotment.get_facilities_for_user(request.user)
qs_filter = {"%s__in" % lookup: allotted_facilities}
return queryset.filter(**qs_filter)


class FacilityFilter(CommonFieldsFilterset):
"""Filter facilities."""

Expand Down
4 changes: 4 additions & 0 deletions fahari/common/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
SystemForm,
UserFacilityAllotmentForm,
)
from .mixins import FormContextMixin, GetAllottedFacilitiesMixin, GetUserFromContextMixin

__all__ = [
"BaseModelForm",
"FacilityAttachmentForm",
"FacilityForm",
"FacilityUserForm",
"FormContextMixin",
"GetAllottedFacilitiesMixin",
"GetUserFromContextMixin",
"OrganisationForm",
"SystemForm",
"UserFacilityAllotmentForm",
Expand Down
2 changes: 1 addition & 1 deletion fahari/common/forms/base_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .mixins import FormContextMixin


class BaseModelForm(ModelForm, FormContextMixin):
class BaseModelForm(FormContextMixin, ModelForm):
"""Base form for the majority of model forms in the project."""

def __init__(self, *args, **kwargs):
Expand Down
51 changes: 45 additions & 6 deletions fahari/common/forms/mixins.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,53 @@
from typing import Any, Dict
from typing import Any, Dict, Optional, Union

from django.forms import Form
from django.contrib.auth import get_user_model
from django.db.models import Model
from django.forms import ModelForm

from ..models import Facility, UserFacilityAllotment
from ..models.common_models import FacilityManager, FacilityQuerySet

class FormContextMixin(Form):
def __init__(self, context: Dict[str, Any] = None, *args, **kwargs):
User = get_user_model()


class FormContextMixin(ModelForm):
"""Add context to a form."""

def __init__(self, *args, **kwargs):
self._context: Dict[str, Any] = kwargs.pop("context", {})
super().__init__(*args, **kwargs)
self._context: Dict[str, Any] = context or {}

@property
def context(self):
def context(self) -> Dict[str, Any]:
"""Return the context as passed on the form during initialization."""

return self._context


class GetUserFromContextMixin(FormContextMixin):
"""Add a method to retrieve the logged in user from a form's context."""

def get_logged_in_user(self) -> Optional[Model]:
"""Determine and return the logged in user from the context."""

request = self.context.get("request", None)
user = getattr(request, "user", None) if request else None
return user


class GetAllottedFacilitiesMixin(GetUserFromContextMixin):
"""Add a method to retrieve the allotted facilities for the logged-in user.
The logged-in user is determined from a form's context data.
"""

def get_allotted_facilities(self) -> Union[FacilityManager, FacilityQuerySet]:
"""Return a queryset consisting of all the allotted facilities of the logged in user.
If the logged in user cannot be determined, then an empty queryset is returned instead.
"""

user = self.get_logged_in_user()
if not user:
return Facility.objects.none()
return UserFacilityAllotment.get_facilities_for_user(user)
31 changes: 18 additions & 13 deletions fahari/common/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ def setUp(self):
self.user.organisation = self.global_organisation
self.user.save()

# Allot the given users to all the facilities in FYJ counties
self.user_facility_allotment = baker.make(
UserFacilityAllotment,
allotment_type=UserFacilityAllotment.AllotmentType.BY_REGION.value,
counties=WHITELIST_COUNTIES,
organisation=self.global_organisation,
region_type=UserFacilityAllotment.RegionType.COUNTY.value,
user=self.user,
)

assert self.client.login(username=username, password="pass123") is True

self.patch_organisation = partial(
Expand Down Expand Up @@ -610,10 +620,11 @@ def setUp(self):
)

def test_create(self):
user = baker.make(get_user_model(), organisation=self.global_organisation)
data = {
"allotment_type": self.by_facility.value,
"facilities": map(lambda f: f.pk, self.facilities),
"user": self.user.pk,
"user": user.pk,
"organisation": self.global_organisation.pk,
}
response = self.client.post(reverse("common:user_facility_allotment_create"), data=data)
Expand All @@ -623,21 +634,14 @@ def test_create(self):
)

def test_update(self):
instance: UserFacilityAllotment = baker.make(
UserFacilityAllotment,
allotment_type=self.by_facility.value,
facilities=self.facilities,
organisation=self.global_organisation,
user=self.user,
)
instance = self.user_facility_allotment
data = {
"pk": instance.pk,
"allotment_type": self.by_region.value,
"region_type": UserFacilityAllotment.RegionType.COUNTY.value,
"counties": ["Nairobi"],
"allotment_type": self.by_facility.value,
"facilities": map(lambda f: f.pk, self.facilities),
"user": self.user.pk,
"organisation": self.global_organisation.pk,
"active": True,
"active": False,
}
response = self.client.post(
reverse("common:user_facility_allotment_update", kwargs={"pk": instance.pk}), data=data
Expand All @@ -648,12 +652,13 @@ def test_update(self):
)

def test_delete(self):
user = baker.make(get_user_model(), organisation=self.global_organisation)
instance: UserFacilityAllotment = baker.make(
UserFacilityAllotment,
allotment_type=self.by_facility.value,
facilities=self.facilities,
organisation=self.global_organisation,
user=self.user,
user=user,
)
response = self.client.post(
reverse("common:user_facility_allotment_delete", kwargs={"pk": instance.pk}),
Expand Down
4 changes: 2 additions & 2 deletions fahari/common/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
UserFacilityAllotmentView,
UserFacilityViewSet,
)
from .mixins import ApprovedMixin, BaseFormMixin, GetKwargsMixin
from .mixins import ApprovedMixin, BaseFormMixin, FormContextMixin

__all__ = [
"AboutView",
Expand All @@ -40,7 +40,7 @@
"FacilityUserViewSet",
"FacilityView",
"FacilityViewSet",
"GetKwargsMixin",
"FormContextMixin",
"HomeView",
"SystemCreateView",
"SystemDeleteView",
Expand Down
1 change: 1 addition & 0 deletions fahari/common/views/common_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class FacilityViewSet(BaseView):
"mfl_code",
"registration_number",
)
facility_field_lookup = "pk"


class SystemContextMixin:
Expand Down
22 changes: 19 additions & 3 deletions fahari/common/views/mixins.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any, Dict

from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import (
LoginRequiredMixin,
Expand Down Expand Up @@ -37,8 +39,22 @@ def form_valid(self, form):
return super().form_valid(form)


class GetKwargsMixin(ModelFormMixin, LoginRequiredMixin, View):
def get_form_kwargs(self):
class FormContextMixin(ModelFormMixin, LoginRequiredMixin, View):
"""Mixin to inject context data into a form."""

def get_form_context(self) -> Dict[str, Any]:
"""Return the data to be used as a form's context."""

return {
"view": self,
"args": getattr(self, "args", ()),
"kwargs": getattr(self, "kwargs", {}),
"request": getattr(self, "request", None),
}

def get_form_kwargs(self) -> Dict[str, Any]:
"""Extend the base implementation to add context data on the returned form kwargs."""

kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
kwargs["context"] = self.get_form_context()
return kwargs

0 comments on commit f460ca0

Please sign in to comment.