Skip to content

Commit

Permalink
Merge pull request GeriLife#41 from GeriLife/home-residents
Browse files Browse the repository at this point in the history
Add Home residents feature with activity level listing
  • Loading branch information
brylie authored Nov 28, 2023
2 parents 7180f8d + ea2e813 commit 6079264
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 16 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![Maintainability](https://api.codeclimate.com/v1/badges/76c12aa54357667b81b4/maintainability)](https://codeclimate.com/github/GeriLife/caregiving/maintainability)

# GeriLife Caregiving

A toolkit for caregivers working to promote wellness in elder-care communities.
Expand Down
10 changes: 10 additions & 0 deletions homes/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import factory
from .models import Home


class HomeFactory(factory.django.DjangoModelFactory):
class Meta:
model = Home
django_get_or_create = ("name",)

name: str = factory.Sequence(lambda n: f"Home {n}")
15 changes: 15 additions & 0 deletions homes/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import TYPE_CHECKING
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from shortuuid.django_fields import ShortUUIDField

if TYPE_CHECKING:
from residents.models import Resident


class Home(models.Model):
name = models.CharField(max_length=25)
Expand All @@ -22,3 +26,14 @@ def __str__(self) -> str:

def get_absolute_url(self):
return reverse("home-detail-view", kwargs={"url_uuid": self.url_uuid})

@property
def current_residents(self) -> models.QuerySet["Resident"]:
"""Returns a QuerySet of all current residents for this home."""
# avoid circular import
from residents.models import Resident

return Resident.objects.filter(
residency__home=self,
residency__move_out__isnull=True,
).order_by("first_name")
37 changes: 31 additions & 6 deletions homes/templates/homes/home_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,35 @@
{% block content %}
<h1>{{ home.name }}</h1>

<!-- only load analytics charts if work has been recorded -->
{% if work_has_been_recorded %}
{% include "homes/home_detail_charts.html" %}
{% else %}
<p>{% translate "No work has been recorded yet." %}</p>
{% endif %}
<div class="container">
<div class="row mb-3">
<div class="col-md-4">
{% if home.current_residents %}
<h2>{% translate "Current Residents" %}</h2>
<ul class="list-group">
{% for resident in home.current_residents %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ resident.get_absolute_url }}" class="stretched-link">{{ resident.full_name }}</a>
<span class="badge text-bg-{{ resident.activity_level.color }}">
{{ resident.activity_level.text }}
</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>

<div class="row">
<!-- only load analytics charts if work has been recorded -->
<h2>
{% translate "Work" %}
</h2>
{% if work_has_been_recorded %}
{% include "homes/home_detail_charts.html" %}
{% else %}
<p>{% translate "No work has been recorded yet." %}</p>
{% endif %}
</div>
</div>
{% endblock content %}
53 changes: 52 additions & 1 deletion homes/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# Create your tests here.
from django.test import TestCase

from .factories import HomeFactory
from residents.factories import ResidentFactory, ResidencyFactory


class HomeModelTests(TestCase):
def setUp(self):
# Create test data using factories
self.home1 = HomeFactory(name="Home 1")

self.home_1_current_resident = ResidentFactory(first_name="Alice")
self.home_1_past_resident = ResidentFactory(first_name="Bob")

ResidencyFactory(
resident=self.home_1_current_resident,
home=self.home1,
move_in="2020-01-01",
move_out=None,
)
ResidencyFactory(
resident=self.home_1_past_resident,
home=self.home1,
move_in="2020-01-02",
move_out="2020-02-01",
)

def test_home_current_residencies(self):
current_residencies_home1 = self.home1.current_residencies
self.assertEqual(current_residencies_home1.count(), 1)
self.assertTrue(
current_residencies_home1.filter(
resident=self.home_1_current_resident,
).exists(),
)
self.assertFalse(
current_residencies_home1.filter(
resident=self.home_1_past_resident,
).exists(),
)

def test_home_current_residents(self):
current_residents_home1 = self.home1.current_residents
self.assertEqual(current_residents_home1.count(), 1)
self.assertIn(
self.home_1_current_resident,
current_residents_home1,
)
self.assertNotIn(
self.home_1_past_resident,
current_residents_home1,
)
34 changes: 33 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ flake8 = "^4.0.1"
isort = "^5.10.1"
black = "^22.6.0"

[tool.poetry.group.dev.dependencies]
factory-boy = "^3.3.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
27 changes: 27 additions & 0 deletions residents/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import factory
import random
import string

from .models import Resident, Residency


