Skip to content

Commit

Permalink
Merge pull request #86 from GeriLife/add-home-user-assignment
Browse files Browse the repository at this point in the history
Add home userassignment
  • Loading branch information
brylie authored Jan 5, 2024
2 parents a13d8bc + 2bae701 commit 4e68c52
Show file tree
Hide file tree
Showing 19 changed files with 931 additions and 59 deletions.
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ such as bug reports, ideas, design, testing, and code.
- [Migrations](#migrations)
- [Create superuser](#create-superuser)
- [Run the server](#run-the-server)
- [Privacy and Data Protection Guidelines](#privacy-and-data-protection-guidelines)

## Community Discussions

Expand Down Expand Up @@ -137,3 +138,29 @@ When all migrations are applied and you have a superuser, run the server as foll
```sh
python manage.py runserver
```

## Privacy and Data Protection Guidelines

As an open-source community committed to upholding the highest standards of privacy and data security, we align our practices with principles derived from the General Data Protection Regulation (GDPR) and other similar privacy frameworks. While some GDPR principles are more relevant at an organizational level, many can be directly applied to software development, especially in features involving user data. Below are key guidelines that contributors should follow:

1. **Data Minimization:** Only collect data that is essential for the intended functionality. Avoid unnecessary collection of personal information. When in doubt, less is more.

2. **Consent and Transparency:** Ensure that the software provides clear mechanisms for obtaining user consent where applicable. Users should be informed about what data is collected, why it is collected, and how it will be used.

3. **Anonymization and Pseudonymization:** Where possible, anonymize or pseudonymize personal data to reduce privacy risks. This is particularly crucial in datasets that may be publicly released or shared.

4. **Security by Design:** Integrate data protection features from the earliest stages of development. This includes implementing robust encryption, access controls, and secure data storage practices.

5. **Access Control:** Limit access to personal data to only those components or personnel who strictly need it for processing. Implement appropriate authentication and authorization mechanisms.

6. **Data Portability:** Facilitate easy extraction and transfer of data in a common format, allowing users to move their data between different services seamlessly.

7. **User Rights:** Respect user rights such as the right to access their data, the right to rectify inaccuracies, and the right to erasure (‘right to be forgotten’).

8. **Regular Audits and Updates:** Regularly review and update the software to address emerging security vulnerabilities and ensure compliance with evolving data protection laws.

9. **Documentation and Compliance:** Document data flows and privacy measures. While the software itself may not be directly subject to GDPR, good documentation practices help downstream users to achieve compliance.

10. **Community Awareness:** Encourage a culture of privacy awareness and compliance within the community. Contributors should stay informed about data protection best practices and legal requirements.

Remember, adhering to these guidelines not only helps in compliance with regulations like the GDPR but also builds trust with users and the broader community. As contributors, your commitment to these principles is invaluable in fostering a responsible and privacy-conscious software ecosystem.
11 changes: 11 additions & 0 deletions accounts/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import factory

from .models import User


class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User

username = factory.Sequence(lambda n: f"user{n}")
email = factory.Faker("email")
43 changes: 43 additions & 0 deletions accounts/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.db.models import QuerySet
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

Expand All @@ -10,3 +11,45 @@ class Meta:

def __str__(self):
return self.username

def get_full_name(self) -> str:
return self.first_name + " " + self.last_name

@property
def homes(self) -> QuerySet["homes.Home"]:
from homes.models import Home

return Home.objects.filter(home_user_relations__user=self)

@property
def can_add_activity(self) -> bool:
"""Return True if the user can add an activity.
A user can add an activity if they are a superuser or if they
are associated with at least one home.
"""
return self.is_superuser or self.homes.exists()

def can_manage_residents(self, resident_ids: list[int]) -> bool:
"""Return True if the user can manage the residents.
A user can manage the residents if they are a superuser or if
they are associated with all of the residents' homes.
"""
from residents.models import Resident
from homes.models import HomeUserRelation

if self.is_superuser:
return True

residents = Resident.objects.filter(id__in=resident_ids)

for resident in residents:
# Check if the user is associated with the resident's home
if HomeUserRelation.objects.filter(
home=resident.current_home,
user=self,
).exists():
return True

return False
59 changes: 58 additions & 1 deletion accounts/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,58 @@
# Create your tests here.
from django.test import TestCase
from django.contrib.auth import get_user_model

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


User = get_user_model()


class CanManageResidentsTest(TestCase):
def setUp(self):
self.superuser = User.objects.create_superuser(
username="superuser",
email="[email protected]",
password="testpassword",
)
self.regular_user = User.objects.create_user(
username="regularuser",
email="[email protected]",
password="testpassword",
)

self.home1 = HomeFactory()
self.home2 = HomeFactory()

self.resident1 = ResidentFactory()
self.resident2 = ResidentFactory()

ResidencyFactory(
resident=self.resident1,
home=self.home1,
)
ResidencyFactory(
resident=self.resident2,
home=self.home2,
)

HomeUserRelationFactory(
home=self.home1,
user=self.regular_user,
)

def test_superuser_can_manage_all_residents(self):
resident_ids = [self.resident1.id, self.resident2.id]
self.assertTrue(self.superuser.can_manage_residents(resident_ids))

def test_regular_user_can_manage_associated_residents(self):
resident_ids = [
self.resident1.id,
]
self.assertTrue(self.regular_user.can_manage_residents(resident_ids))

def test_regular_user_cannot_manage_unassociated_residents(self):
resident_ids = [
self.resident2.id,
]
self.assertFalse(self.regular_user.can_manage_residents(resident_ids))
30 changes: 27 additions & 3 deletions activities/views.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import uuid

from django.db import transaction
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import ListView, FormView
from django.db import transaction

from metrics.models import ResidentActivity
from residents.models import Residency, Resident
from metrics.forms import ResidentActivityForm


class ResidentActivityListView(ListView):
class ResidentActivityListView(LoginRequiredMixin, ListView):
template_name = "activities/resident_activity_list.html"
queryset = ResidentActivity.objects.all()
context_object_name = "activities"
paginate_by = 100
ordering = ["-activity_date"]


class ResidentActivityFormView(FormView):
class ResidentActivityFormView(LoginRequiredMixin, FormView):
template_name = "activities/resident_activity_form.html"
form_class = ResidentActivityForm
success_url = reverse_lazy("activity-list-view")

# Check whether request user is authorized to view this page
def dispatch(self, request, *args, **kwargs):
if not request.user.can_add_activity:
return self.handle_no_permission()

return super().dispatch(request, *args, **kwargs)

def get_form_kwargs(self):
"""Override the get_form_kwargs method to pass the user to the form.
This will allow the form to filter the residents by the user's
homes or the superuser to filter by all homes.
"""

kwargs = super().get_form_kwargs()

kwargs["user"] = self.request.user

return kwargs

@transaction.atomic
def post(self, request, *args, **kwargs):
"""Override the post method to add the resident activity in the same
Expand All @@ -38,6 +59,9 @@ def post(self, request, *args, **kwargs):
# generate group activity ID based on current epoch time
group_activity_id = uuid.uuid4()

if not request.user.can_manage_residents(resident_ids):
return self.handle_no_permission()

for resident_id in resident_ids:
try:
resident = Resident.objects.get(id=resident_id)
Expand Down
20 changes: 16 additions & 4 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,22 @@
SECRET_KEY = "django-insecure-+24wlkd-xp!1)z)9#2=3gk+fhv-r9mo4*(kcfc=drz2=68m^-r"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[])
CSRF_TRUSTED_ORIGINS = env.list("DJANGO_CSRF_TRUSTED_ORIGINS", default=[])
DEBUG = env.bool(
"DJANGO_DEBUG",
default=True,
)

ALLOWED_HOSTS = env.list(
"DJANGO_ALLOWED_HOSTS",
default=[
"localhost",
"127.0.0.1",
],
)
CSRF_TRUSTED_ORIGINS = env.list(
"DJANGO_CSRF_TRUSTED_ORIGINS",
default=[],
)

INSTALLED_APPS = [
"django.contrib.admin",
Expand Down
8 changes: 7 additions & 1 deletion homes/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from django.contrib import admin

from .models import Home, HomeGroup
from .models import Home, HomeGroup, HomeUserRelation


@admin.register(Home)
class HomeAdmin(admin.ModelAdmin):
pass


# register the HomeUserRelation model
@admin.register(HomeUserRelation)
class HomeUserRelationAdmin(admin.ModelAdmin):
pass


# register the HomeGroup model
@admin.register(HomeGroup)
class HomeGroupAdmin(admin.ModelAdmin):
Expand Down
21 changes: 20 additions & 1 deletion homes/factories.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import factory
from factory import Sequence

from .models import Home, HomeGroup, HomeUserRelation


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

name: str = Sequence(lambda n: f"Home {n}")


class HomeGroupFactory(factory.django.DjangoModelFactory):
class Meta:
model = HomeGroup
django_get_or_create = ("name",)

name: str = Sequence(lambda n: f"Home Group {n}")


class HomeUserRelationFactory(factory.django.DjangoModelFactory):
class Meta:
model = HomeUserRelation
django_get_or_create = ("home", "user")

home: Home = factory.SubFactory(HomeFactory)
user: Home = factory.SubFactory(HomeFactory)
30 changes: 30 additions & 0 deletions homes/migrations/0007_homeuserrelation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.0 on 2024-01-04 19:24

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


class Migration(migrations.Migration):

dependencies = [
('homes', '0006_alter_home_home_group'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='HomeUserRelation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('home', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='home_user_relations', to='homes.home')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='home_user_relations', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'home user relation',
'verbose_name_plural': 'home user relations',
'db_table': 'home_user_relation',
'unique_together': {('user', 'home')},
},
),
]
Loading

0 comments on commit 4e68c52

Please sign in to comment.