Skip to content

Commit e9ec6aa

Browse files
committed
Add HeaProfileViewSet and UserViewSet api endpoints see HEA-580
1 parent 1296645 commit e9ec6aa

File tree

7 files changed

+246
-2
lines changed

7 files changed

+246
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 5.1.1 on 2024-11-17 15:34
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
import model_utils.fields
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
("auth", "0012_alter_user_first_name_max_length"),
14+
("common", "0009_countryclassifiedproductaliases_aliases"),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="HeaProfile",
20+
fields=[
21+
(
22+
"created",
23+
model_utils.fields.AutoCreatedField(
24+
default=django.utils.timezone.now, editable=False, verbose_name="created"
25+
),
26+
),
27+
(
28+
"modified",
29+
model_utils.fields.AutoLastModifiedField(
30+
default=django.utils.timezone.now, editable=False, verbose_name="modified"
31+
),
32+
),
33+
(
34+
"user",
35+
models.OneToOneField(
36+
on_delete=django.db.models.deletion.CASCADE,
37+
primary_key=True,
38+
serialize=False,
39+
to=settings.AUTH_USER_MODEL,
40+
),
41+
),
42+
("expert", models.BooleanField(default=False)),
43+
("skip_tour", models.BooleanField(default=False)),
44+
("tour_last_viewed", models.DateField(null=True)),
45+
("livelihood_explorer_data", models.JSONField(blank=True, default=dict, null=True)),
46+
],
47+
options={
48+
"verbose_name": "hea user profile",
49+
"verbose_name_plural": "hea user profiles",
50+
},
51+
),
52+
]

apps/common/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import operator
99
from functools import reduce
1010

11+
from django.contrib.auth.models import User
1112
from django.core import validators
1213
from django.core.cache import cache
1314
from django.core.exceptions import ObjectDoesNotExist, ValidationError
@@ -950,3 +951,23 @@ class Meta:
950951
fields=["country", "product"], name="common_countryclassified_country_code_product_code_uniq"
951952
)
952953
]
954+
955+
956+
class HeaProfile(Model):
957+
"""
958+
A profile to store data associated with a user to be used by the Livelihoods Explorer
959+
to create a dynamic user experience.
960+
"""
961+
962+
user = models.OneToOneField(User, on_delete=CASCADE, primary_key=True, unique=True)
963+
expert = models.BooleanField(default=False)
964+
skip_tour = models.BooleanField(default=False)
965+
tour_last_viewed = models.DateField(null=True)
966+
livelihood_explorer_data = models.JSONField(default=dict, null=True, blank=True)
967+
968+
def __str__(self):
969+
return f"hea_profile: {str(self.user)}"
970+
971+
class Meta:
972+
verbose_name = _("hea user profile")
973+
verbose_name_plural = _("hea user profiles")

apps/common/serializers.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from django.contrib.auth.models import User
12
from rest_framework import serializers
23

3-
from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure
4+
from .models import ClassifiedProduct, Country, Currency, HeaProfile, UnitOfMeasure
45

56

67
class CountrySerializer(serializers.ModelSerializer):
@@ -61,3 +62,37 @@ class Meta:
6162
"kcals_per_unit",
6263
"aliases",
6364
]
65+
66+
67+
class UserSerializer(serializers.ModelSerializer):
68+
class Meta:
69+
model = User
70+
fields = ["id", "username", "first_name", "last_name"]
71+
72+
73+
class CurrentUserSerializer(serializers.ModelSerializer):
74+
permissions = serializers.ListField(source="get_all_permissions", read_only=True)
75+
groups = serializers.SerializerMethodField()
76+
77+
def get_groups(self, user):
78+
return user.groups.values_list("name", flat=True)
79+
80+
class Meta:
81+
model = User
82+
fields = [
83+
"id",
84+
"username",
85+
"first_name",
86+
"last_name",
87+
"email",
88+
"permissions",
89+
"groups",
90+
"is_staff",
91+
"is_superuser",
92+
]
93+
94+
95+
class HeaProfileSerializer(serializers.ModelSerializer):
96+
class Meta:
97+
model = HeaProfile
98+
fields = ("user", "expert", "skip_tour", "tour_last_viewed", "livelihood_explorer_data")