class ResidentFactory(factory.django.DjangoModelFactory):
class Meta:
model = Resident
django_get_or_create = ("first_name", "last_initial")

first_name: str = factory.Sequence(lambda n: f"First {n}")
# choose a random alphabetical character for the last initial
last_initial = factory.LazyFunction(lambda: random.choice(string.ascii_uppercase))
url_uuid: str = factory.Sequence(lambda n: f"url-uuid-{n}")


class ResidencyFactory(factory.django.DjangoModelFactory):
class Meta:
model = Residency
django_get_or_create = ("resident", "home")

resident = factory.SubFactory(ResidentFactory)
home = factory.SubFactory("homes.factories.HomeFactory")
move_in = factory.Faker("date")
move_out = None
53 changes: 45 additions & 8 deletions residents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@
from homes.models import Home


# Activity ranges
#
# Based on the count of activities in the past seven days:
# - inactive: almost no activities
# - low: a few activities
# - good: an appropriate amount of activities
# - high: a lot of activities (maybe too many)
#
# Note: range ends are exclusive, so the max value is the same as the next
# range's min value.
WEEKLY_ACTIVITY_RANGES = {
"inactive": range(0, 1), # Includes only 0.
"low": range(1, 5), # Includes 1, 2, 3, 4.
"good": range(5, 10), # Includes 5, 6, 7, 8, 9.
"high": range(10, 1000), # Includes 10 onwards ... (1000 is arbitrary).
}


class Resident(models.Model):
first_name = models.CharField(max_length=255)
last_initial = models.CharField(max_length=1)
Expand Down Expand Up @@ -42,17 +60,36 @@ def activity_level(self):
- warning: 2-4
- success: 5+
"""

activity_count = self.activities.filter(
date__gte=timezone.now() - timezone.timedelta(days=7),
one_week_ago = timezone.now() - timezone.timedelta(days=7)
activity_count: int = self.activities.filter( # type: ignore
date__gte=one_week_ago,
).count()

if activity_count <= 1:
return "danger"
elif activity_count <= 4:
return "warning"
if self.on_hiatus:
return {
"color": "info",
"text": _("On hiatus"),
}
elif activity_count in WEEKLY_ACTIVITY_RANGES["inactive"]:
return {
"color": "danger",
"text": _("Inactive"),
}
elif activity_count in WEEKLY_ACTIVITY_RANGES["low"]:
return {
"color": "warning",
"text": _("Low"),
}
elif activity_count in WEEKLY_ACTIVITY_RANGES["good"]:
return {
"color": "success",
"text": _("Moderate"),
}
else:
return "success"
return {
"color": "warning",
"text": _("High"),
}


class Residency(models.Model):
Expand Down
43 changes: 43 additions & 0 deletions residents/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from unittest.mock import MagicMock, patch

from .models import Residency, Resident
from homes.models import Home
Expand Down Expand Up @@ -60,3 +61,45 @@ def test_overlapping_residency_not_allowed(self):
# saving a model instance
# https://docs.djangoproject.com/en/4.0/ref/models/instances/#django.db.models.Model.clean
overlapping_residency.clean()


class TestResidentActivityLevel(TestCase):
def setUp(self):
self.resident = Resident.objects.create(first_name="John", last_initial="W")

def test_activity_levels(self):
expected_results = {
0: {"color": "danger", "text": "Inactive"},
1: {"color": "warning", "text": "Low"},
2: {"color": "warning", "text": "Low"},
3: {"color": "warning", "text": "Low"},
4: {"color": "warning", "text": "Low"},
5: {"color": "success", "text": "Moderate"},
6: {"color": "success", "text": "Moderate"},
7: {"color": "success", "text": "Moderate"},
8: {"color": "success", "text": "Moderate"},
9: {"color": "success", "text": "Moderate"},
10: {"color": "warning", "text": "High"},
11: {"color": "warning", "text": "High"},
12: {"color": "warning", "text": "High"},
13: {"color": "warning", "text": "High"},
14: {"color": "warning", "text": "High"},
}

for count in range(0, 15):
with self.subTest(count=count):
with patch(
"residents.models.Resident.activities",
new_callable=MagicMock,
) as mock_activities:
# Set up the method chain to return the correct count
mock_activities.filter.return_value.count.return_value = count

# Test the actual functionality
expected = expected_results.get(count)
self.assertEqual(self.resident.activity_level, expected)

def test_on_hiatus(self):
self.resident.on_hiatus = True
expected = {"color": "info", "text": "On hiatus"}
self.assertEqual(self.resident.activity_level, expected)

0 comments on commit 6079264

Please sign in to comment.