Skip to content

Commit

Permalink
Merge pull request #130 from FEWS-NET/HEA-580/Add-HEA-user-profile-mo…
Browse files Browse the repository at this point in the history
…dels-and-endpoints-similar-to-the-FDW-ones

Add HeaProfileViewSet and UserViewSet api endpoints see HEA-580
  • Loading branch information
rhunwicks authored Nov 22, 2024
2 parents bd9f36f + 3c49812 commit 2c0270d
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 2 deletions.
49 changes: 49 additions & 0 deletions apps/common/migrations/0010_userprofile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 5.0.2 on 2024-11-22 06:07

import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("common", "0009_countryclassifiedproductaliases_aliases"),
]

operations = [
migrations.CreateModel(
name="UserProfile",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
("profile_data", models.JSONField(blank=True, default=dict, null=True)),
],
options={
"verbose_name": "user profile",
"verbose_name_plural": "user profiles",
},
),
]
17 changes: 17 additions & 0 deletions apps/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import operator
from functools import reduce

from django.contrib.auth.models import User
from django.core import validators
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
Expand Down Expand Up @@ -950,3 +951,19 @@ class Meta:
fields=["country", "product"], name="common_countryclassified_country_code_product_code_uniq"
)
]


class UserProfile(Model):
"""
A profile to store data associated with a user to enable a customized user experience
"""

user = models.OneToOneField(User, on_delete=CASCADE, primary_key=True, unique=True)
profile_data = models.JSONField(default=dict, null=True, blank=True)

def __str__(self):
return f"user_profile: {str(self.user)}"

class Meta:
verbose_name = _("user profile")
verbose_name_plural = _("user profiles")
37 changes: 36 additions & 1 deletion apps/common/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth.models import User
from rest_framework import serializers

from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure
from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure, UserProfile


class CountrySerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -61,3 +62,37 @@ class Meta:
"kcals_per_unit",
"aliases",
]


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]


class CurrentUserSerializer(serializers.ModelSerializer):
permissions = serializers.ListField(source="get_all_permissions", read_only=True)
groups = serializers.SerializerMethodField()

def get_groups(self, user):
return user.groups.values_list("name", flat=True)

class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"permissions",
"groups",
"is_staff",
"is_superuser",
]


class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ("user", "profile_data")
8 changes: 8 additions & 0 deletions apps/common/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ def groups(self, create, extracted, **kwargs):
self.groups.add(group)


class UserProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = "common.UserProfile"
django_get_or_create = ("user",)

user = factory.SubFactory(UserFactory)


class GroupFactory(factory.django.DjangoModelFactory):
class Meta:
model = "auth.Group"
Expand Down
57 changes: 57 additions & 0 deletions apps/common/tests/test_viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CurrencyFactory,
UnitOfMeasureFactory,
UserFactory,
UserProfileFactory,
)


Expand Down Expand Up @@ -190,3 +191,59 @@ def test_search_fields(self):
self.assertEqual(response.status_code, 200)
result = json.loads(response.content.decode("utf-8"))
self.assertEqual(len(result), 1)


class UserViewSetTestCase(APITestCase):
def setUp(self):
self.user = UserFactory(username="testuser", password="password123", first_name="Test", last_name="User")
self.client.force_authenticate(user=self.user)
self.url = reverse("user-list")

def test_get_current_user(self):
response = self.client.get(f"{self.url}current/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["username"], self.user.username)

def test_search_users(self):
UserFactory(username="searchuser", password="password123", first_name="Search", last_name="User")
response = self.client.get(self.url, {"search": "Search"})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["first_name"], "Search")


class UserProfileViewSetTestCase(APITestCase):
def setUp(self):
self.user = UserFactory(username="testuser", password="password123")
self.profile = UserProfileFactory(user=self.user)
self.client.force_authenticate(user=self.user)
self.url = reverse("userprofile-list")

def test_get_current_profile(self):
response = self.client.get(f"{self.url}current/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["user"], self.user.id)

def test_superuser_access_profiles(self):
superuser = UserFactory(username="admin", password="password123", is_superuser=True)
self.client.force_authenticate(user=superuser)
response = self.client.get(f"{self.url}{self.profile.user.id}/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["user"], self.user.id)

def test_queryset_filters(self):
other_user = UserFactory(username="otheruser", password="password123")
UserProfileFactory(user=other_user)

# Current user profile only
response = self.client.get(f"{self.url}?pk=current")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["user"], self.user.id)

# Superuser access to all profiles
superuser = UserFactory(username="admin", password="password123", is_superuser=True)
self.client.force_authenticate(user=superuser)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertGreaterEqual(len(response.data), 2)
69 changes: 68 additions & 1 deletion apps/common/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
from django.contrib.auth.models import User
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as filters
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import NotAcceptable
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission, IsAuthenticated

from .fields import translation_fields
from .filters import MultiFieldFilter
from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure
from .models import ClassifiedProduct, Country, Currency, UnitOfMeasure, UserProfile
from .serializers import (
ClassifiedProductSerializer,
CountrySerializer,
CurrencySerializer,
CurrentUserSerializer,
UnitOfMeasureSerializer,
UserProfileSerializer,
UserSerializer,
)


Expand Down Expand Up @@ -323,3 +329,64 @@ class ClassifiedProductViewSet(BaseModelViewSet):
*translation_fields("description"),
*translation_fields("common_name"),
)


class CurrentUserOnly(BasePermission):
def has_permission(self, request, view):
if request.user.is_superuser:
return True
elif view.kwargs == {"pk": "current"}:
# Even anonymous users can see their current user record
return True
elif request.query_params.get("pk") == "current":
# List views seem to use query_params rather than kwargs
return True
return False


class UserViewSet(BaseModelViewSet):
"""
Allows users to be viewed or edited.
"""

queryset = User.objects.all()
permission_classes = [CurrentUserOnly]
serializer_class = UserSerializer
search_fields = ["username", "first_name", "last_name"]

def get_object(self):
pk = self.kwargs.get("pk")

if pk == "current":
self.serializer_class = CurrentUserSerializer
return self.request.user if self.request.user.id else User.get_anonymous()

return super().get_object()

@action(detail=True, methods=["get"])
def current(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)


class UserProfileViewSet(BaseModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [CurrentUserOnly, IsAuthenticated]

def get_object(self):
pk = self.kwargs.get("pk")
if pk == "current":
return self.request.user.userprofile if self.request.user.id else None
return super().get_object()

def get_queryset(self):
queryset = super().get_queryset()
pk = self.request.query_params.get("pk") or self.kwargs.get("pk")

if pk == "current":
return queryset.filter(user=self.request.user.id)
elif pk:
# Superusers can access profiles without using pk=current.
return queryset.filter(user=pk)
else:
return queryset
4 changes: 4 additions & 0 deletions hea/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
CountryViewSet,
CurrencyViewSet,
UnitOfMeasureViewSet,
UserProfileViewSet,
UserViewSet,
)
from metadata.viewsets import (
HazardCategoryViewSet,
Expand All @@ -67,6 +69,8 @@
router.register(r"currency", CurrencyViewSet)
router.register(r"unitofmeasure", UnitOfMeasureViewSet)
router.register(r"classifiedproduct", ClassifiedProductViewSet)
router.register(r"user", UserViewSet)
router.register(r"userprofile", UserProfileViewSet)

# Metadata
router.register(r"livelihoodcategory", LivelihoodCategoryViewSet)
Expand Down

0 comments on commit 2c0270d

Please sign in to comment.