apps/common/tests/factories.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ def groups(self, create, extracted, **kwargs):
6060
self.groups.add(group)
6161

6262

63+
class HeaProfileFactory(factory.django.DjangoModelFactory):
64+
class Meta:
65+
model = "common.HeaProfile"
66+
django_get_or_create = ("user",)
67+
68+
user = factory.SubFactory(UserFactory)
69+
70+
6371
class GroupFactory(factory.django.DjangoModelFactory):
6472
class Meta:
6573
model = "auth.Group"

apps/common/tests/test_viewsets.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ClassifiedProductFactory,
1010
CountryFactory,
1111
CurrencyFactory,
12+
HeaProfileFactory,
1213
UnitOfMeasureFactory,
1314
UserFactory,
1415
)
@@ -190,3 +191,59 @@ def test_search_fields(self):
190191
self.assertEqual(response.status_code, 200)
191192
result = json.loads(response.content.decode("utf-8"))
192193
self.assertEqual(len(result), 1)
194+
195+
196+
class UserViewSetTestCase(APITestCase):
197+
def setUp(self):
198+
self.user = UserFactory(username="testuser", password="password123", first_name="Test", last_name="User")
199+
self.client.force_authenticate(user=self.user)
200+
self.url = reverse("user-list")
201+
202+
def test_get_current_user(self):
203+
response = self.client.get(f"{self.url}current/")
204+
self.assertEqual(response.status_code, 200)
205+
self.assertEqual(response.data["username"], self.user.username)
206+
207+
def test_search_users(self):
208+
UserFactory(username="searchuser", password="password123", first_name="Search", last_name="User")
209+
response = self.client.get(self.url, {"search": "Search"})
210+
self.assertEqual(response.status_code, 200)
211+
self.assertEqual(len(response.data), 1)
212+
self.assertEqual(response.data[0]["first_name"], "Search")
213+
214+
215+
class HeaProfileViewSetTestCase(APITestCase):
216+
def setUp(self):
217+
self.user = UserFactory(username="testuser", password="password123")
218+
self.profile = HeaProfileFactory(user=self.user)
219+
self.client.force_authenticate(user=self.user)
220+
self.url = reverse("heaprofile-list")
221+
222+
def test_get_current_profile(self):
223+
response = self.client.get(f"{self.url}current/")
224+
self.assertEqual(response.status_code, 200)
225+
self.assertEqual(response.data["user"], self.user.id)
226+
227+
def test_superuser_access_profiles(self):
228+
superuser = UserFactory(username="admin", password="password123", is_superuser=True)
229+
self.client.force_authenticate(user=superuser)
230+
response = self.client.get(f"{self.url}{self.profile.user.id}/")
231+
self.assertEqual(response.status_code, 200)
232+
self.assertEqual(response.data["user"], self.user.id)
233+
234+
def test_queryset_filters(self):
235+
other_user = UserFactory(username="otheruser", password="password123")
236+
HeaProfileFactory(user=other_user)
237+
238+
# Current user profile only
239+
response = self.client.get(f"{self.url}?pk=current")
240+
self.assertEqual(response.status_code, 200)
241+
self.assertEqual(len(response.data), 1)
242+
self.assertEqual(response.data[0]["user"], self.user.id)
243+
244+
# Superuser access to all profiles
245+
superuser = UserFactory(username="admin", password="password123", is_superuser=True)
246+
self.client.force_authenticate(user=superuser)
247+
response = self.client.get(self.url)
248+
self.assertEqual(response.status_code, 200)
249+
self.assertGreaterEqual(len(response.data), 2)

apps/common/viewsets.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
from django.contrib.auth.models import User
12
from django.utils.text import format_lazy
23
from django.utils.translation import gettext_lazy as _
34
from django_filters import rest_framework as filters
45
from rest_framework import viewsets
6+
from rest_framework.decorators import action
57
from rest_framework.exceptions import NotAcceptable
68
from rest_framework.pagination import PageNumberPagination
9+
from rest_framework.permissions import BasePermission, IsAuthenticated
710

811
from .fields import translation_fields
912
from .filters import MultiFieldFilter
10-
from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure
13+
from .models import ClassifiedProduct, Country, Currency, HeaProfile, UnitOfMeasure
1114
from .serializers import (
1215
ClassifiedProductSerializer,
1316
CountrySerializer,
1417
CurrencySerializer,
18+
CurrentUserSerializer,
19+
HeaProfileSerializer,
1520
UnitOfMeasureSerializer,
21+
UserSerializer,
1622
)
1723

1824

@@ -323,3 +329,64 @@ class ClassifiedProductViewSet(BaseModelViewSet):
323329
*translation_fields("description"),
324330
*translation_fields("common_name"),
325331
)
332+
333+
334+
class CurrentUserOnly(BasePermission):
335+
def has_permission(self, request, view):
336+
if request.user.is_superuser:
337+
return True
338+
elif view.kwargs == {"pk": "current"}:
339+
# Even anonymous users can see their current user record
340+
return True
341+
elif request.query_params.get("pk") == "current":
342+
# List views seem to use query_params rather than kwargs
343+
return True
344+
return False
345+
346+
347+
class UserViewSet(BaseModelViewSet):
348+
"""
349+
Allows users to be viewed or edited.
350+
"""
351+
352+
queryset = User.objects.all()
353+
permission_classes = [CurrentUserOnly]
354+
serializer_class = UserSerializer
355+
search_fields = ["username", "first_name", "last_name"]
356+
357+
def get_object(self):
358+
pk = self.kwargs.get("pk")
359+
360+
if pk == "current":
361+
self.serializer_class = CurrentUserSerializer
362+
return self.request.user if self.request.user.id else User.get_anonymous()
363+
364+
return super().get_object()
365+
366+
@action(detail=True, methods=["get"])
367+
def current(self, request, *args, **kwargs):
368+
return self.retrieve(request, *args, **kwargs)
369+
370+
371+
class HeaProfileViewSet(BaseModelViewSet):
372+
queryset = HeaProfile.objects.all()
373+
serializer_class = HeaProfileSerializer
374+
permission_classes = [CurrentUserOnly, IsAuthenticated]
375+
376+
def get_object(self):
377+
pk = self.kwargs.get("pk")
378+
if pk == "current":
379+
return self.request.user.heaprofile if self.request.user.id else None
380+
return super().get_object()
381+
382+
def get_queryset(self):
383+
queryset = super().get_queryset()
384+
pk = self.request.query_params.get("pk") or self.kwargs.get("pk")
385+
386+
if pk == "current":
387+
return queryset.filter(user=self.request.user.id)
388+
elif pk:
389+
# Superusers can access profiles without using pk=current.
390+
return queryset.filter(user=pk)
391+
else:
392+
return queryset

hea/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
ClassifiedProductViewSet,
5050
CountryViewSet,
5151
CurrencyViewSet,
52+
HeaProfileViewSet,
5253
UnitOfMeasureViewSet,
54+
UserViewSet,
5355
)
5456
from metadata.viewsets import (
5557
HazardCategoryViewSet,
@@ -67,6 +69,8 @@
6769
router.register(r"currency", CurrencyViewSet)
6870
router.register(r"unitofmeasure", UnitOfMeasureViewSet)
6971
router.register(r"classifiedproduct", ClassifiedProductViewSet)
72+
router.register(r"user", UserViewSet)
73+
router.register(r"heaprofile", HeaProfileViewSet)
7074

7175
# Metadata
7276
router.register(r"livelihoodcategory", LivelihoodCategoryViewSet)

0 commit comments

Comments
 (0)